Skip to content

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

Cassandra CQL Index Commands

Secondary indexes enable queries on columns that are not part of the primary key. Cassandra supports multiple index implementations, each with different performance characteristics and query capabilities.


  • CREATE INDEX initiates asynchronous index building; the command returns before indexing completes
  • Index updates are applied synchronously as part of the write path once the index is built
  • IF NOT EXISTS provides idempotent index creation
  • DROP INDEX removes the index immediately; the command returns after schema propagation
  • Each index is local to each node (node indexes only its own data)

Undefined Behavior

The following behaviors are undefined and must not be relied upon:

  • Query availability during build: Queries using the index may fail or return partial results until build completes on all nodes
  • Build completion time: Index build duration depends on data volume and cluster load; no timeout or progress guarantee
  • Query performance: Scatter-gather queries (without partition key) have unbounded latency depending on cluster size and data distribution
  • Result completeness during failures: If nodes are unavailable, indexed queries may return incomplete results
  • Index-only scans: All index queries require reading the base table; index does not store complete row data
StateQuery BehaviorHow to Check
BuildingQueries may fail or return partial resultsnodetool indexbuildstatus
BuiltQueries return complete results from available nodesnodetool indexbuildstatus (no pending builds)
FailedIndex unusable; must drop and recreateCheck logs for build errors

Note: system_schema.indexes shows index definitions but not build status. Use nodetool indexbuildstatus to check build progress.

Query TypeNodes ContactedPerformance
Index query with partition keyReplicas for that partitionFast (single partition)
Index query without partition keyAll nodes in clusterSlow (scatter-gather)
Index query with token rangeNodes owning that rangeVariable
Failure ModeOutcomeClient Action
Timeout during CREATE INDEXSchema may have propagated; build may be in progressCheck index status
Node failure during buildBuild continues on other nodes; failed node rebuilds on restartMonitor build status
Query timeout on indexed columnPartial results possibleRetry or add partition key constraint
IndexNotAvailableExceptionIndex build incompleteWait for build to complete
VersionBehavior
3.4+SASI indexes available (CASSANDRA-10661)
4.0+Improved index build handling
5.0+SAI as default index implementation (CEP-7), SASI deprecated

Secondary indexes in Cassandra are local indexes—each node indexes only the data it owns. When querying an indexed column without the partition key:

Client Query → Coordinator → Scatter to ALL Nodes → Gather Results → Return
└── Each node searches local index

This architecture has implications:

  • Without partition key: Query must contact all nodes (scatter-gather)
  • With partition key: Query contacts only replica nodes (efficient)
  • Index maintenance: Indexes are updated synchronously with writes

Performance Consideration

Secondary indexes perform best when:

  • The query includes the partition key
  • The indexed column has low-to-medium cardinality
  • The data distribution is relatively uniform

For high-cardinality columns or queries without partition keys, consider denormalized tables or materialized views instead.

TypeImplementationBest ForCassandra Version
Legacy Secondary2iLow cardinality, equality queriesAll
SASISASIIndexText search, ranges3.4+
SAIStorageAttachedIndexGeneral purpose, ranges, text5.0+

Create a secondary index on a table column.

CREATE [ CUSTOM ] INDEX [ IF NOT EXISTS ] [ *index_name* ]
ON [ *keyspace_name*. ] *table_name* ( *index_target* )
[ USING '*index_class*' ]
[ WITH OPTIONS = { *option_map* } ]

index_target:

*column_name*
| KEYS ( *map_column* )
| VALUES ( *map_column* )
| ENTRIES ( *map_column* )
| FULL ( *frozen_collection_column* )

CREATE INDEX creates a secondary index enabling queries on the specified column. The index is built asynchronously after creation; the command returns before indexing completes.

Optional name for the index. If omitted, Cassandra generates a name in the format table_column_idx.

-- Named index
CREATE INDEX users_email_idx ON users (email);
-- Auto-named (becomes users_email_idx)
CREATE INDEX ON users (email);

Prevents error if index already exists.

Specifies what to index:

Index regular column values:

CREATE INDEX ON users (email);
CREATE INDEX ON users (country);

Index collection elements:

-- Index SET or LIST elements
CREATE INDEX ON users (tags); -- SET<TEXT>
CREATE INDEX ON users (phone_numbers); -- LIST<TEXT>
-- Index MAP keys
CREATE INDEX ON users (KEYS(attributes));
-- Index MAP values
CREATE INDEX ON users (VALUES(attributes));
-- Index MAP key-value pairs
CREATE INDEX ON users (ENTRIES(attributes));
-- Index entire frozen collection
CREATE INDEX ON users (FULL(frozen_addresses));
TargetCollection TypeEnables Query
columnSET, LISTWHERE column CONTAINS value
KEYS(column)MAPWHERE column CONTAINS KEY key
VALUES(column)MAPWHERE column CONTAINS value
ENTRIES(column)MAPWHERE column[key] = value
FULL(column)FROZENWHERE column = entire_collection

Specifies the index implementation class.

CREATE INDEX ON users (country);
  • Hash-based index
  • Equality queries only
  • Best for low-cardinality columns
CREATE CUSTOM INDEX ON users (email)
USING 'StorageAttachedIndex';

SAI (Cassandra 5.0+) provides:

  • Efficient numeric range queries
  • Text pattern matching (LIKE)
  • Better performance than legacy indexes
  • Lower storage overhead
CREATE CUSTOM INDEX ON users (username)
USING 'org.apache.cassandra.index.sasi.SASIIndex';

SASI Status

SASI is marked as experimental and is not recommended for production use. Use SAI (Cassandra 5.0+) instead.

Index-specific configuration options.

