DS DevShelfHub Projects · AI tools
Cheatsheets / Cassandra
Cheatsheet · Dev tooling

Cassandra CQL Cheatsheet: Keys, Consistency and nodetool

By DevShelfHub

CQL, keyspaces, partition vs clustering keys, lightweight transactions, consistency levels, TTL + tombstones, compaction strategies, drivers, nodetool.

110 items 9 min CQL Wide column Tunable

Start hereQuick start · 6 you’ll reach for daily

Open cqlshcqlsh host 9042 -u app -p ****
Switch keyspaceUSE app;
DescribeDESCRIBE TABLE sensors;
Insert with TTLINSERT ... USING TTL 86400
Read by PKSELECT * FROM t WHERE pk = ?
Conditional createINSERT ... IF NOT EXISTS

Target versions · paceVersions

Targets: cassandra ≥ 5.0 cqlsh bundled with server cassandra-driver ≥ 3.29 (Python)

Cassandra 5.0 brings Storage-Attached Indexes (SAI), Unified Compaction, vector search, and trie memtables. CQL is mostly forward-compatible across majors. Materialized views remain experimental in 4.x/5.x — avoid them in production. ScyllaDB and Apache Cassandra speak the same wire protocol; most CQL syntax in this sheet is portable.

Docker · cqlshSetup

bash
# Docker — single-node Cassandra 5 (replace with the latest LTS in prod)
docker run -d --name cass \
    -p 9042:9042 \
    -e CASSANDRA_CLUSTER_NAME=devcluster \
    cassandra:5

# Wait until the node is up (status UN = Up Normal)
docker exec cass nodetool status

# Connect with cqlsh (bundled in the image)
docker exec -it cass cqlsh

# Auth + TLS (typical prod connect)
cqlsh c1.internal 9042 -u app -p **** --ssl

# Bulk import / export from the shell
# Inside cqlsh:
#   COPY app.sensors FROM 'rows.csv' WITH HEADER=true;
#   COPY app.sensors TO   'rows.csv' WITH HEADER=true;

# Python driver
pip install "cassandra-driver>=3.29"

python - <<'PY'
from cassandra.cluster import Cluster
from cassandra.auth import PlainTextAuthProvider
auth = PlainTextAuthProvider('cassandra', 'cassandra')
with Cluster(['127.0.0.1'], port=9042, auth_provider=auth) as cluster:
    session = cluster.connect()
    print(session.execute("SELECT release_version FROM system.local").one())
PY

The shellcqlsh basics

cqlsh host 9042Connect over the binary CQL protocol.
cqlsh -u user -p ****Auth via PasswordAuthenticator.
cqlsh --sslTLS — requires ~/.cassandra/cqlshrc with cert paths.
cqlsh -e "DESCRIBE KEYSPACES"One-shot. Output, exit.
cqlsh --request-timeout=30Bump the client timeout for heavy queries.
DESCRIBE [KEYSPACES | KEYSPACE k | TABLE t | TYPE u]Schema introspection.
USE app;Set the default keyspace for the session.
SOURCE 'script.cql'Run a script from disk.
TRACING ON / OFFShow coordinator round-trips, per-replica timing.
CONSISTENCY LOCAL_QUORUMPer-session consistency level.
PAGING OFFDisable automatic paging in the shell.
COPY app.t FROM 'rows.csv' WITH HEADER=trueCSV import.
COPY app.t TO 'rows.csv' WITH HEADER=trueCSV export.

Replication strategiesKeyspaces & replication

CREATE KEYSPACE app WITH replication = {'class':'SimpleStrategy','replication_factor':3}Dev only Ignores DCs. Don’t use in prod.
CREATE KEYSPACE app WITH replication = {'class':'NetworkTopologyStrategy','dc1':3,'dc2':3}Preferred DC-aware replication.
ALTER KEYSPACE app WITH replication = {'class':'NetworkTopologyStrategy','dc1':3,'dc2':5}Change RF. Schedule nodetool repair after.
DROP KEYSPACE appDrop everything in the keyspace.
WITH durable_writes = falseSkip commit log. Faster, lossy on crash. Rare.
SELECT * FROM system_schema.keyspacesInspect every keyspace and its replication config.

Primitives · collections · UDTsData types

