Start hereQuick start · 6 you’ll reach for daily
Target versions · paceVersions
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
# 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 9042 | Connect over the binary CQL protocol. |
| cqlsh -u user -p **** | Auth via PasswordAuthenticator. |
| cqlsh --ssl | TLS — requires ~/.cassandra/cqlshrc with cert paths. |
| cqlsh -e "DESCRIBE KEYSPACES" | One-shot. Output, exit. |
| cqlsh --request-timeout=30 | Bump 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 / OFF | Show coordinator round-trips, per-replica timing. |
| CONSISTENCY LOCAL_QUORUM | Per-session consistency level. |
| PAGING OFF | Disable automatic paging in the shell. |
| COPY app.t FROM 'rows.csv' WITH HEADER=true | CSV import. |
| COPY app.t TO 'rows.csv' WITH HEADER=true | CSV 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 app | Drop everything in the keyspace. |
| WITH durable_writes = false | Skip commit log. Faster, lossy on crash. Rare. |
| SELECT * FROM system_schema.keyspaces | Inspect every keyspace and its replication config. |
Primitives · collections · UDTsData types
| text · varchar · ascii | UTF-8 / 7-bit strings. |
| int · bigint · smallint · tinyint · varint | Bounded ints + arbitrary-precision varint. |
| float · double · decimal | IEEE floats + fixed precision. |
| boolean · blob · inet | Bool / bytes / IP address. |
| date · time · timestamp | Day / nanos-of-day / instant. |
| uuid · timeuuid | v4 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. |
| counter | Special 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 = 86400 | Implicit TTL on every insert. |
| WITH gc_grace_seconds = 86400 | Tombstone 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 text | Add a column. Existing rows have NULL. |
| ALTER TABLE t DROP col | Drop 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. |
-- 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 86400 | Per-row TTL in seconds. |
| INSERT ... USING TIMESTAMP 1716000000000000 | Override the write timestamp. Use only when you know what you’re doing. |
| INSERT ... IF NOT EXISTS | Lightweight 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 BATCH | Logged batch — atomicity across partitions. Not for perf. |
| BEGIN UNLOGGED BATCH ... APPLY BATCH | Same-partition fan-in only. Cheap. |
-- 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 ASC | Override 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 FILTERING | Danger 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 5 | Top-N per partition. |
| SELECT * FROM t WHERE token(id) > token(?) LIMIT 1000 | Server-side range pagination across the ring. |
| INSERT INTO t JSON '{...}' · SELECT JSON * FROM t | JSON 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_ONE | Fastest, weakest. Use for tolerant reads. |
| QUORUM · LOCAL_QUORUM | Preferred production default. LOCAL_ = same DC. |
| ALL | Every replica. Rare — one node down breaks the read. |
| SERIAL · LOCAL_SERIAL | Reads 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 > RF | Rule of thumb for strong consistency. e.g. RF=3 → QUORUM read + QUORUM write. |
Deletes are writesTTL & tombstones
| INSERT ... USING TTL 86400 | Row expires after N seconds. |
| default_time_to_live (table option) | TTL applied to every insert unless overridden. |
| gc_grace_seconds | How 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.table | Tombstone 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 compactionstats | Watch in-flight compactions. |
cassandra-driverPython driver
| from cassandra.cluster import Cluster | Main entry point. |
| from cassandra.auth import PlainTextAuthProvider | Password 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, DCAwareRoundRobinPolicy | Load 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. |
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 status | Cluster topology + per-node state (UN / DN). |
| nodetool info | This node’s heap, load, uptime. |
| nodetool tablestats keyspace.table | Read/write counts, tombstones, bloom filter stats. |
| nodetool repair [-pr] | Anti-entropy repair. -pr = primary range only. |
| nodetool compactionstats | Active compactions + ETA. |
| nodetool flush keyspace | Force memtable flush to SSTable. |
| nodetool drain | Pre-shutdown flush. Run before stopping a node cleanly. |
| nodetool gossipinfo | Inspect 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.
# 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
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.
Common trapsWatch out for
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.
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.
DELETEs or expired TTL rows accumulate until
gc_grace_seconds + compaction clear them. Watch
nodetool tablestats for tombstone count vs live rows.