Data modeling and schema tuning for Apache Cassandra databases

Data models and the resulting schemas (keyspaces, tables, and indexes) influence the performance of your Apache Cassandra cluster and associated applications that you develop. Optimal data modeling typically leads to better performance, scalability, and stability for your Apache Cassandra cluster and applications. Suboptimal data models can lead to poor performance, even with small amounts of data.

This guide introduces important data modeling concepts, strategies you can use to evaluate your data model, and considerations for properly tuned schemas.

Prepare your data model

Data modeling includes theoretical designs as well as the practical outcomes of those designs. For example:

  • Logical and physical models

  • Data access patterns (queries)

  • Keyspaces and replication

  • Indexes

  • Table properties like compaction and compression

  • Table schema elements like columns and primary keys

  • Hot data and caches

To get started with data modeling, see the following:

If you are migrating from a relational database, be aware that there are significant differences between RDBMS and Cassandra-based data models.

Notably, data models are based on query patterns, not entities and relationships. Additionally, distributed databases scale horizontally by adding more nodes to the cluster rather than relying on a single machine.

Often, developers migrating from relational databases are attracted to materialized views, secondary indexes, and user-defined functions (UDFs). However, these patterns aren’t ideal for heavy usage in distributed NoSQL databases due to performance impacts at scale. Review this guide and the related information to understand the purpose and requirements for different data types and auxiliary indexes.

Tune table properties

When you create or alter a table, you can specify table configuration properties, including the compaction strategy and data compression options. Understanding how to use and tune these parameters is important for preparing your data model and planning your clusters.

To check the properties for an existing table, use the DESCRIBE TABLE command.

Compaction

The UnifiedCompactionStrategy (UCS) is recommended for all Apache Cassandra versions that support it. For earlier versions, the default SizeTieredCompactionStrategy (STCS) is acceptable for most use cases.

If you intend to use TimeWindowCompactionStrategy (TWCS) for time series data and expiring time-to-live (TTL) workloads, you must enable TWCS when you create the table. You cannot apply TWCS to existing tables that weren’t created with the proper time windowing layout.

For information about the compaction process, supported compaction strategies, and table properties for compaction, see the following:

In addition to the table properties for compaction, there are settings in cassandra.yaml that influence compaction behavior and performance for the entire node:

  • compaction_throughput or compaction_throughput_mb_per_sec

  • concurrent_compactors

  • memtable_* parameters, particularly when using LCS

  • commitlog_* parameters

You can use write survey mode to test compaction and compression configurations on an isolated node.

Many pending compactions is a strong indicator of suboptimal compaction configuration. Use metrics retrieval and observability tools to get pending compaction statistics. For example, to get the current number of pending compactions for a node, run one of the following commands:

nodetool sjk mx -f Value -mg -b org.apache.cassandra.metrics:type=Compaction,name=PendingTasks
nodetool compactionstats

Compression

To decrease disk consumption, Cassandra stores data on disk in compressed form by default. For this reason, misconfigured compression can significantly degrade performance.

If you alter a table’s compression settings, the new configuration only applies to new SSTables.

To apply the new settings to all existing SSTables, use nodetool upgradesstables with the -a option:

nodetool upgradesstables -a KEYSPACE_NAME TABLE_NAME

For more information about compression, see the following:

Disable background read repair in earlier versions

Read repair happens when Cassandra detects data inconsistencies during a read operation. By default, it blocks application operations until the repair process is complete. Read repair behavior and options depend on your Cassandra version.

Earlier Cassandra versions included a background read repair option that was set with read_repair_chance and dclocal_read_repair_chance. This behavior was removed in Cassandra 4.0, and this version of background read repair isn’t recommended in earlier versions.

If you are running an earlier version that includes background read repair, set read_repair_chance and dclocal_read_repair_chance to 0 to disable background read repair. You can use a script to disable background read repair on all tables on a node, for example:

for i in `cqlsh -e 'describe schema;' |grep 'CREATE TABLE'|sed -e 's|CREATE TABLE \([^ ]*\) .*|\1|'`; do
 echo "alter table $i with read_repair_chance = 0.0 and dclocal_read_repair_chance = 0.0;"