text · varchar · asciiUTF-8 / 7-bit strings.
int · bigint · smallint · tinyint · varintBounded ints + arbitrary-precision varint.
float · double · decimalIEEE floats + fixed precision.
boolean · blob · inetBool / bytes / IP address.
date · time · timestampDay / nanos-of-day / instant.
uuid · timeuuidv4 uuid for partition keys; v1 (timeuuid) for clustering — sortable.
list<text> · set<int> · map<text,int>Collections. Single-cell — rewritten on update.
frozen<list<...>>Stored as one opaque value. Indexable; immutable per row.
counterSpecial distributed counter. Can’t mix with normal cols in one table.
CREATE TYPE address (...); col frozen<address>User-defined type. Often used frozen.

PRIMARY KEY · compaction · TTLDDL

PRIMARY KEY ((id))Partition key only. One row per partition.
PRIMARY KEY ((id), ts)Partition key (id) + clustering column ts.
PRIMARY KEY ((id, bucket), ts)Composite partition key — the trick for unbounded growth.
WITH CLUSTERING ORDER BY (ts DESC)Physical sort within a partition.
WITH default_time_to_live = 86400Implicit TTL on every insert.
WITH gc_grace_seconds = 86400Tombstone retention window. Must be ≥ repair frequency.
WITH compaction = {'class':'TimeWindowCompactionStrategy', 'compaction_window_unit':'DAYS', 'compaction_window_size':1}TWCS — right call for time-series + TTL.
WITH compression = {'sstable_compression':'LZ4Compressor'}SSTable codec. ZstdCompressor trades CPU for size.
ALTER TABLE t ADD col textAdd a column. Existing rows have NULL.
ALTER TABLE t DROP colDrop column. Cleaned during compaction; not instant.
CREATE INDEX idx_x ON t(col)Legacy 2i. Local per-replica index; skews badly on high cardinality.
CREATE CUSTOM INDEX idx ON t(col) USING 'StorageAttachedIndex'Preferred SAI (5.0+) — range queries, collections, vectors.
sql
-- Keyspace with NetworkTopologyStrategy (multi-DC ready).
CREATE KEYSPACE IF NOT EXISTS app
WITH replication = {
    'class':           'NetworkTopologyStrategy',
    'dc1':             3,
    'dc2':             3
}
AND durable_writes = true;

USE app;

-- Time-series table: one partition per (sensor_id, day).
-- Partitions stay bounded; reads hit one partition; TWCS handles compaction efficiently.
CREATE TABLE IF NOT EXISTS sensor_readings (
    sensor_id    uuid,
    day          date,
    ts           timestamp,
    temperature  float,
    humidity     float,
    PRIMARY KEY ((sensor_id, day), ts)
)
WITH CLUSTERING ORDER BY (ts DESC)
AND default_time_to_live = 7776000                 -- 90 days
AND gc_grace_seconds     = 86400                   -- 1 day (because TTL data tombstones rapidly)
AND compaction = {
    'class':                  'TimeWindowCompactionStrategy',
    'compaction_window_unit': 'DAYS',
    'compaction_window_size': 1
}
AND compression = { 'sstable_compression': 'LZ4Compressor' };

-- A storage-attached index — works on any column, supports range + collections.
CREATE CUSTOM INDEX IF NOT EXISTS sensor_temp_sai
ON sensor_readings (temperature)
USING 'StorageAttachedIndex';

INSERT · UPDATE · DELETE · LWTDML

INSERT INTO t (id, ts, val) VALUES (?, ?, ?)UPSERT semantics — insert and update are the same.
INSERT ... USING TTL 86400Per-row TTL in seconds.
INSERT ... USING TIMESTAMP 1716000000000000Override the write timestamp. Use only when you know what you’re doing.
INSERT ... IF NOT EXISTSLightweight transaction. Paxos round-trip — slow.
UPDATE t SET col = ? WHERE id = ? AND ts = ?Same key requirements as SELECT.
UPDATE ... IF col = ?Compare-and-set LWT.
DELETE FROM t WHERE id = ?Writes a partition tombstone (logical delete).
DELETE col FROM t WHERE id = ?Cell-level tombstone — column NULLs out, row remains.
BEGIN BATCH ... APPLY BATCHLogged batch — atomicity across partitions. Not for perf.
BEGIN UNLOGGED BATCH ... APPLY BATCHSame-partition fan-in only. Cheap.
sql
-- Lightweight transactions (LWT). Use sparingly — every LWT is a Paxos round-trip.

