SAI performance

Storage-Attached Indexing (SAI) is a highly-scalable, globally-distributed index that combines a storage-attached architecture with a number of highly optimized on-disk index structures.

SAI provides the most indexing functionality available for CQL with simplified data modeling:

  • Allows multiple indexes on the same table (one per column).

    There is a 1-to-1 mapping of an SAI index to a column. SAI doesn’t support many-to-1 or many-to-many relationships between columns and indexes. However, you can create a separate index on each column in a given table, and then use multiple indexes in the same query.

  • Supports most data types.

  • Enables vector search for AI applications.

  • Allows indexes on tables with static columns.

  • No need for multiple denormalized tables for particular query patterns. Create a table that is most natural for you, write to that one table, and then query it any way you want with SAI indexes.

  • Supports indexing on individual columns in a composite partition key. Before the introduction of SAI, you could index clustering keys, but you could not index parts of a composite partition.

  • Supports analyzers and non-tokenizing filters, such as tokenized text, case sensitivity, Unicode normalization.

Performance

SAI is deeply integrated with the storage engine of Cassandra, leveraging existing read/write components like memtables and SSTables. It doesn’t abstractly index tables; instead, SAI indexes memtables and SSTables as they are written, and it resolves the differences between those indexes at read time. Each memtable is an in-memory data structure resembling a write-back cache that is specific to a particular database table. Each SSTable is an immutable data file to which the database writes memtables periodically. SSTables are stored on disk sequentially and maintained for each database table.

Consequently, the design of SAI has very little operational complexity on top of the core database. From snapshot creation, to schema management, to data expiration, SAI integrates tightly with the capabilities and mechanisms that the core database already provides.

SAI outperforms other indexing methods available for CQL:

  • Shares common index data across multiple indexes on same table.

  • Alleviates write-time scalability issues.

  • Significantly reduced disk usage for indexing.

  • Great numeric range performance.

  • Zero copy streaming of indexes: When you bootstrap or decommission nodes in a cluster, the indexes are fully streamed with the SSTables and not serialized or rebuilt on the receiving node’s end.

SAI requires significantly lower disk usage compared to other native or bolt-on Cassandra index solutions. However, SAI introduces additional disk usage compared with unindexed data. SAI disk usage depends on the underlying data model and the number of columns indexed.

SAI write path

When an SAI index exists, SAI is notified of all mutations against the current memtable. SAI supports partition deletions, range tombstones, and row removals. SAI handles inserts and updates, which SAI treats exactly the same way. SAI also handles any delete operations in a query post-filtering step.

SAI write path overview

As a result, SAI incurs no special penalties when indexing frequently deleted columns. If an insert or update contains valid content for the indexed column, the content is fed to a memtable index. This structure is partitioned into sub-indexes — whose data structures differ based on content type — on Threads Per Core (TPC) boundaries. Incoming content is processed based on the token range containing the partition key for the updated row. This feature means sub-indexes are only written by a single TPC thread.

Then the primary key of the updated row is associated with the indexed value. SAI calculates an estimate of the incremental heap consumption of the new entry. This estimate counts against the heap usage of the underlying memtable. This feature also means that as more columns are indexed on a table, the memtable flush rate will increase, and the size of flushed SSTables will decrease. The number of total writes and the estimated heap usage of all live memtable indexes are exposed as metrics. See SAI metrics.

Memtable flush

Once the memtable flush threshold has been reached, SAI writes a new SSTable index for each indexed column, as the SSTable is being written. SSTable indexes are built when the flush operation occurs.

Write path flow as detailed in subsequent text

The flush operation has two phases:

  1. In the first phase, rows are written to the new SSTable.

    For each one, a row ID is generated and three index components are created. The components are:

    • On-disk mappings of row IDs to their corresponding token values. SAI supports only the Murmur3Partitioner.

    • SSTable partition offsets

    • A temporary mapping of primary keys to their row IDs, which is used in a subsequent phase

    The contents of the first two component files are compressed arrays whose ordinals correspond to row IDs.

  2. In the second phase, after all rows are written to the new SSTable and the shared SSTable-level index components have been completed, SAI begins its indexing work on each indexed column.

    SAI flushes the contents of memtable indexes directly to disk, rather than creating an additional in-memory representation. This is possible because memtable indexes are sorted by term/value and then by primary key.