done| tee alters.cql
cqlsh -f alters.cql

For more information, see CASSANDRA-9753 and CASSANDRA-13910.

Repair down nodes to avoid resurrected deletes (zombies)

It’s important that you understand how tombstones work and their impact on database performance. Specifically, you need to know how to address down nodes to avoid resurrecting deletes that a down node missed.

When a mutation deletes data, the database marks the data with a tombstone. This helps ensure consistency across replicas and proper reconciliation during compaction. For more information, see About Deletes and Tombstones in Apache Cassandra and Tombstones in Apache Cassandra.

The gc_grace_seconds property sets the length of time that a tombstone is retained in SSTables. When this time period expires, deleted data and tombstones are cleared from the SSTables during compaction.

If any nodes in a cluster go down when data is being deleted, those nodes must be returned to the cluster and repaired before gc_grace_seconds expires. Otherwise, deleted data can be resurrected because the desynchronized nodes don’t have a tombstone for the deleted data. If a node is down or not repaired for longer than gc_grace_seconds, you must clear all Cassandra data from the node and replace the node.

If a cluster is performing poorly due to too many tombstones, you can reduce gc_grace_seconds to clear tombstones more frequently. However:

  • After changing gc_grace_seconds you must run a full token range repair within the new gc_grace_seconds period.

  • If gc_grace_seconds is extremely low, tombstones might be removed before all replicas have seen the deletion, resulting in resurrected data (zombies).

  • If gc_grace_seconds is too low, it can impact hinted handoff by preventing collection and replaying of hints. If this occurs, repair is required.

Get the schema

After creating your keyspaces and tables in your Cassandra cluster, get the schema to evaluate your data model.

The schema is included in a diagnostic tarball generated by the DataStax diagnostic collection scripts.

You can also get the schema with cqlsh:

  1. Connect to your Cassandra cluster using cqlsh.

  2. Run the DESCRIBE SCHEMA command and pipe the output to a file:

    cqlsh -e 'DESCRIBE SCHEMA;' > schema.cql

The examples in this guide assume you have a running Cassandra cluster with keyspaces and tables already created, and you have output the schema definition to a file named schema.cql. If you use a different file name, edit the example commands accordingly.

For diagnostic tarballs, schema.cql is located at /driver/schema for each node.

The schema is retrieved from a single node. Typically, this is acceptable because schema changes are propagated to all nodes in the cluster.

In situations where you need to compare nodes or investigate desynchronized nodes, use nodetool commands. These commands run on every node in a cluster to provide a complete report of the cluster state. Then, you can compare the output for the anomalous nodes.

Evaluate keyspace replication

  • Use NetworkTopologyStrategy for production.

  • Use an odd number for the replication factor, typically 3 or 5. Higher replication is usually not necessary. Even-numbered replication makes clusters less resistant because there are no redundant nodes to serve requests in the case of a node failure. Additional duplication and preservation of data for failover or disaster recovery are better handled through multi-datacenter clusters and backups.

  • When you add or move a datacenter, you must manually reconfigure replication for the system_auth keyspace.

Fix replication problems

To fix problems with replication, you can either run the ALTER KEYSPACE command manually, or use a script to perform these actions automatically.

  • System keyspaces that use LocalStrategy or EverywhereStrategy must be left unchanged.

  • You must run nodetool repair on all keyspaces where a change is made.

For example, the following script can help you adjust the replication factor of system and user-provided keyspaces based on the current cluster topology. It generates a file with CQL commands that you can run after reviewing them.

adjust-keyspaces.sh script
#!/bin/bash

rep_factor=3
fix_system=1
keyspaces=("system_auth" "system_distributed"  "dse_security" "solr_admin" "dse_perf" "dse_leases" "dse_analytics" '"HiveMetaStore"' "system_traces" "dse_advrep" "dse_system")
cqlsh_options=""
nodetool_options=""

function usage() {
    echo "Usage: $0 [-n replication_factor] [-c cqlsh_options_as_a_string] [-o nodetool_options_as_a_string] [keyspace1 keyspace2 ...]"
    echo "Defaults:"
    echo "   replication_factor = $rep_factor"
    echo "   keyspace1.. = name(s) of keyspace(s) to fix replication factor."
    echo "                 fix all system keyspaces if not specified"
}