-- Insert only if the row is new (claim a username)
INSERT INTO accounts (username, user_id, created_at)
VALUES ('ada', uuid(), toTimestamp(now()))
IF NOT EXISTS;
-- Returns: [applied] = true, or [applied] = false with the existing row.

-- Conditional update — classic compare-and-set
UPDATE accounts
SET    email = 'ada@example.com'
WHERE  username = 'ada'
IF     email   = 'old@example.com';

-- Multi-column predicate
UPDATE counters SET balance = 90, version = 2
WHERE  account_id = 7
IF     balance = 100 AND version = 1;

-- Reading what an LWT just decided — needs SERIAL / LOCAL_SERIAL consistency
SELECT email FROM accounts
WHERE  username = 'ada'
USING  CONSISTENCY LOCAL_SERIAL;

Partition key firstQueries

SELECT * FROM t WHERE id = ?Always include the partition key.
SELECT * FROM t WHERE id IN (?,?,?)Multi-partition fetch. Coordinator fans out.
SELECT * FROM t WHERE id = ? AND ts > ?Range on a clustering column — only after equality on the prefix.
SELECT * FROM t WHERE id = ? ORDER BY ts ASCOverride clustering order — only for the cluster prefix.
SELECT * FROM t WHERE col = ?Errors without a partition key — unless an index covers col.
SELECT * FROM t WHERE col = ? ALLOW FILTERINGDanger Force a full-cluster scan. Don’t.
SELECT writetime(col), ttl(col) FROM t WHERE id = ?Per-cell metadata — when it was written, when it expires.
SELECT * FROM t WHERE id = ? PER PARTITION LIMIT 5Top-N per partition.
SELECT * FROM t WHERE token(id) > token(?) LIMIT 1000Server-side range pagination across the ring.
INSERT INTO t JSON '{...}' · SELECT JSON * FROM tJSON in/out. Convenient from HTTP layers.

2i · SAI · collectionsSecondary indexes & SAI

CREATE INDEX idx_x ON t(col)Legacy 2i — local per-replica index. Skewed cols ruin it.
CREATE CUSTOM INDEX idx ON t(col) USING 'StorageAttachedIndex'Preferred SAI. Range, collections, vector.
... WITH OPTIONS = {'case_sensitive':'false','normalize':'true'}SAI text options.
CREATE INDEX ON t(KEYS(map_col))Index map keys.
CREATE INDEX ON t(VALUES(map_col))Index map values.
CREATE INDEX ON t(ENTRIES(map_col))Index map (key,value) pairs.

Tunable per queryConsistency levels

ONE · LOCAL_ONEFastest, weakest. Use for tolerant reads.
QUORUM · LOCAL_QUORUMPreferred production default. LOCAL_ = same DC.
ALLEvery replica. Rare — one node down breaks the read.
SERIAL · LOCAL_SERIALReads inside LWTs. Match write SERIAL level.
EACH_QUORUM (write-only)Quorum in every DC. Expensive cross-DC writes.
ANY (write-only)Hinted-handoff acceptable. Risk losing the write if hint times out.
R + W > RFRule of thumb for strong consistency. e.g. RF=3 → QUORUM read + QUORUM write.

Deletes are writesTTL & tombstones

INSERT ... USING TTL 86400Row expires after N seconds.
default_time_to_live (table option)TTL applied to every insert unless overridden.
gc_grace_secondsHow long tombstones survive. Must be ≥ repair interval — default 10 days.
DELETE FROM t WHERE id = ?Writes a tombstone — row physically lives until compaction.
nodetool tablestats keyspace.tableTombstone counts, bloom filter false positives, read latency.
tombstone_failure_threshold (yaml)Reads scanning more tombstones than this fail. Default 100k.

SSTable merge strategiesCompaction strategies