CREATE CUSTOM INDEX ON users (email)
USING 'StorageAttachedIndex'
WITH OPTIONS = {
'case_sensitive': 'false',
'normalize': 'true',
'ascii': 'true'
};
OptionTypeDefaultDescription
case_sensitivebooleantrueCase-sensitive text comparison
normalizebooleanfalseUnicode normalization
asciibooleanfalseASCII folding (é → e)
CREATE CUSTOM INDEX ON products (description)
USING 'org.apache.cassandra.index.sasi.SASIIndex'
WITH OPTIONS = {
'mode': 'CONTAINS',
'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer',
'case_sensitive': 'false'
};
OptionValuesDescription
modePREFIX, CONTAINS, SPARSEQuery matching mode
analyzer_classClass nameText analyzer for tokenization
case_sensitivetrue/falseCase sensitivity
max_compaction_flush_memory_in_mbNumberMemory limit for indexing
-- Simple column index
CREATE INDEX ON users (email);
-- Named index
CREATE INDEX users_country_idx ON users (country);
-- Index with IF NOT EXISTS
CREATE INDEX IF NOT EXISTS ON users (last_name);
-- Numeric column for range queries
CREATE CUSTOM INDEX ON products (price)
USING 'StorageAttachedIndex';
-- Case-insensitive text search
CREATE CUSTOM INDEX ON users (username)
USING 'StorageAttachedIndex'
WITH OPTIONS = {'case_sensitive': 'false'};
-- Multiple SAI indexes on same table
CREATE CUSTOM INDEX ON orders (status) USING 'StorageAttachedIndex';
CREATE CUSTOM INDEX ON orders (total) USING 'StorageAttachedIndex';
CREATE CUSTOM INDEX ON orders (created_at) USING 'StorageAttachedIndex';
-- Table with collections
CREATE TABLE user_profiles (
user_id UUID PRIMARY KEY,
tags SET<TEXT>,
attributes MAP<TEXT, TEXT>,
scores LIST<INT>
);
-- Index SET elements
CREATE INDEX ON user_profiles (tags);
-- Query: WHERE tags CONTAINS 'premium'
-- Index MAP keys
CREATE INDEX ON user_profiles (KEYS(attributes));
-- Query: WHERE attributes CONTAINS KEY 'department'
-- Index MAP values
CREATE INDEX ON user_profiles (VALUES(attributes));
-- Query: WHERE attributes CONTAINS 'engineering'
-- Index MAP entries
CREATE INDEX ON user_profiles (ENTRIES(attributes));
-- Query: WHERE attributes['department'] = 'engineering'
-- Equality query
SELECT * FROM users WHERE email = 'user@example.com';
-- SAI range query
SELECT * FROM products WHERE price > 100 AND price < 500;
-- SAI pattern matching
SELECT * FROM users WHERE username LIKE 'john%';
-- Collection query
SELECT * FROM user_profiles WHERE tags CONTAINS 'premium';
-- With partition key (most efficient)
SELECT * FROM orders
WHERE customer_id = ? AND status = 'pending';

Restrictions

  • Cannot index partition key columns (already indexed)
  • Cannot create multiple indexes on the same column
  • Cannot index COUNTER columns
  • Legacy indexes do not support range queries
  • LIKE queries require SAI or SASI indexes

Cardinality Considerations

CardinalityExampleIndex Recommendation
Very lowboolean, status (few values)Secondary index OK
Low-mediumcountry, categorySecondary index OK
Highemail, user_idAvoid; use primary key or denormalize
Uniqueuuid, timestampNever index; use primary key

High-cardinality indexes create large index structures on each node, causing:

  • High memory usage
  • Slow index lookups
  • Heavy read amplification
  • Index building occurs asynchronously after CREATE INDEX returns
  • Monitor index build progress: nodetool compactionstats
  • Indexes are stored in separate SSTable files
  • Dropping a table automatically drops all its indexes
  • Index updates are synchronous with writes, adding write latency

Index Build Time

For existing tables with data, index building can take significant time:

Terminal window
# Monitor index build progress
nodetool compactionstats
# View pending index builds
nodetool compactionstats | grep "Secondary index"

Remove a secondary index.

DROP INDEX [ IF EXISTS ] [ *keyspace_name*. ] *index_name*

DROP INDEX removes a secondary index. Queries using the index will fail after removal. The index data is deleted asynchronously.

Prevents error if index does not exist.

The name of the index to drop. Must be qualified with keyspace if not using USE.

-- Drop by name
DROP INDEX users_email_idx;
-- With keyspace qualification
DROP INDEX my_keyspace.users_email_idx;
-- Safe drop
DROP INDEX IF EXISTS users_country_idx;
-- List all indexes in keyspace
SELECT index_name, table_name, options
FROM system_schema.indexes
WHERE keyspace_name = 'my_keyspace';
-- Describe table to see indexes
DESCRIBE TABLE users;

Restrictions

  • Cannot drop index while queries are actively using it (they will fail)
  • Requires DROP permission on the table
  • Index drop is a metadata operation; data files are deleted asynchronously
  • Queries using the dropped index will fail with error
  • Consider application impact before dropping indexes in production

Good Use Cases

  • Low-to-medium cardinality columns (< 1000 unique values per partition)
  • Queries that usually include the partition key
  • Filtering within partitions
  • Collection element searches

Avoid Indexes When

  • Column has high cardinality (many unique values)
  • Queries never include the partition key
  • Column is frequently updated
  • Table has very large partitions
ScenarioAlternative to Secondary Index
High-cardinality lookupsCreate a lookup table with the column as partition key
Complex queriesUse materialized views
Full-text searchExternal search engine (Elasticsearch, Solr)
Range queries on multiple columnsDenormalized tables