L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Design et création · SKILL.md

nosql-expert

nosql-expert

Expert guidance for distributed NoSQL databases (Cassandra, DynamoDB, ScyllaDB). Covers query-first modeling, partition key design, hot partition prevention, single-table design, and BASE vs ACID tradeoffs. Triggers on: 'Cassandra', 'DynamoDB', 'NoSQL', 'partition key', 'hot partition', 'single-table design', 'GSI', 'LSI', 'WCU', 'RCU', 'ScyllaDB'.

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: nosql-expert description: "Expert guidance for distributed NoSQL databases (Cassandra, DynamoDB, ScyllaDB). Covers query-first modeling, partition key design, hot partition prevention, single-table design, and BASE vs ACID tradeoffs. Triggers on: 'Cassandra', 'DynamoDB', 'NoSQL', 'partition key', 'hot partition', 'single-table design', 'GSI', 'LSI', 'WCU', 'RCU', 'ScyllaDB'." argument-hint: "[entity, access pattern, or problem description]" allowed-tools: Read context: fork metadata: triggers: Cassandra, DynamoDB, ScyllaDB, NoSQL, partition key, hot partition, single-table design, GSI, LSI, WCU, RCU, ALLOW FILTERING, clustering key, adjacency list, BASE consistency, eventual consistency, distributed database related-skills: database-schema-designer, sql-pro, architecture-design domain: infrastructure role: specialist scope: design output-format: document last-reviewed: "2026-03-15"

Iron Law

NEVER DESIGN A NOSQL SCHEMA WITHOUT LISTING ALL ACCESS PATTERNS FIRST — A TABLE DESIGNED FOR THE WRONG QUERIES REQUIRES A FULL MIGRATION


When to Use This Skill

Load this skill when:

  • Designing or reviewing a Cassandra, ScyllaDB, or DynamoDB schema
  • Evaluating partition key choices or clustering column order
  • Diagnosing hot partitions, Scan operations, or ALLOW FILTERING usage
  • Deciding between single-table design and multi-table design in DynamoDB
  • Modeling access patterns for a new feature on a distributed NoSQL store
  • Migrating a relational model to a NoSQL model

The Mental Shift: SQL vs Distributed NoSQL

FeatureSQL (Relational)Distributed NoSQL (Cassandra/DynamoDB)
Data modelingModel Entities + RelationshipsModel Queries (Access Patterns)
JoinsCPU-intensive, resolved at read timePre-computed (denormalized) at write time
Storage costExpensive — minimize duplicationCheap — duplicate data for read speed
ConsistencyACID (Strong)BASE (Eventual) / Tunable
ScalabilityVertical (bigger machine)Horizontal (more nodes/shards)
Schema changesALTER TABLE, migrationsOften requires new table or full migration
AggregatesCOUNT, SUM, GROUP BYPre-calculate in separate counter tables

The golden rule: In relational design, you model entities. In distributed NoSQL, you model the queries your application will execute.


Core Design Patterns

Pattern 1: Query-First Modeling

Before touching a schema tool, list every access pattern:

Access Patterns:
1. Get user by user_id
2. Get all orders for a user, sorted by created_at DESC
3. Get all orders in status PENDING for a given tenant
4. Get order by order_id

Each row in that list becomes either a primary key structure or an index. If a pattern has no corresponding key or index, it cannot be served efficiently — fix the schema before writing code.

Pattern 2: Partition Key Design for Even Distribution

The partition key determines which node holds the data. Bad cardinality causes hot partitions.

-- BAD: status has 3-5 values — most traffic lands on one partition
PRIMARY KEY (status, created_at)

-- GOOD: user_id has millions of unique values — traffic spreads evenly
PRIMARY KEY (user_id, created_at)

-- GOOD: bucket sharding when one natural key is too large
-- Split USER#123 data across date buckets
PRIMARY KEY ((user_id, bucket), created_at)  -- bucket = YYYY-MM

Cardinality target: the partition key should have enough unique values that no single value represents more than 1-2% of total traffic.

Pattern 3: Clustering Column Ordering for Range Queries

Clustering columns define the sort order within a partition. Design them to match your range query direction.

-- Cassandra: retrieve a user's messages, newest first
CREATE TABLE messages_by_user (
    user_id     UUID,
    sent_at     TIMESTAMP,
    message_id  UUID,
    body        TEXT,
    PRIMARY KEY ((user_id), sent_at, message_id)
) WITH CLUSTERING ORDER BY (sent_at DESC, message_id ASC);