Specifically in the second phase, SAI iterates over the memtable index to produce pairs of terms and their token-sorted row IDs. This iterator translates primary keys to row IDs using the temporary mapping structure built in the first phase. The terms iterator (with postings) is then passed to separate writing components based on whether each indexed element is for a string or numeric column:

  • String: The SAI index writer iterates over each term, first writing its postings (term/row pairs) to disk, and then recording the offset of those postings in the postings file as the payload of a new entry (for the term itself) in an on-disk, byte-ordered trie.

    The trie is heap-friendly, providing string prefix compression for terms, and can match any query that can be expressed as a deterministic finite automaton. The feature minimizes on-disk footprint, supports simple token skipping, and optimizes equality and non-exact matching on strings.

  • Numeric and non-literal types (timestamp, date, UUID): SAI separates the writing of a numeric index into two steps:

    1. The terms are passed to a balanced k-dimensional tree (kd-tree) writer, which writes the kd-tree to disk. As the leaf blocks of the tree are written, their postings are recorded temporarily in memory.

    2. Those temporary postings are then used to build postings on-disk, at the leaves, and at various levels of the index nodes.

    A kd-tree is a balanced structure that provides fast lookups across one or more dimensions, compression for values and postings, and optimizes equality and range queries on numeric and non-literal types.

When a column index flush completes, a special empty marker file is flagged to indicate success. This procedure is used on startup and incremental rebuild operations to differentiate case where the SSTable index files are missing for a column, or there is no indexable data (such as when an SSTable only contains tombstones).

SAI then increments a counter on the number of memtable indexes flushed for the column, and adds to a histogram the number of cells flushed per second.

Compaction

For SAI, when compaction is triggered, each index group creates an SSTable Flush Observer to coordinate the process of writing all attached column indexes from the newly compacted data in their respective SSTable writer instances.

Unlike memtable flush (where indexed terms are already sorted), when iterating merged data during compaction, SAI buffers indexed values and their row ids, which are added in token order. To avoid issues such as exhausting available heap resources, SAI uses an accumulated segment buffer, which is flushed to disk synchronously by using a proprietary calculation. Then each segment records a segment row ID offset and only stores the segment row ID (SSTable row ID minus segment row ID offset).

SAI flushes segments into the same file synchronously to avoid the cost of rewriting all segments, and to reduce the cost of partition-restricted queries and paging range queries. This reduces the search space.

The on-disk layout from the per-column indexed posting, to the SSTable offset, to the SSTable partition:

SAI on-disk layout as described in surrounding text

The actual segment flushing process is very similar to memtable flush. However, buffered terms are sorted before they can be written with their postings to their respective type-specific on-disk structures. At the end of compaction for a given index, a special empty marker file is flagged to indicate success, and the number of segments is recorded in SAI global indexing metrics.

When the entire compaction task finishes, SAI receives an SSTable List Changed Notification that contains the SSTables added and removed during the transaction. SSTable Context Manager and Index View Manager are responsible for replacing old SSTable indexes with new ones atomically. At this point, new SSTable indexes are available for queries.

SAI read path

This section explains how index queries are processed by the SAI coordinator and executed by replicas. Unlike legacy secondary indexes, where at most one column index will be used per query, SAI implements a Query Plan that makes it possible to use all available column indexes in a single query.

SAI read path as described in subsequent sections

Index selection and coordinator processing

When presented with a query, the first action the SAI coordinator performs, to take advantage of one or more indexes, is to identify the most selective index. That is, the index that will most aggressively narrow the filtering space and the number of ultimate results. Specifically, the most selective index is the one that returns the lowest value from an estimated results row calculation. If multiple SAI indexes are present (that is, where each SAI index is based on a different column, but the query involved more than one column), it does not matter which SAI index is selected first.

