Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Cassandra Data Indexes

Indexes in Cassandra provide efficient data access patterns beyond partition key lookups. Understanding the available index types, their implementation differences, and appropriate use cases is essential for query optimization.


Every Cassandra table has an implicit primary key index. This index is fundamental to Cassandra's data model and requires no additional configuration.

Partition Key Index: Each SSTable contains an index mapping partition key tokens to their data positions. This enables O(log n) lookups within an SSTable.

Clustering Column Ordering: Within a partition, data is physically sorted by clustering columns. Range queries on clustering columns are efficient because they read contiguous disk regions.

-- Primary key enables these efficient queries:
CREATE TABLE events (
sensor_id uuid,
event_time timestamp,
reading double,
PRIMARY KEY (sensor_id, event_time)
);
-- Partition key lookup: O(log n) per SSTable
SELECT * FROM events WHERE sensor_id = ?;
-- Clustering range: sequential read within partition
SELECT * FROM events WHERE sensor_id = ? AND event_time > ?;

Primary key indexes only support queries that include the partition key. Without secondary indexes, queries on non-key columns require full table scans—scanning every partition across all nodes.

-- Without secondary index: requires ALLOW FILTERING (full scan)
SELECT * FROM events WHERE reading > 100.0 ALLOW FILTERING;
-- With secondary index: targeted lookup
CREATE INDEX ON events (reading);
SELECT * FROM events WHERE reading > 100.0;

Evolution of Secondary Indexes in Cassandra

Section titled “Evolution of Secondary Indexes in Cassandra”

Cassandra has developed multiple secondary index implementations over time:

VersionIndex TypeStatus
0.7 (2011)Secondary Index (2i)Legacy, still supported
3.4 (2016)SASIExperimental, limited support
5.0 (2023)SAIRecommended for new deployments

Each generation addressed limitations of its predecessors while introducing new capabilities and trade-offs.

FeatureCassandra 4.xCassandra 5.0+
Secondary Index (2i)
SASI (experimental)
SAI⚠️ (experimental)✅ (recommended)
Vector search
Equality queries
Range queriesSASI onlySAI/SASI
LIKE prefixSASI onlySAI/SASI
LIKE containsSASI onlySAI/SASI
Collection indexing2i only2i/SAI
ANN (vector)SAI only

Version Recommendation

  • Cassandra 5.0+: Use SAI for all new indexes
  • Cassandra 4.x: Use 2i for equality, SASI for range (with caution)
  • Upgrading: Plan migration from SASI to SAI when moving to 5.0

All Cassandra secondary indexes share a common principle: they create a mapping from indexed column values to partition keys. The implementation of this mapping differs significantly between index types.

Secondary Index Concept: Value → Partition Key MappingSecondary Index Concept: Value → Partition Key MappingBase Table: usersIndex on citypk=user1 | name='Alice' | city='NYC'pk=user2 | name='Bob' | city='LA'pk=user3 | name='Carol' | city='NYC''NYC' → [user1, user3]'LA' → [user2]

The primary architectural distinction between index types is where index data is stored:

Index Storage Architecture ComparisonIndex Storage Architecture ComparisonSecondary Index (2i) - Separate Hidden TablesSASI - SSTable-AttachedSAI - SSTable-AttachedBase TableSSTableIndex Table(hidden)SSTableBase TableSSTableIndex Component(attached to SSTable)Base TableSSTableIndex Component(attached to SSTable)separatecompactionsameSSTablesameSSTable

Separate Tables (2i): Legacy secondary indexes store index data in hidden tables. These tables have their own SSTables and compact independently from base table data.

SSTable-Attached (SASI, SAI): Modern indexes attach index data directly to base table SSTables. Index data compacts together with base table data, maintaining consistency.


CharacteristicSecondary Index (2i)SASISAI
StorageSeparate hidden tableAttached to SSTableAttached to SSTable
Cassandra Version0.7+3.4+5.0+
StatusLegacyExperimentalRecommended
Query TypesEquality onlyEquality, range, LIKEEquality, range, LIKE
Numeric RangeNoYesYes
Text SearchNoPREFIX, CONTAINSYes
AND QueriesScatter-gatherSingle-passSingle-pass
Write OverheadMediumMediumLow (typical)
Cardinality HandlingPoor at extremesBetterBest (generally)
Production ReadyYes (with caveats)NoYes

This table shows which CQL operators are generally supported by each index type. Actual behavior may vary by Cassandra version, analyzer configuration, and data type. Consult version-specific documentation for definitive support.

OperatorSecondary Index (2i)SASISAINotes
= (equality)All index types
>Requires SPARSE mode for SASI
>=Requires SPARSE mode for SASI
<Requires SPARSE mode for SASI
<=Requires SPARSE mode for SASI
LIKE 'prefix%'PREFIX mode default for SASI
LIKE '%substring%'⚠️SASI: CONTAINS mode; SAI: requires analyzer
LIKE '%suffix'Not supported by any index
INMultiple equality values
CONTAINS (collection)Collection element search
CONTAINS KEY (map)Map key search
!= (not equal)Not supported by any index
OR (cross-column)Application-level union required