STCS (SizeTieredCompactionStrategy)Default. Write-heavy. More disk waste; less read-friendly.
LCS (LeveledCompactionStrategy)Read-heavy or update-heavy tables. Tight read amplification.
TWCS (TimeWindowCompactionStrategy)Preferred for time-series + TTL. Old windows drop whole.
UCS (UnifiedCompactionStrategy)5.0+ hybrid. Tune scaling_parameters.
ALTER TABLE t WITH compaction = {'class':'LeveledCompactionStrategy'}Switch strategy. Cluster re-compacts gradually.
nodetool compactionstatsWatch in-flight compactions.

cassandra-driverPython driver

from cassandra.cluster import ClusterMain entry point.
from cassandra.auth import PlainTextAuthProviderPassword auth.
cluster = Cluster(['c1','c2'], port=9042, auth_provider=auth)Multiple contact points; the driver discovers the rest.
session = cluster.connect('app')Sets default keyspace.
from cassandra.policies import TokenAwarePolicy, DCAwareRoundRobinPolicyLoad balancing — route to a replica that owns the partition.
stmt = session.prepare("INSERT INTO t (a, b) VALUES (?, ?)")Prepared statement — injection-safe, cached on every coordinator.
session.execute(stmt, (a, b))Bind + execute.
future = session.execute_async(stmt, args); future.add_callbacks(ok, err)Async fanout.
session.execute(stmt, args, timeout=10, consistency_level=ConsistencyLevel.LOCAL_QUORUM)Per-call options.
BatchStatement(BatchType.LOGGED).add(stmt, args)Same-partition fan-in. Avoid cross-partition batches.
python
from cassandra.cluster import Cluster, ExecutionProfile, EXEC_PROFILE_DEFAULT
from cassandra.auth import PlainTextAuthProvider
from cassandra.policies import DCAwareRoundRobinPolicy, TokenAwarePolicy
from cassandra import ConsistencyLevel
from cassandra.query import BatchStatement, BatchType
from uuid import uuid4
from datetime import datetime, date

# Connection profile: token-aware → DC-aware. Default CL = LOCAL_QUORUM.
profile = ExecutionProfile(
    load_balancing_policy = TokenAwarePolicy(DCAwareRoundRobinPolicy(local_dc='dc1')),
    consistency_level     = ConsistencyLevel.LOCAL_QUORUM,
    request_timeout       = 10.0,
)

cluster = Cluster(
    contact_points = ['c1.internal', 'c2.internal'],
    port           = 9042,
    auth_provider  = PlainTextAuthProvider('app', '****'),
    execution_profiles = {EXEC_PROFILE_DEFAULT: profile},
)
session = cluster.connect('app')

# Prepared statements — cached on every coordinator, immune to injection.
insert = session.prepare("""
    INSERT INTO sensor_readings (sensor_id, day, ts, temperature, humidity)
    VALUES (?, ?, ?, ?, ?)
""")

# Logged batch only for same-partition writes; otherwise prefer concurrent execute.
batch = BatchStatement(batch_type=BatchType.LOGGED)
sid, today, now = uuid4(), date.today(), datetime.utcnow()
batch.add(insert, (sid, today, now, 22.4, 41.2))
session.execute(batch)

# Async fanout
futures = [session.execute_async(insert, (sid, today, now, t, h)) for t, h in samples]
for f in futures: f.result()                              # block / surface errors

Operational CLInodetool

nodetool statusCluster topology + per-node state (UN / DN).
nodetool infoThis node’s heap, load, uptime.
nodetool tablestats keyspace.tableRead/write counts, tombstones, bloom filter stats.
nodetool repair [-pr]Anti-entropy repair. -pr = primary range only.
nodetool compactionstatsActive compactions + ETA.
nodetool flush keyspaceForce memtable flush to SSTable.
nodetool drainPre-shutdown flush. Run before stopping a node cleanly.
nodetool gossipinfoInspect gossip state — useful when nodes look unhealthy.

Full pipeline · ~25 linesEnd-to-end · Time-series ingest

Creates a TWCS-backed sensor readings table with a composite partition key (sensor_id, day), inserts a few rows via a prepared statement, reads the latest five per partition.

python
# End-to-end: time-series sensor ingest + per-day query, Python driver.
from cassandra.cluster import Cluster
from cassandra import ConsistencyLevel
from uuid import uuid4
from datetime import datetime, date, timedelta
import random