-- Query maps directly to the key structure — no ALLOW FILTERING needed
SELECT * FROM messages_by_user
WHERE user_id = ? AND sent_at > ?
LIMIT 20;

Pattern 4: Denormalization — Duplicate for Read Speed

In distributed NoSQL, writes are cheap. Storage is cheap. Joins are impossible at scale. Duplicate data across tables so every read is a single-partition lookup.

-- Relational: one Author table, one Book table, JOIN at read time
-- Distributed NoSQL: store author data inside each book row

-- Table 1: books_by_author — primary lookup path
books_by_author: PK=(author_id), CK=(published_at, book_id)
  -> stores: title, genre, author_name, author_bio_short

-- Table 2: books_by_genre — secondary lookup path
books_by_genre: PK=(genre), CK=(published_at, book_id)
  -> stores: title, author_id, author_name (duplicated)

Rule: when author_name changes, update both tables in the same application transaction. Accept that brief inconsistency is tolerable (BASE).

Pattern 5: Adjacency List (DynamoDB Single-Table Design)

Store multiple entity types and their relationships in one DynamoDB table using composite key prefixes. This minimizes WCU/RCU consumption and enables single-request relationship fetches.

Table: AppData
PK (partition key)    SK (sort key)         Attributes
------------------    ---------------       --------------------
USER#123              USER#123              name, email, created_at
USER#123              ORDER#2024-001        total, status, items
USER#123              ORDER#2024-002        total, status, items
ORDER#2024-001        ORDER#2024-001        total, status, user_id
PRODUCT#SKU-99        PRODUCT#SKU-99        name, price, stock

-- Fetch user + all their orders in ONE query (same partition)
Query: PK = "USER#123", SK begins_with "ORDER#"

Apache Cassandra / ScyllaDB Specifics

Primary Key Structure

PRIMARY KEY ((partition_key_columns), clustering_columns)
--           ^--- always required      ^--- optional, define sort order
  • The partition key routes data to a node. Can be composite: ((col1, col2), ...).
  • Clustering columns define row ordering within a partition.

Write Path: Why Writes Are Cheap

Cassandra uses an LSM (Log-Structured Merge) tree. Writes are sequential appends to a commit log and memtable — no random I/O. This makes write-heavy workloads (IoT, event streams, time-series) natural fits.

Tombstones: Deletes Are Not Free

Deletes write a tombstone marker. Tombstones accumulate until compaction. High-velocity delete patterns (e.g., deleting individual items from a feed) cause tombstone buildup that degrades read performance.

Alternatives:

  • Use TTL (WITH TTL 86400) for time-bounded data — Cassandra expires rows without tombstones
  • Model deletes as status updates (status = 'deleted') instead of physical deletes
  • Use a separate "archived" table and write there instead of deleting from the primary table

ALLOW FILTERING: Always a Red Flag

-- This compiles but scans every partition in the cluster
SELECT * FROM orders WHERE status = 'PENDING' ALLOW FILTERING;

-- Fix: create a table keyed by the query pattern
CREATE TABLE orders_by_status (
    status      TEXT,
    created_at  TIMESTAMP,
    order_id    UUID,
    PRIMARY KEY ((status), created_at, order_id)
);

If you see ALLOW FILTERING in production queries, the schema is wrong for that access pattern.

No Joins, No Aggregates

-- FORBIDDEN in Cassandra at scale:
SELECT COUNT(*) FROM large_table;   -- full cluster scan
SELECT * FROM a JOIN b ON ...;      -- not supported

-- CORRECT: pre-compute counters in a dedicated counter table
CREATE TABLE order_counts_by_user (
    user_id   UUID PRIMARY KEY,
    total     COUNTER
);
UPDATE order_counts_by_user SET total = total + 1 WHERE user_id = ?;

AWS DynamoDB Specifics

Capacity Modes

ModeWhen to UseCost Model
On-DemandUnpredictable or spiky trafficPay per request (WCU/RCU consumed)
ProvisionedSteady, predictable trafficPay per provisioned WCU/RCU (cheaper at scale)

Single-table design reduces the number of WCU/RCU consumed because multiple entity types share one table — fewer round trips per request.

GSI vs LSI