Legend: ✅ Supported | ⚠️ Partial/Conditional | ❌ Not Supported

Data TypeSecondary Index (2i)SASISAI
text / varchar
int / bigint
float / double
decimal
timestamp
date / time
uuid / timeuuid
boolean⚠️
inet
blob⚠️
list<T>
set<T>
map<K,V>
vector<float, N>
frozen<T>⚠️

SASI Collection Support

SASI does not support indexing collections. Use SAI or denormalized tables for collection queries.

Index Type Selection GuideIndex Type Selection GuideUse SAI(recommended)YESUsing Cassandra 5.0+?NOUse SASI (3.4+)or external searchYESNeed text search(LIKE, CONTAINS)?NOUse SASI (3.4+)or redesign modelYESNeed numericrange queries?NOHigh cardinalitycolumn?YESNOAvoid 2iRedesign model or use SAISecondary Index (2i)acceptable

When a query uses one index, all index types follow a similar pattern:

  1. Query coordinator identifies relevant nodes
  2. Each node queries its local index
  3. Index returns matching partition keys
  4. Node reads base table partitions
  5. Results returned to coordinator

Queries with multiple indexed predicates differ significantly:

Secondary Index (2i): Executes each predicate separately, intersects results at coordinator. Creates scatter-gather pattern with potential for large intermediate result sets.

SASI / SAI: Intersects predicates within each SSTable before returning results. More efficient for multi-predicate queries.

-- Multi-predicate query
SELECT * FROM users WHERE city = 'NYC' AND age > 25;
-- 2i: Two separate index lookups, coordinator intersection
-- SAI: Single-pass intersection per SSTable

All secondary indexes add overhead to the write path:

Index TypeWrite OverheadReason
2iMediumSeparate table mutation
SASIMediumIndex structure update
SAILowOptimized append-only design
Query Type2iSASISAI
Single equalityFairGoodGood
Multiple ANDPoorGoodGood
RangeN/AGoodGood
High selectivityPoorFairGood
Low selectivityPoorFairFair

Do Not Index These Columns

The following patterns will cause performance problems or outright failures. These are not recommendations—they are hard constraints.

Never Index UUIDs, Timestamps, or Unique Identifiers

Problem: Index size equals or exceeds base table size. Every query contacts all nodes to find one row.

Symptoms: Query latency worse than full table scan, excessive disk usage, coordinator timeouts.

Instead: Include the column in the partition key or create a denormalized lookup table.

-- DO NOT DO THIS
CREATE INDEX ON events (event_id); -- event_id is UUID
-- INSTEAD: Make it the partition key
CREATE TABLE events_by_id (
event_id uuid PRIMARY KEY,
...
);

Never Index Boolean or Low-Enum Columns Without Partition Key

Problem: Each index entry points to millions of rows. Single query returns unbounded results.

Symptoms: Memory exhaustion, GC storms, query timeouts, coordinator OOM.

Instead: Partition by the low-cardinality value, or always combine with partition key restriction.

-- DO NOT DO THIS
CREATE INDEX ON users (is_active); -- Returns 50% of all rows
-- INSTEAD: Partition by status
CREATE TABLE users_by_status (
is_active boolean,
user_id uuid,
PRIMARY KEY (is_active, user_id)
);

Avoid Indexing Columns That Change Often

Problem: Every update requires index delete + insert. Tombstones accumulate rapidly.

Symptoms: Growing read latency, tombstone warnings, compaction pressure.

Instead: Store mutable state separately or accept query trade-offs.

-- PROBLEMATIC
CREATE INDEX ON sessions (last_activity); -- Updated every request
-- Every update creates a tombstone in the index
-- After 1M updates: 1M tombstones to scan

Global Queries Without Partition Restriction

Section titled “Global Queries Without Partition Restriction”

Avoid Index-Only Queries in Large Clusters

Problem: Query contacts ALL nodes, latency = slowest node, no locality benefit.

Acceptable: Combined with partition key (restricts to one node).

Problematic: Global queries in large clusters with high throughput requirements.

-- SLOW: Contacts all nodes
SELECT * FROM users WHERE city = 'NYC';
-- FAST: Restricted to one partition
SELECT * FROM users WHERE region = 'us-east' AND city = 'NYC';

Terminal window
# Check index build progress
nodetool describecluster
# Rebuild index (SAI)
nodetool rebuild_index keyspace table index_name
# View index status
nodetool tablestats keyspace.table
# JMX metrics for index performance
org.apache.cassandra.metrics:type=Index,scope=*,name=*
# Per-table index metrics
org.apache.cassandra.metrics:type=Table,keyspace=*,scope=*,name=IndexSummaryOffHeapMemoryUsed