Once the best index for the read operation is selected, the index is embedded with the read command, which enters the distributed range read apparatus. A distributed range read queries the ring in one or more rounds in token order. The SAI coordinator estimates the number of rows per range based on local data and the query limit to determine the number of ranges to contact. That is, to estimate the concurrency factor. For each round, a concurrency factor determines how many replicas will be queried in parallel.

  1. Before the first round commences, SAI estimates the initial concurrency factor via a proprietary calculation, shown here as Step 1.

    SAI step to estimate a concurrency factor as described in surrounding text

    Once the initial concurrency factor established, the range read begins.

    SAI iterates through the ranges as described in surrounding text
  2. SAI coordinator sends requests to the required ranges in parallel based on the Concurrency factor.

  3. SAI coordinator waits for the responses from the requested replicas.

  4. SAI coordinator collects the results and recomputes the concurrency factor based on returned rows and query limit.

At the completion of each round, if the limit has not been reached, the concurrency factor is adjusted to take into account the shape of the results already read. If no results are returned from the first round, the concurrency factor is immediately increased to the minimum calculation of remaining token ranges and the maximum calculation of the concurrency factor. If results are returned, the concurrency factor is updated. SAI repeats steps 2, 3, and 4 until the query limit is satisfied.

SAI repeats the steps until the limit is satisfied as described in surrounding text

To avoid querying replicas with failed indexes, each node propagates its own local index status to peers via gossip. At the coordinator, read requests will filter replicas that contain non-queryable indexes used in the request. In most cases, the second round of replica queries should return all the necessary results. Further rounds may be necessary if the distribution of results around the replicas is extremely imbalanced.

Column cardinality can affect read performance when it comes to range queries among replicas. The number of rows matching a value of a high-cardinality column, such as credit card numbers, is more likely to be isolated on very few nodes (or even isolated to one node), while the rows matching a value on a low-cardinality column are more likely to reside on numerous nodes.

If a query does not specify a partition key, the coordinator scans the token ring and groups token ranges by endpoints (nodes). The coordinator then concurrently executes read commands for all participating endpoints. In the worst case, where the indexed column has very high cardinality, an entire cluster scan might be required before finding a match.

With low-cardinality columns, be aware that if the query LIMIT is higher than the number of values in your targeted column, the database has to search all replicas again before determining that the LIMIT cannot be satisfied. In this case, the database returns only the number of matching results.

Replica query planning and view acquisition

Once a replica receives a token range read request from the SAI coordinator, the local index query begins. SAI implements an index searcher interface via a Query Plan that makes it possible to access all available SAI column indexes in a single query.

The Query Plan performs an analysis of the expressions passed to it via the read command. SAI determines which indexes should be used to satisfy the query clauses on the given columns. Once column expressions are paired with indexes, a view of the active SSTable indexes for each column index is acquired by a Query Controller. In order to avoid compaction removing index files used by in-flight queries, before reading any index files, the Query Controller tries to acquire references to the SSTables for index files that intersect with the query’s token range, and releases them when the read request completes.

At this point, a Token Flow is created to stream matches from each column index. Those flows, along with the Boolean logic that determines how they are merged, is wrapped up in an Operation, which is returned to the Query Plan component.

Role of the SAI Token Flow framework

The SAI query engine revolves around a Token Flow framework that defines how SAI asynchronously iterates over, skips into, and merges streams of matching partitions from both individual SSTable indexes and entire column indexes. SAI uses a token to describe a container for partition matches within a Cassandra ring token.

Iteration is the simplest of the three operations. Specifically, the iteration of postings involves sequential disk access — via the chunk cache — to row IDs, which are used to lookup ring token and partition key offset information.

Token skipping is used to skip unmatched tokens when continuing from the previous paging boundary, or when a larger token is found during token intersection.

Match streaming and post-filtering example

Consider an example with an individual column index (such as age = 44), the flow produced is the union of all memtable indexes and all SSTable indexes.

  • SAI iterates over each memtable index "lazily" (that is, one at a time) in token order through its individual token range-partitioned instances. This feature reduces the overhead that would occur otherwise from unnecessary searches of data toward the end of the ring.

  • On-disk index: SAI returns the union of all matching SSTable index. Within one SSTable index, there can be multiple segments because of the memory limit during compaction. Similar to the memtable index, SAI lazily searches segments in token sorted order.

