A clustered columnar index (CCI) stores data in column-oriented format on a column store read-only instance, enabling fast analytical processing (AP) queries on large datasets. This document covers when to use CCIs and how to configure partitions, sort keys, and dictionary encoding for optimal performance.
CCIs are asynchronously built using Change Data Capture (CDC) nodes to maintain consistent query results. Data synchronization from the primary instance to column store read-only instances introduces second-level latency. Avoid using CCIs in scenarios that require real-time data access.
Use cases
Use CCIs in the following scenarios:
Mixed workloads or complex query acceleration: Your application runs complex AP queries—such as aggregations and joins on large tables—and row-based storage no longer meets performance requirements. CCIs significantly improve query performance in these situations.
Cold data archiving: Your data grows over time and storage costs are a concern. Column store read-only nodes access data through CCI metadata stored in Object Storage Service (OSS), reducing storage costs for cold data. For details, see TTL.
Historical snapshots: You need to retain and query historical data over time without expiration. A CCI acts as a non-expiring replica of historical data, making it well-suited for audit and backup workflows.
ETL pipelines: You need a data replica of the primary instance to run extract, transform, load (ETL) jobs without impacting production. Connect to a column store read-only instance to run ETL and transfer data to downstream systems.
Partitioned tables
Partitioning methods
Partitioning divides large tables into smaller, independently managed segments, improving query performance and data management for large datasets. The supported syntax for partitioning CCIs is:
PARTITION BY
HASH({column_name | partition_func(column_name)})
| KEY(column_list)
| RANGE({column_name | partition_func(column_name)})
| RANGE COLUMNS(column_list)
| LIST({column_name | partition_func(column_name)})
| LIST COLUMNS(column_list)The available partitioning types are:
RANGE: Divides data based on value ranges of a column. For example, partition a sales history table by year so that all records from the same year share a partition.
LIST: Divides data based on whether column values fall within a predefined set. This is common in SaaS scenarios—for example, partitioning a user table by country.
HASH / KEY: Divides data based on the hash value of a column, distributing rows evenly across partitions. Use this when data distribution is unpredictable or when even distribution is required.
For a complete reference, see Partitioning types.
Choose a partitioning method
CCIs are designed primarily to accelerate AP queries involving aggregations and joins. To take full advantage of columnar parallel scanning, use hash partitioning in most cases.
Follow these guidelines for specific scenarios:
| Scenario | Recommended method |
|---|---|
| AP queries (aggregations, joins) | Hash partitioning |
| Cold data archiving based on time | Range partitioning on a time column |
| Time-related query filters | Add a time column as a subpartition key |
| Data queryable by a predefined value set | List partitioning (verify row-oriented storage doesn't suffice first) |
For range- or list-partitioned tables: if row-oriented storage meets your query requirements, prefer it over columnar partitioning.
For time-based subpartitions, see Subpartitioning. Avoid creating subpartitions unless queries heavily rely on time-range conditions.
Configure the number of partitions
Calculate the number of partitions using this formula:
Number of computing nodes × Number of cores per compute nodeThe default is 16 partitions—do not use this default for production workloads. You can set the partition count higher than the formula result to accommodate future data growth.
For queries that join multiple tables, keep the partition count consistent across all involved tables within the same instance.
Select a partition key
The partition key determines how data is distributed across partitions. A poor partition key leads to data skew, which concentrates load on a single partition and degrades performance.
Choose a column with evenly distributed values, such as:
Transaction ID
Device ID
User ID
Auto-increment column
Avoid time-based columns (date, timestamp) as the partition key. Most writes tend to concentrate in the same time period, causing data skew. Time-range queries also end up hitting a single partition rather than distributing load across nodes. If time filtering is important for your queries, use the time column as a subpartition key instead.
Choose a column frequently used in `JOIN` or `GROUP BY` to minimize data redistribution overhead. For example, if you analyze historical orders by customer, use the customer ID column.
Choose a column frequently used in equality or non-range conditions to enable partition pruning.
Each table has one partition key, which can consist of multiple fields. Fewer fields in the partition key generally improve adaptability in complex query scenarios.
If no partition key is specified when you create a table, the system uses the primary key. If no explicit primary key exists, the system uses the implicit primary key.
After creating a CCI, run
check columnar partition db_name.tbl_nameto inspect data distribution across partitions. Review the output to confirm the partition key is appropriate and to identify potential data skew.
Sort key
How it works
The sort key controls how data is ordered within the CCI file. Each column data block stores the minimum and maximum values of the data it contains. When a query runs, the Pruner component evaluates these min/max values against the query conditions and classifies each block as:
Relevant: must be scanned
Possibly relevant: scanned as a candidate
Irrelevant: skipped entirely
A well-chosen sort key increases the proportion of blocks Pruner can skip, directly reducing scan volume and improving query speed.
For details on Pruner, see Configure a filter algorithm for IMCI-based queries.
Select a sort key
| Query pattern | Sort key recommendation |
|---|---|
| Queries that filter on a value range of a specific column | Use that column as the sort key |
Paginated queries with ORDER BY | Use the ORDER BY column as the sort key |
| All other cases | Use the partition key as the sort key |
Dictionary encoding
How it works
Dictionary encoding converts string values into integers, which speeds up GROUP BY and FILTER operations, improves compression ratios, and reduces storage costs.
Specify columns for dictionary encoding when creating a CCI:
-- Specify columns for dictionary encoding.
DICTIONARY_COLUMNS='col1,col2';
-- Create a CCI with dictionary encoding.
CREATE CLUSTERED COLUMNAR INDEX `cc_i_seller` ON t_order (`seller_id`)
PARTITION BY HASH(`order_id`) PARTITIONS 16
DICTIONARY_COLUMNS='order_id,seller_id';In a distributed database, dictionary-based queries require parsing and merging the dictionary across nodes, which adds overhead. Dictionary-based filtering is disabled by default during query execution. To enable it, set ENABLE_COLUMNAR_SLICE_DICT to TRUE.
Select columns for dictionary encoding
Encode columns with low cardinality—columns with a small number of distinct values relative to total row count, such as gender or region.
Avoid encoding all character columns. Columns with high cardinality offer minimal compression benefit and add encoding and decoding overhead.
FAQ
Does changing cluster specifications affect the number of partitions?
No. The partition count is set at CCI creation and is not affected by changes to cluster specifications.
Can I change the partition key, sort key, number of partitions, or dictionary-encoded columns after creating a CCI?
No. Drop the CCI and recreate it with the new configuration.
Do I need a column store read-only instance to create CCIs?
No. CCIs can be created on the primary instance. To query CCI data, we recommend that you purchase a column store read-only instance.