while getopts ":hc:o:n:" opt; do
    case $opt in
        n) rep_factor=$OPTARG
           ;;
        c) cqlsh_options=$OPTARG
           ;;
        o) nodetool_options=$OPTARG
           ;;
        h) usage
           exit 0
           ;;
        *) usage
           exit 1
           ;;
    esac
done
shift "$((OPTIND -1))"

if [ -z "$(command -v nodetool)" ]; then
    echo "Can't find nodetool in the PATH"
    exit 1
fi

if [ -z "$(command -v cqlsh)" ]; then
    echo "Can't find cqlsh in the PATH"
    exit 1
fi

if [ $# -ne 0 ] ; then
    keyspaces=()
    fix_system=0
    for ks in "$@" ; do
        keyspaces+=("$ks")
    done
fi

my_pid=$$

NODETOOL_FILE=/tmp/nodetool-out-${my_pid}.txt
SCHEMA_FILE=/tmp/cqlsh-schema-${my_pid}.txt

# File with commands to execute
CQL_FILE=/tmp/fix-keyspaces-${my_pid}.cql
rm -f $CQL_FILE
touch $CQL_FILE

nodetool $nodetool_options status > $NODETOOL_FILE
RES=$?
if [ $RES -ne 0 ] ; then
    echo "Can't execute 'nodetool status'! Exit code: $?"
    exit 1
fi

if grep -e '^[UD][NJLM] ' "$NODETOOL_FILE"|grep -v -e '^UN ' > /dev/null 2>&1 ; then
    echo "Cluster has nodes with non-UN status, can't adjust replication factor!"
    exit 1
fi

cqlsh $cqlsh_options -e 'DESCRIBE FULL SCHEMA;' > $SCHEMA_FILE
RES=$?
if [ $RES -ne 0 ] ; then
    echo "Can't get schema via cqlsh! Exit code: $?"
    exit 1
fi

declare -A all_ks
for i in $(grep 'CREATE KEYSPACE' "$SCHEMA_FILE"|grep -e 'SimpleStrategy\|NetworkTopologyStrategy'|sed -e 's|^CREATE KEYSPACE \([^ ]*\).*$|\1|'); do
    all_ks[$i]=1
done
#echo "All keyspaces=${!all_ks[@]}"

declare -A all_dcs

curr_dc=''
cnt=0
while read -d $'\n' line ; do
#    echo "$line"
    if echo "$line"|grep -e '^Datacenter: '  > /dev/null 2>&1 ; then
        new_dc=$(echo "$line"|sed -e 's|^Datacenter: \([^ ]*\).*$|\1|')
#        echo "new_dc=$new_dc"
        if [ -z "$new_dc" ] ; then
            echo "Can't extract DC from line '$line'"
            exit 1
        fi
        if [ -z "$curr_dc" ] ; then
            curr_dc=$new_dc
        else
            max_rf=$rep_factor
            if [ $rep_factor -gt $cnt ]; then
                max_rf=$cnt
            fi
            echo "$curr_dc has $cnt nodes max RF=$max_rf"
            all_dcs[$curr_dc]=$max_rf
            curr_dc=$new_dc
            cnt=0
        fi
    fi
    if echo "$line"|grep -e '^[UD][NJLM] ' > /dev/null 2>&1 ; then
        ((cnt++))
    fi
done < $NODETOOL_FILE
# push the last DC as well
max_rf=$rep_factor
if [ $rep_factor -gt $cnt ]; then
    max_rf=$cnt
fi
echo "$curr_dc has $cnt nodes max RF=$max_rf"
all_dcs[$curr_dc]=$max_rf

#echo "All DCs=${!all_dcs[@]}"
if [ ${#all_dcs[@]} -eq 0 ]; then
    echo "Can't identify data centers!"
    exit 1
fi

to_repair=()
for i in "${keyspaces[@]}"; do
#    echo "Processing $i"
    if [ -z "${all_ks[$i]}" ]; then
        if [ $fix_system = "0" ]; then
            echo "$i not in the list of existing keyspaces!"
        fi
        continue
    fi
    echo -n "ALTER KEYSPACE $i WITH replication = {'class': 'NetworkTopologyStrategy'" >> $CQL_FILE
    for key in "${!all_dcs[@]}"; do
        echo -n ", '$key': ${all_dcs[$key]}" >> $CQL_FILE
    done
    echo "};" >> $CQL_FILE
    to_repair+=("$i")
done

ret_code=0
if [ ${#to_repair[@]} -eq 0 ]; then
    rm -f $CQL_FILE
    echo "No keyspaces processed!"
    ret_code=1
else
    echo "Please execute command 'cqlsh -f $CQL_FILE $cqlsh_options' to adjust replication factor for keyspaces"
    echo "After that, execute following commands on each node of the cluster:"
    for i in "${to_repair[@]}" ; do
        echo "nodetool $nodetool_options repair -pr $i"
    done
fi

# remove already processed files
rm -f $SCHEMA_FILE $NODETOOL_FILE

exit $ret_code
Usage

The script must be executed on one of the nodes in your cluster because it uses cqlsh and nodetool to find cluster topology and extract a list of keyspaces.

The script accepts several command line parameters, and it supports clusters with no authentication, as well as clusters with non-default configuration:

  • -h: Get usage information.

  • -n replication_factor: Specify the desired replication factor for keyspaces. If this parameter is greater than the number of nodes in a specific datacenter, then the number of nodes is used instead.

  • -c cqlsh_options: All options that must be passed to cqlsh, such as authentication.

  • -o nodetool_options: All options that must be passed to nodetool.

  • Optional list of keyspaces for which adjustment should be done. If not specified, a hardcoded list of system keyspaces is used.

Output

The script generates a file with CQL commands for changing replication factors of keyspaces, and it prints usage instructions, including a nodetool repair command for each keyspace that would be changed. You must run nodetool repair on all keyspaces where a change is made. For example:

SearchAnalytics has 3 nodes max RF=3
Please execute command 'cqlsh -f /tmp/fix-keyspaces-7111.cql' to adjust replication factor for keyspaces
After that, execute following commands on each node of the cluster:
nodetool  repair -pr system_auth
nodetool  repair -pr system_distributed
nodetool  repair -pr dse_security
nodetool  repair -pr dse_perf
nodetool  repair -pr dse_leases
nodetool  repair -pr dse_analytics
nodetool  repair -pr "HiveMetaStore"
nodetool  repair -pr system_traces

The script adjusts replication for the system_auth, system_distributed, and system_traces keyspaces.

Evaluate the number of tables

Using your schema.cql file, use the following command to check how many tables and keyspaces are in your cluster:

grep 'CREATE TABLE' schema.cql |wc -l

Having too many tables in a cluster can cause substantial performance degradation. Inherently, throughput drops as the number of tables increases.

Because performance is influenced by a range of other factors, it is difficult to determine a universal table threshold. You can use the following guidelines as a starting point:

  • Set the tables_warn_threshold guardrail to 500 or less, depending on your cluster resources and workloads. For SAI-heavy and vector search workloads, set this guardrail lower (100 to 200).

    Clusters can support more tables, but this warning alerts you that you might need to audit underutilized tables or start planning to rearchitect your data model and schemas.

  • Set the tables_fail_threshold guardrail to 1000 or less, depending on your cluster resources and workloads. For SAI-heavy and vector search workloads, set this guardrail much lower (200 to 500).

    Distributed database clusters almost universally experience performance degradation issues at or before 1000 tables, such as excessive memory usage, long-running compactions, and query failures. While some clusters might remain stable, this many tables is usually evidence of an inefficient data model.

    For example, a Cassandra data modeling best practice is one table per major query pattern; however, if your applications use 1000 or more substantially different query patterns, your query patterns could probably be optimized through prepared statements or indexes.

  • Understand how JVM heap and byte count can reduce a cluster’s table threshold:

    • Each table uses a small amount of memory for metadata.

    • All tables produce memtables and, through compaction, SSTables.

    • The number of columns and rows in a table can increase pressure on memory by storing more auxiliary data, such as bloom filters, caches, and indexes.

    • Each keyspace causes additional overhead in JVM memory. Having many keyspaces can reduce your cluster’s actual table capacity, regardless of the number of tables per keyspace.

  • Tune table properties for appropriate resource usage and performance.

Evaluate keys and partitions

Primary keys, partitions, and clustering columns are fundamental to query performance. For an introduction to these concepts, see the following:

Partition size

For efficient operation, partitions must be sized within certain limits. Partition size can be measured by the number of values in the partition and the partition size on disk.

A cell is a stored value in one column in one row. In addition to the actual data value, each cell has associated metadata, such as timestamp, optional TTL, and additional data for more complex data types, such as collections.

Cassandra has a practical limit of 2 billion (231) cells per partition. It is difficult to exactly predict the required disk space because this depends on the number of rows, columns, primary key columns, and static columns in the table. However, for most use cases, DataStax recommends fewer than 100,000 items per partition and partitions smaller than 100 MB.

The presence of large partitions (greater than 100 MB) is a sign of a suboptimal data model, and they create an additional load on the database. For example:

  • Low cardinality of partition keys.

  • Non-uniform spread of data between partitions.

  • Too many columns and rows in the table, especially when every row contains data for all or most columns.

  • Storing large BLOBs or long text strings in cells.

The performance impacts of large partitions include:

  • Imbalanced partitions cause some nodes to run more operations (hotspots) and trigger compaction more often.

  • More data must be transferred when reading the whole partition.

  • Performance degradation in external systems. For example, a partition is the minimal object mapped into a Apache Spark™ partition, so any imbalance in the database partitions can lead to an imbalance when processing data with Spark.

For more information, see the following:

Get information about partitions

If your tables have large partitions, low cardinality partitions, or other issues with partitions, the only long term solution is to change your data model and recreate the tables with more performant partition keys and clustering columns.

Use the following tools to get information about partitions, including the size and cardinality:

nodetool tablehistograms

Use the nodetool tablehistograms command to find the partition sizes for 75, 95, 99, and 100 percentiles:

nodetool tablehistograms KEYSPACE_NAME TABLE_NAME

If the output reports significantly different Partition Size and Cell Count values for each partition, then it is likely that the partition key values are unevenly distributed across partitions, there are too many columns in the table, or too many elements in non-frozen collection columns.

test/widerows histograms
Percentile  SSTables     Write Latency      Read Latency    Partition Size        Cell Count
                              (micros)          (micros)           (bytes)
50%             1.00            126.93           1310.72            545791               103
75%             1.00            152.32           1310.72            545791               124
95%             1.00            785.94           1310.72            545791               124
98%             1.00           1629.72           1310.72            545791               124
99%             1.00           2346.80           1310.72            545791               124
Min             1.00             73.46           1310.72            454827                87
Max             1.00           2346.80           1572.86            545791               124

Similar information can be obtained from the sstablepartitions command.

nodetool tablestats

Information about maximum partition size is available through nodetool tablestats.

In the output, check the following:

  • Compacted partition maximum bytes: Make sure the values are smaller than the recommended 100 MB.

  • Number of partitions: This is an estimate that can indicate low cardinality partition keys.

dsbulk

You can use DataStax Bulk Loader (DSBulk) to identify partition keys that have the largest number of rows if your cluster has a non-uniform spread of partition key values. The main advantage of DSBulk is that it works with the whole cluster. For example, to find the largest partitions in a table:

dsbulk count -k KEYSPACE_NAME -t TABLE_NAME --log.verbosity 0 --stats.modes partitions
Result
'29' 106 51.71
'96' 99 48.29
sstablemetadata

In Cassandra 4.0 and later, you can use the sstablemetadata utility with the -s command line parameter to identify the largest partitions in specific SSTables. For Cassandra 3.x, use the sstable-tools project.

sstablemetadata provides information about the largest partitions as both row count and size in bytes. However, it runs on individual SSTable files, so it could be difficult to analyze partitions that are split across multiple files.

sstablemetadata -u -s path_to_file/mc-1-big-Data.db
Result
SSTable: /Users/ott/var/dse-5.1/data/cassandra/data/datastax/vehicle-8311d7d14eb211e8b416e79c15bfa204/mc-1-big
Size: 58378400
Partitions: 800
Tombstones: 0
Cells: 2982161
WidestPartitions:
  [266:20180425] 534
  [202:20180425] 534
  [232:20180425] 534
  [187:20180425] 534
  [228:20180425] 534
LargestPartitions:
  [266:20180425] 134568 (131.4 kB)
  [202:20180425] 134568 (131.4 kB)
  [232:20180425] 134568 (131.4 kB)
  [187:20180425] 134568 (131.4 kB)
  [228:20180425] 134568 (131.4 kB)
...
SELECT DISTINCT

To check for low cardinality partition keys, you can run a SELECT DISTINCT query on the partition key columns, and then check the count in the output for the number of distinct values returned.

SELECT DISTINCT partition_key_list, count(*) FROM table

Evaluate the number of columns per table

DataStax doesn’t recommend creating tables with hundreds or thousands of columns for the following reasons:

  • It is easy to exceed the recommended maximum number of cells per partition and columns per row.

  • Every cell has a storage cost.

  • Range scans perform poorly.

If a table has too many columns, analyze access patterns for that table, and then consider the following solutions:

  • If multiple columns are always read together, combine those columns into one frozen user-defined type (UDT) where all data in the UDT is written as one cell per row.

  • Handle serialization and deserialization of data at the application level, not within the table schema.

  • Where appropriate, store values as BLOBs.

  • Split data into separate tables.

Evaluate data types

Cassandra provides a rich set of data types that can be used for table columns. Because so many data types exist, it can be easy for new users to choose an incorrect or suboptimal data type. Using appropriate data types helps ensure efficient storage and accurate query results.

The following list describes notable antipatterns to avoid when choosing data types. It doesn’t describe all data types or all possible antipatterns.

Avoid overreliance on the text type

Don’t use the text type when another specialized type is more appropriate. It can use excess storage or produce incorrect query results.

For example, don’t use text to store timestamps. A timestamp encoded as text using ISO-8601 notation occupies 28 bytes, while the specialized timestamp type uses only 8 bytes.

Choose numeric types with appropriate precision

Don’t use numeric types that have larger value ranges than are necessary for the data because this stores values at an unnecessary precision, wasting space on irrelevant values. For example, don’t use bigint (8 bytes) when int (4 bytes) is sufficient. The excess storage is even worse when decimal and varint types are used incorrectly because they don’t have a fixed size; they are sized based on the actual value.

Range queries can fail silently when inappropriate types are used for sorted data

Don’t use string types when numeric or chronological ordering is needed. For example, storing numbers or timestamps as text can cause range queries (greater than, less than, equal to) to fail because the text representation of those values doesn’t support the required ordering for comparison. If a sortable type is used, but it isn’t the correct type for the data, then range queries can run successfully but return incorrect results.

Prefer collections (maps, lists, sets) over UDTs and tuples

To store multiple values in a single column, you can use collections (maps, lists, sets), user-defined types (UDTs), and tuples:

  • Collections are preferred when possible, particularly frozen collections.

    Performance degradation is possible when a database has large collections and non-frozen collections. This includes read amplification, non-idempotent writes, increased storage overhead, and extra read-before-write operations. To learn when to use collections, how to create efficient collections, and how to reduce performance impacts, see Collections in CQL.

  • UDTs are useful because they are customizable. However, they are more complex to manage and prone to becoming overloaded with too many elements. Consider collections before falling back to UDTs. When UDTs are used, use them deliberately and implement organizational policies that reduce the likelihood of uncontrolled alterations to UDTs.

  • DataStax recommends collections and UDTs over tuples. Tuples are always frozen, requiring a full rewrite for each update with explicit null values for unset/empty elements, and they require additional handling during upgrade and migrations to avoid data loss.

Collections, UDTs, and tuples all have practical and literal size limits that must be monitored and preemptively addressed to avoid performance degradation.

Prefer frozen collections when using collections

Collection types (map, list, set) can be either frozen or non-frozen. This is an important distinction that can impact application performance and data model designs.

Counters are non-idempotent and restrict the table schema

The counter type is useful for integers that are modified by increments or decrements. However, because counter operations are non-idempotent, they cannot be retried automatically without risking overcounts or undercounts. Additionally, the counter type places restrictions on the table schema and operations like ALTER TABLE and INSERT. For example, tables with counter columns can have only counter columns and the primary key columns. If you choose to use counter columns, be aware of the limitations, and make sure your application logic accounts for the non-idempotent nature of counter operations. For more information, see CQL data types: Counter.

BLOBs have a practical limit of 2 GB

Storing large BLOBs, such as images or videos, can lead to unbalanced partitions and degraded performance. Instead, for large binary content, store the data in an object store or content delivery network (CDN), and then store the URI in your database.

Evaluate indexes

By default, you can query tables using partition key columns, including the primary key and clustering columns. To query on non-partition key columns, use secondary (auxiliary) indexes:

  • Storage-Attached Indexing (SAI) (recommended)

  • Original secondary indexing (2i)

  • SSTable-Attached Secondary Indexing (SASI)

  • Materialized views (enables non-primary key queries but isn’t a literal index)

For more information about index types, use cases, and CQL commands for creating and querying indexes, see Index types and use cases and Prepare to use materialized views.

Follow these best practices to ensure your indexes are performant and not creating unnecessary load on the cluster:

Indexing isn’t a replacement for an effective data model

Indexes can enable specific query patterns that aren’t otherwise possible, but they aren’t a replacement for best practices like one table per query. Each index has a maintenance and storage cost in the database, and poorly designed indexes lead to poor query performance.

Avoid superfluous indexes and audit unused indexes

Because the database needs resources to build and maintain secondary indexes, DataStax recommends limiting the number of indexes and removing unused indexes. Using your schema.cql file, use the following command to check how many indexes are in your cluster:

grep 'CREATE INDEX' schema.cql|wc -l
Storage-attached indexing (SAI) is recommended

SAI is recommended for most workloads but these indexes require careful planning and preparation of hardware capacity. For more information, see the following:

Original secondary indexing (2i, index lookups) isn’t recommended

2i isn’t recommended except for edge cases that aren’t addressed by other indexing strategies. This type of indexing is prone to performance degradation and only suitable for specific schemas and query patterns. For more information, see the following:

Don’t use SSTable-attached Secondary Indexes (SASI)

SASI is deprecated in Cassandra 5.0 and not recommended in earlier versions. If you are migrating or upgrading from an earlier version, remove SASI indexes from your schema and data model. Using your schema.cql file, use the following command to check for SASI indexes in your cluster:

grep 'CREATE CUSTOM INDEX.*SASIIndex' schema.cql|wc -l
Understand performance implications and limitations of materialized views

Materialized views (MVs) aren’t a literal index type, but they can be used to enable queries that aren’t possible with a table’s primary key. For information about how MVs work, known limiations, and performance impacts, see Prepare to use materialized views.

MVs are considered experimental and not recommended for production use.

They can be useful for data modeling because you can write data to one table, and then use MVs to test different primary keys, query patterns, and slices of data from that one table.

MVs are usually less performant than duplicate writes to multiple tables that have different primary keys. Although these duplicate writes add complexity to your application logic, this approach has advantages over MVs:

  • Complete flexibility in defining the primary key for each table, whereas MVs must respect the base table’s primary key.

  • Avoids performance impacts, such as extra read-before-write cycles when iterating writes to MVs.

  • Avoids repair issues known to occur with materialized views.

Both tables and MVs consume storage space in the cluster because MV data is stored in each view, similar to an index or table. Each MV increases the total size of the data stored. This can be significant when there are many MVs or the base table is large. Depending on your workloads, separate tables or secondary indexes can be more efficient to store.

If you choose to use MVs, plan your data model to minimize the number of MVs that you create. Using your schema.cql file, use the following command to check the number of MVs in your cluster:

grep 'CREATE MATERIALIZED VIEW' schema.cql|wc -l

Evaluate query patterns

DataStax recommends that you design your application to use idempotent operations whenever possible. Minimize non-idempotent and expensive read-before-write operations, such as lightweight transactions (LWTs) and certain collection mutations, because they can degrade performance and throughput.

In some cases, these operations are unavoidable. If you must use non-idempotent and read-before-write operations, follow industry best practices for these patterns. For example, make sure your applications have appropriate retry logic and your clusters have adequate resources to handle the additional load.

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