When there are multiple indexed expressions in the query (such as WHERE age=44 AND state='CA') connected with AND query operator, the results of indexed expressions are intersected, which returns partition keys that match all indexed expressions.

SAI intersection of age and state columns

After the index search, SAI exposes a flow of partition keys. For every single partition key, SAI executes a single partition read operation, which returns the rows in the given partition. As rows are materialized, SAI uses a filter tree to apply another round of filtering. SAI performs this subsequent filtering step to address the following:

  • Partition granularity: SAI keeps track of partition offsets. In the case of a wide partition schema, not all rows in the partition will match the index expressions.

  • Tombstones: SAI does not index tombstones. It’s possible that an indexed row has been shadowed by newly added tombstones.

  • Non-indexed expressions: Operations may include non-index expressions for which there are no index structures.

SAI is not an enterprise search engine. At its core, SAI is a filtering engine that uses indexes on table columns. While it provides some of the same functionality, SAI is not a complete replacement for DSE Search (Solr).

SAI advantages
  • Simplifies data modeling for client applications that would otherwise rely heavily on maintaining multiple query-specific tables.

  • No schema management like with Solr indexes. The schema/indexing options reside in the index metadata, which is handled by native database schema management.

  • Simplified configuration using existing database parameters in cassandra.yaml and other common locations.

  • No Solr commit log to accept writes during bootstrapping, so SAI doesn’t need to wait to read the database configuration like Solr does.

  • Whereas DSE Search doesn’t support indexes on tables that contain static columns, SAI supports indexes based on static columns that use much less disk space and perform better than Solr-based indexes on non-static fields.

Can the same table have DSE Search and SAI indexes?

You cannot have a DSE Search index and an SAI index on the same table.

How does write performance compare for SAI and DSE Search

Updates to DSE Search indexes require resource intensive read-before-write operations against the partition or row being indexed. In contrast, the SAI write path is integrated with the database write path, caching index updates along with the data mutations and applying the writes atomically. DataStax testing observed 86% better write throughput and 670% better latency with SAI compared to DSE Search. SAI read and write latencies were often less than 10 ms.

Should I migrate from DSE Search to SAI?

This depends on your performance targets and whether your queries and data are appropriate for SAI. In a development environment, you can use the following general approach to test SAI on a few tables and compare performance with DSE Search:

  1. Establish baseline performance test targets.

  2. Determine which queries currently depend on DSE Search and whether they are appropriate use cases for SAI.

    For example, denormalized tables created to enable queries on non-primary key columns of the base table could be replaced by multiple SAI indexes on one table.

  3. For the relevant queries, create SAI indexes on the relevant columns for your query patterns:

    • Drop Solr indexes and then create SAI indexes on your existing tables, or create new tables with SAI indexes for testing.

    • For denormalized tables, create one SAI index on each relevant column on the base table, and then drop the redundant tables.

    • Create separate tables with SAI indexes. Ideally, use a completely separate test environment so your SAI performance test results aren’t disrupted by Solr workloads in the same cluster.

  4. Update your application to use SAI CQL statements where it previously used DSE Search queries.

  5. Remove Solr Cores.

  6. Remove Solr nodes.

  7. Run performance tests again and compare results.

Was this helpful?

Give Feedback

How can we improve the documentation?

© Copyright IBM Corporation 2026 | Privacy policy | Terms of use |  Manage Privacy Choices

Apache, Apache Cassandra, Cassandra, Apache Tomcat, Tomcat, Apache Lucene, Apache Solr, Apache Hadoop, Hadoop, Apache Pulsar, Pulsar, Apache Spark, Spark, Apache TinkerPop, TinkerPop, Apache Kafka and Kafka are either registered trademarks or trademarks of the Apache Software Foundation or its subsidiaries in Canada, the United States and/or other countries. Kubernetes is the registered trademark of the Linux Foundation.

General Inquiries: Contact IBM