with Cluster(['127.0.0.1']) as cluster:
    s = cluster.connect()

    s.execute("""
        CREATE KEYSPACE IF NOT EXISTS demo
        WITH replication = {'class':'SimpleStrategy','replication_factor':1}
    """)
    s.execute("USE demo")
    s.execute("""
        CREATE TABLE IF NOT EXISTS sensor_readings (
            sensor_id uuid, day date, ts timestamp, temperature float,
            PRIMARY KEY ((sensor_id, day), ts)
        ) WITH CLUSTERING ORDER BY (ts DESC)
          AND default_time_to_live = 86400
          AND compaction = {'class':'TimeWindowCompactionStrategy',
                            'compaction_window_unit':'DAYS','compaction_window_size':1}
    """)

    insert = s.prepare("INSERT INTO sensor_readings (sensor_id, day, ts, temperature) "
                       "VALUES (?, ?, ?, ?)")
    select = s.prepare("SELECT ts, temperature FROM sensor_readings "
                       "WHERE sensor_id = ? AND day = ? LIMIT 5")

    sid, today = uuid4(), date.today()
    for i in range(20):
        s.execute(insert, (sid, today, datetime.utcnow() - timedelta(seconds=i),
                           20 + random.random() * 5),
                  execution_profile=None)
    for row in s.execute(select, (sid, today)):
        print(row.ts, round(row.temperature, 2))

Best practiceGood to know

Model your tables around the read. In Cassandra you design one table per access pattern — denormalisation is the point. If you catch yourself joining across tables in the app, you usually need a new table optimised for that query.
Bucket time-series partitions. PRIMARY KEY ((sensor_id, day), ts) keeps any single partition bounded. A naked ((sensor_id), ts) grows forever and gets slow as the cluster ages.
Use SAI, not classic 2i. Storage-Attached Indexes (5.0+) handle range, text, collections, and vectors with predictable cost. Legacy 2i remains a footgun on high-cardinality columns.

Common trapsWatch out for

Never ALLOW FILTERING in production. It scans every node, every partition. The fact that CQL warns you about it isn’t a hint — it’s a rejection. Add an index, add a denormalised table, or rethink the query.
Cross-partition BEGIN BATCH is not a perf trick. Logged batches are for atomicity across partitions; they actually add coordinator + commit-log cost. For raw throughput, use concurrent async executes.
Tombstones kill reads silently. Heavy DELETEs or expired TTL rows accumulate until gc_grace_seconds + compaction clear them. Watch nodetool tablestats for tombstone count vs live rows.

Go deeperSee also

Cassandra FAQ

What is the difference between a partition key and a clustering key in Cassandra?

The partition key determines which node stores a row by hashing it to a token range — all rows with the same partition key live on the same node. Clustering keys sort rows within a partition on disk. Choosing a partition key with high cardinality and even distribution is the single most important Cassandra data-modelling decision.

What are consistency levels in Cassandra?

Consistency levels control how many replicas must acknowledge a read or write before the coordinator returns success. ONE is fastest but may return stale data; QUORUM requires a majority of replicas and balances availability with consistency; ALL requires every replica and is the strongest but least available. Choose based on your replication factor and tolerance for stale reads.

What are tombstones in Cassandra?

Tombstones are deletion markers written to the log-structured storage engine instead of immediate physical deletes. They are required because deletions must propagate across replicas. Tombstones are garbage-collected during compaction after the `gc_grace_seconds` window (default 10 days). Accumulating too many tombstones before compaction can significantly slow reads.

How do lightweight transactions (LWT) work in Cassandra?

Lightweight transactions implement compare-and-swap using the Paxos protocol. Use `INSERT ... IF NOT EXISTS` for unique-row guarantees and `UPDATE ... IF <condition>` for conditional updates. LWTs are roughly 4× slower than normal writes because they require multiple round trips, so reserve them for cases where idempotency or uniqueness truly requires it.

What does the nodetool command do in Cassandra?

nodetool is the primary CLI for operating a Cassandra node. Use `nodetool status` to see ring health and token ownership, `nodetool flush` to force memtable writes to SSTables, `nodetool compact` to trigger compaction, and `nodetool repair` to reconcile replicas. It connects to the JMX interface and must be run on or have network access to the target node.