GSI (Global Secondary Index):

  • Defines a completely different partition key — creates an alternative view of the data
  • Eventually consistent reads (can request strong consistency at 2x cost)
  • Can be added after table creation
  • Limit: 20 per table

LSI (Local Secondary Index):

  • Same partition key as the base table, different sort key
  • Enables additional range queries within a partition
  • MUST be defined at table creation — cannot add later
  • Strongly consistent reads available at no extra cost
  • Limit: 5 per table, shares partition storage (10 GB per partition key)
-- Example: orders table
Base table:   PK=user_id, SK=order_id          -- get order by id
GSI-1:        PK=user_id, SK=created_at        -- get user's orders by date
GSI-2:        PK=status,  SK=created_at        -- ops dashboard: pending orders by date
LSI-1:        PK=user_id, SK=total             -- get user's orders by value (within partition)

TTL for Automatic Expiry

DynamoDB TTL deletes expired items without consuming WCUs and without producing Cassandra-style tombstones.

-- Set TTL attribute on item (epoch timestamp)
expires_at: 1767225600   -- items deleted automatically after this time

-- Enable TTL on the table (attribute name must match)
aws dynamodb update-time-to-live \
  --table-name Sessions \
  --time-to-live-specification "Enabled=true, AttributeName=expires_at"

Use TTL for: sessions, OTP codes, cache entries, soft-deleted records.


Expert Checklist (Before Finalizing Schema)

Run through every item before handing off a schema for review or implementation.

  • Access Pattern Coverage: Every query pattern maps to a table, GSI, or LSI. No pattern requires ALLOW FILTERING or a Scan.
  • Cardinality Check: The partition key has enough unique values that traffic is spread evenly across nodes/shards. No single value represents > 1-2% of total traffic.
  • Partition Size Check: No single partition exceeds 10 GB (Cassandra) or 10 GB (DynamoDB LSI limit). If at risk, add a shard suffix: USER#123#2024-01.
  • Hot Key Risk: Time-based or status-based partition keys are evaluated for hot key risk. Mitigation (bucket sharding, write spreading) is documented.
  • Tombstone Risk (Cassandra): High-velocity delete patterns are replaced with TTL or status-update modeling.
  • Consistency Requirement: The application's consistency tolerance is confirmed. If strong consistency is required everywhere, evaluate whether Cassandra or DynamoDB (without strong-read overhead) is the right tool.
  • Counter Tables: Any aggregate (count, sum) has a dedicated counter table — never computed with SELECT COUNT(*).
  • Index Strategy (DynamoDB): LSIs are confirmed at table creation (cannot be added later). GSIs are evaluated for WCU/RCU overhead.
  • Denormalization Documented: Every duplicated field has a documented update strategy — which code path updates all copies.
  • TTL Strategy: Time-bounded data uses TTL rather than application-level deletes.

Common Anti-Patterns

Scatter-Gather (Scan)

Querying all partitions to find matching items — equivalent to a full table scan.

-- DynamoDB: never do this in production
aws dynamodb scan --table-name Orders --filter-expression "status = :s"

-- Fix: add a GSI with status as partition key

Hot Keys

All traffic for a time period or popular value routes to one partition.

-- BAD: day_of_week has 7 values — Monday gets crushed
PRIMARY KEY ((day_of_week), event_time, event_id)

-- GOOD: shard by (user_id % N) or use a high-cardinality natural key
PRIMARY KEY ((user_id), event_time, event_id)

Relational Modeling in a NoSQL Store

Creating normalized entity tables (Users, Orders, Products) and then trying to join them at the application layer.

-- This pattern reconstructs relational joins in application code:
users = cassandra.execute("SELECT * FROM users WHERE id = ?")
orders = cassandra.execute("SELECT * FROM orders WHERE user_id = ?")
result = merge(users, orders)  # application-side join — latency doubles, errors multiply

-- Fix: denormalize. Store the user fields you need inside the orders table.

Unbounded Partition Growth

Storing all records for a high-traffic entity under one partition key without time-bucketing.

-- BAD: all events for a popular user in one partition — grows without bound
PRIMARY KEY ((user_id), event_time, event_id)

-- GOOD: bucket by month — each partition stays bounded
PRIMARY KEY ((user_id, month), event_time, event_id)
-- month = '2024-03'

Using ALLOW FILTERING as a Workaround

Silencing a Cassandra query error with ALLOW FILTERING instead of fixing the schema. See the Cassandra Specifics section above.

Skills enregistrées