<@U0A71G31CDV> I currently have primary key table ...
# questions-and-troubleshooting
m
@Rocky I currently have primary key table in shared nothing cluster with EBS volumes as persistent store. My data TTL is 24 hours. Size is approximately 10TB. THere are also approximately 550 columns, subject to more additions in the future. Data throughput is approximately 60k records per second. Partial updates are approximately 20k per second. Most unique keys will only ever have 1 partial update applied to it. Some may have 2 partial updates (as I have a delay merge window for multiple updates to the record). Currently, I need a cluster of 12 r6in.8xlarge BE nodes to support this throughput. How feasible would it be to use an aggregate table instead? For the updated columns, define take either the max or latest value inserted. I understand the compaction is lazy and occurs in the background. However, realistically how much would this impact query performance given that I could save on resources since this will be append-only operations
r
Switching from a Primary Key (PK) table to an Aggregate table for this specific workload is a significant architectural trade-off. While it will reduce write-time resource consumption (CPU/Memory for index lookups), it shifts that burden to query time. Here is an analysis of how feasible this is and the likely impact on your performance: 1. Feasibility of the Aggregate Table Using an Aggregate table with
REPLACE_IF_NOT_NULL
(for latest value) or
MAX
for partial updates is technically feasible. In this model: * Ingestion: StarRocks performs simple appends. No primary key index is checked, and no delete vectors are generated. This is significantly cheaper for the BE nodes. * Partial Updates: Every update is a new row. If a record has 1 original insert and 2 partial updates, StarRocks stores 3 rows until compaction merges them. 2. Query Performance Impact (Merge-on-Read) The "lazy compaction" you mentioned is the critical factor. Aggregate tables use a Merge-on-Read strategy. * CPU Overhead: Because you have 550 columns, the merging process at query time will be extremely expensive. StarRocks must compare the versions for every single column to determine the "latest" or "max" value. * Predicate Pushdown: In PK tables, predicates can be pushed down to the storage layer very effectively. In Aggregate tables, many optimizations (like index-based filtering) are restricted because the "correct" value of a row isn't known until the merge is completed. * Version Count: With 20k partial updates/sec and a 24h TTL, you are generating 1.7 billion update rows per day. If background compaction falls behind, your queries might have to merge 2–5 versions for many keys. * Performance Penalty: Official documentation and benchmarks typically show that Primary Key tables are 3x to 10x faster for queries than Merge-on-Read models (like Aggregate/Unique tables) because they avoid this online merging. 3. Resource Savings vs. Bottlenecks * Memory: You will save significant memory. PK tables store indexes (either in RAM or on disk with a RAM cache). For 10TB of data and high throughput, the PK index is likely a major consumer of your
r6in.8xlarge
instances' 256GB RAM. * IO/CPU: While ingestion CPU drops, your Query CPU will spike. If your 12-node cluster is currently used for complex analytical queries, switching to an Aggregate table might actually increase the cluster size needed to maintain the same query SLAs. 4. Recommendation Moving to an Aggregate table is only recommended if your query pattern is very simple (e.g., simple point lookups or very narrow scans) and you are currently bottlenecked strictly by the memory overhead of the PK index. Alternative Optimization for your PK Table: Instead of switching to Aggregate, you can likely optimize your current 12-node cluster usage by tuning the Primary Key table: 1. Persistent Index: Ensure
enable_persistent_index
is
true
. Since you are on EBS, use
LOCAL
persistent index. This moves the index from memory to disk, drastically reducing RAM pressure. 2. Column-Mode Partial Updates: For 550 columns where you only update a few, use
partial_update_mode = 'column'
. This reduces the IO weight of updates. 3. Compaction Tuning: If you are seeing "too many versions" errors, increase
update_compaction_num_threads_per_disk
and
update_compaction_per_tablet_min_interval_seconds
. Summary: For a 550-column table, the query-time merge cost of an Aggregate table is usually prohibitive. I recommend sticking with the Primary Key table but optimizing the index and compaction settings to lower the resource footprint. References • best_practices/primarykey_table.md • table_design/table_types/primary_key_table.md
m
@Rocky Based on historical use cases, is StarRocks shared data mode really suitable to support such high throughput as I described? Within reasonable cost.
r
StarRocks shared-data mode (cloud-native architecture) is increasingly suitable for high-throughput workloads like yours, particularly starting with version 3.2.x. However, the feasibility depends on how you handle the transaction commit overhead and the local data cache. Here is an analysis based on historical performance and architectural design: 1. Throughput Feasibility (80k ops/s) Your combined load of 60k inserts + 20k partial updates per second is high but achievable in shared-data mode if you follow these principles: * Batching is Mandatory: StarRocks shared-data mode serializes transaction commits in the FE. To handle 80k records/s, you must batch your stream loads (e.g., 5-10 second intervals or 100MB+ batches). High-frequency "tiny" commits will bottleneck the metadata service. * Batch Publish Version: In version 3.2.3+, StarRocks introduced
lake_enable_batch_publish_version
. This optimization drastically reduces the FE overhead for high-concurrency ingestion, allowing shared-data clusters to reach throughput levels comparable to shared-nothing clusters. * Column-Mode Partial Updates: With 550 columns, you should use Column-mode partial updates (supported in shared-data from v3.3.1). This avoids the "read-on-write" overhead of row mode, which would be extremely heavy for such a wide table. 2. Cost Analysis: Shared-Nothing vs. Shared-Data Your current 12-node
r6in.8xlarge
cluster is likely expensive due to the high RAM (256GB/node) and EBS volume costs. * Storage Savings: Switching to S3/Object Storage will significantly reduce your storage bill for 10TB of data compared to EBS. * Compute Efficiency: Shared-data mode allows you to use Compute Nodes (CN) which are stateless. You can scale these up or down based on query demand without the 24-hour data rebalancing wait required by shared-nothing. * RAM Management: Because Primary Key indexes can be persistent in shared-data mode (storing the index on the CN's local NVMe/SSD instead of RAM), you may no longer need the high-memory
r6in
instances. You could potentially move to compute-optimized instances (
c6id
or
m6id
) with fast local disks for the cache and index. 3. The Query Performance "Catch" The performance of shared-data mode relies heavily on the Data Cache. * Cache Size: For 10TB of data with a 24h TTL, you need enough local disk space across your CN nodes to cache the "hot" data. If your queries frequently miss the cache and hit S3, performance will drop significantly. * Compaction: In shared-data mode, compaction is performed by CN nodes. If you have 20k updates/sec, the CNs must be powerful enough to merge these versions in the background. If compaction falls behind, query performance will degrade. Summary Comparison Metric | Shared-Nothing (Current) | Shared-Data (Storage-Compute Sep) Ingestion | High (Direct to local disk) | High (with
batch_publish
optimization) Partial Updates | Efficient | Efficient (using Column Mode v3.3.1+) Scaling | Slow (requires data reshuffling) | Fast (stateless CN scaling) Cost | High (EBS + High RAM) | Lower (S3 + Local SSD for cache) Reliability | Tablet-based replicas | S3 durability + FE-managed metadata Verdict: It is feasible, but do not use an Aggregate table. Stick with a Primary Key table in shared-data mode (v3.3.x recommended). The Primary Key table's Delete+Insert strategy is far more efficient for 550-column queries than the Merge-on-Read required by Aggregate tables. To succeed, ensure your CN nodes have large, fast local NVMe/SSD disks for the
star_cache
and
persistent_index
, and use StarRocks v3.3+ to leverage column-mode partial updates. References • faq/shared_data_faq.md • knowledge/usage/load/shared_data_batch_ingestion.md
m
@Rocky I've validated (on shared nothing at least) that column mode is infeasible. I did a simple test to confirm that it is incompatible with merge commit with is an essential feature on shared nothing cluster. Furthermore, the column partial update seconds metric on grafana was climbing so high (40-50s) that compaction was unable to run, causing update compaction scores to climb to >2000. Is there any know issues with column update mode? I do have around 25 different columns that I update, so that might influence it's performance? Is anything different on shared data
r
The issues you've encountered with *Column-mode partial updates*—specifically the incompatibility with Merge Commit and the massive spike in "Apply" latency—are consistent with the architectural limits of that feature at your scale. 1. Why Column Mode is failing at 25 columns While 25 columns is only ~4.5% of your 550 total, it is too many for the "Column-mode" architecture at a throughput of 20k updates/sec. * IO Amplification: In Column-mode, StarRocks creates Delta Column Group (DCG) files for the updated columns. For every batch committed, the BE must write 25 separate column files. At 20k ops/s, even with batching, the metadata overhead and small-file IO required to track and write these 25 columns during the
APPLY
phase becomes a bottleneck. * Apply Latency: The 40–50s "column partial update seconds" indicates the BE is overwhelmed by these small IO operations. Because the Primary Key table must complete the "Apply" phase (mapping keys to row IDs and writing DCGs) before a version becomes visible, a 50s apply time effectively stalls the pipeline. * Compaction Staleness: Compaction cannot merge rowsets that are still in the "Apply" state. This is why your compaction scores climbed to >2000; the system was generating new versions faster than it could finalize and compact the old ones. 2. "Merge Commit" Incompatibility "Merge Commit" (in the Flink/Kafka connectors) is designed to group many small HTTP stream loads into a single transaction. * Conflict: There are known issues in versions prior to 4.0 where features like Conditional Updates (often used with partial updates) or specific Column-mode metadata structures do not interact correctly with the "Merge Commit" transaction coordinator. * Validation: Your finding is correct; "Merge Commit" and Column-mode partial updates often encounter "invalid rssid" or transaction-state errors because the way Column-mode manages column-level versioning is more complex than standard Row-mode. 3. Is Shared-Data any different? Shared-data mode (v3.3+) supports Column-mode partial updates, but it is not a magic fix for your current issue: * Remote IO: In shared-data, those 25 DCG files have to be written to S3. Without a very aggressive local cache, this often increases the Apply latency due to network RTT. * Optimizations: Newer versions (v3.4.0+, v4.0) have introduced Parallelized Partial-Update Publish, which can parallelize the writing of these DCG files. However, 25 columns still remains on the high end for this feature. 4. Re-evaluating the Aggregate Table (The "Append-Only" Route) Given that your Primary Key table is failing under the write load, the Aggregate Table becomes a much more feasible "plan B," but with a specific configuration: * When it works: Since you only have 1 or 2 updates per key, the query-time merge cost is significantly lower than a table with hundreds of updates per key. * Configuration: Use the
REPLACE_IF_NOT_NULL
aggregate function for your updated columns. * Query Performance: You will see a roughly 3x-5x drop in query performance compared to a healthy PK table because of the "Merge-on-Read" overhead for 550 columns. However, compared to a stalled PK table with a compaction score of 2000, the Aggregate table will likely be more stable and provide more predictable latencies. * Resource Savings: You will likely be able to reduce your cluster size or move to cheaper instances (less RAM needed), as you no longer need to maintain the massive Primary Key index. Recommendation 1. If Query Performance is Critical: Stick with the Primary Key table but switch back to Row-mode. To handle the throughput, you must use Persistent Index on local NVMe/SSD (available on
r6in
or
m6id
instances) to keep memory usage low and ingestion high.
2. If Cost/Throughput is the Priority: Move to an Aggregate Table. Use
REPLACE_IF_NOT_NULL
. The "Merge-on-Read" cost is high for 550 columns, but since your "updates per key" is extremely low (max 2), the merge-on-read overhead is minimized. 3. Shared-Data: Only switch to shared-data if you want to lower storage costs (S3 vs EBS) and can leverage v4.0 which has the most advanced ingestion parallelization. Avoid Column-mode in shared-data for now if you are updating 25 columns. References • loading/Load_to_Primary_Key_tables.md • sql-reference/sql-statements/table_bucket_part_index/UPDATE.md • table_design/table_types/table_capabilities.md • loading/SQL_transaction.md • faq/Others.md • loading/loading_introduction/feature-support-loading-and-unloading.md
m
@Rocky There are 65655 unique campaigns ids. Still use bitmap or bloom filters?
r
With ~65,000 unique campaign IDs in a dataset of your scale (billions of rows per day), you are in a "medium-high" cardinality range. For this specific use case, a Bloom Filter is generally the superior choice, but there are nuances. 1. Bloom Filter (Recommended for Campaign IDs) For high-cardinality ID columns that are frequently used in point lookups (
=
) or
IN
clauses, the Bloom Filter is the standard choice in StarRocks. * How it helps: It allows StarRocks to skip entire data blocks (Pages) if the ID is not present, drastically reducing I/O. * Pros: Low storage overhead compared to Bitmap indexes. It has minimal impact on ingestion speed, which is critical for your 60k–80k ops/sec throughput. * Cons: It does not support range queries (
>
,
<
) or
IS NULL
filters. It only works for equality. 2. Bitmap Index (The "Combination" Choice) Contrary to legacy systems, StarRocks bitmap indexes are actually designed to handle high cardinality well. However, they come with a different trade-off. * When to use: Use a Bitmap index if your queries often combine the
campaign_id
with other low-to-medium cardinality columns (e.g.,
WHERE campaign_id = 123 AND status = 'active' AND region = 'US'
). StarRocks can perform bitwise operations across multiple bitmap indexes very quickly before reading any data. * Pros: Extremely fast for complex multi-column filtering. * Cons: Higher storage cost. For 65k unique values, the bitmap index will be significantly larger than a Bloom filter and will add measurable CPU/IO overhead during your high-speed ingestion (load phase). 3. Comparison for 65,535 Unique Values Feature | Bloom Filter | Bitmap Index Storage Cost | Very Low | Moderate to High Ingestion Impact | Minimal | Noticeable at 80k/s Point Queries (
=
)
| Excellent | Excellent Multi-column AND/OR | Limited | Exceptional Range Queries | Not Supported | Not Supported Recommendation * Go with Bloom Filter if your primary goal is to accelerate simple filters like
WHERE campaign_id = ?
while maintaining your high ingestion throughput. * Go with Bitmap Index only if you have complex "slice and dice" queries where you filter on many different columns simultaneously and the Bloom filter isn't providing enough speedup. Important Note for Primary Key Tables: If
campaign_id
is part of your Primary Key or Sort Key (Ordering Keys), you do not need either. StarRocks already builds a prefix index for sorting keys that handles these lookups much more efficiently than a secondary Bloom/Bitmap index. Only add these indexes if the column is not at the beginning of your sort key. References • table_design/indexes/Bitmap_index.md • knowledge/usage/best-practice.md
m
@Rocky So bitmap index is suitable since i am using it with IN (x, x, x) predicate with other filters?
r
Yes, a Bitmap Index is technically suitable for
IN (x, x, x)
predicates and is especially powerful when combined with other filters. However, given your extremely high ingestion rate (80k ops/s), there are critical performance trade-offs you must consider. 1. The "Combination" Advantage The main reason to choose a Bitmap index over a Bloom filter for your 65,535 campaign IDs is multi-column filtering. * If your query looks like:
WHERE campaign_id IN (1, 2, 3) AND status = 'active' AND country = 'US'
* StarRocks can perform a fast bitwise
AND
operation on the indexes of all three columns to identify the matching rows before reading any actual data from the 550-column table. This saves a massive amount of I/O. 2. The Ingestion Penalty (The Risk for Your Case) This is the biggest concern for your 80k ops/s throughput: * Load Overhead: Unlike Bloom filters (which are very lightweight to generate), Bitmaps require more CPU and memory to construct during the data ingestion and compaction phases. * Compaction Pressure: You already mentioned having issues with compaction scores climbing to >2000. Adding a Bitmap index adds one more thing for the BE to process during compaction. For a 10TB dataset, this could further slow down the
Apply
phase of your Primary Key table. 3. Check your Sort Key First Before adding a Bitmap index, check your table's
ORDER BY
(Sort Key) columns: * If
campaign_id
is the first column in your
ORDER BY
clause, you do not need a Bitmap or Bloom filter. StarRocks already builds a "Prefix Index" and maintains min/max values for every data block, which is faster and has zero extra overhead. * If
campaign_id
is not in the sort key, then an index is helpful. Comparison for
IN
Predicates at 80k ops/s
Feature | Bloom Filter | Bitmap Index
IN
Performance
| Excellent for skipping blocks. | Excellent for identifying rows. Multi-filter performance | Filters columns independently. | Combines filters via bitwise logic. Ingestion Impact | Very Low. | Moderate to High. Storage Overhead | Fixed (small). | Variable (grows with cardinality). Final Recommendation 1. Try Bloom Filter first: Given your 80k/s throughput and existing compaction struggles, start with a Bloom Filter. It is much "safer" for high-ingestion environments and still provides massive speedups for
IN
predicates by allowing the engine to skip data blocks that don't contain those IDs. 2. Only move to Bitmap if: You find that queries with multiple filters are still slow AND you have successfully stabilized your compaction/ingestion (by using Row-mode and Persistent Index). 3. Use
m6id
or
c6id
instances:
If you go with Bitmaps, the local NVMe disks on these AWS instances will help handle the extra I/O required for index maintenance during compaction.
m
@Rocky But doesn't ORDER KEY have to be part of PRIMARY KEY as well? So that means I need to recreate the table
r
Since StarRocks version 3.0, the Sort Key (
ORDER BY
)
and the Primary Key have been decoupled. This means you have significantly more flexibility than you might think: 1. No, the Sort Key does not have to be part of the Primary Key You can define a table where your Primary Key is one set of columns (for uniqueness/updates) and your Sort Key is a completely different set of columns (for query performance). Example:
Copy code
sql
CREATE TABLE my_table (
    id BIGINT NOT NULL,
    campaign_id INT NOT NULL,
    timestamp DATETIME,
    ...
)
PRIMARY KEY (id)                 -- Used for uniqueness and indexing updates
ORDER BY (campaign_id, timestamp) -- Used for physical storage order and Prefix Index
DISTRIBUTED BY HASH(id);
2. You don't necessarily need to recreate the table If you already have a Primary Key table and want to change the sorting order to include
campaign_id
at the front (to get the "Prefix Index" benefit), you can use:
Copy code
sql
ALTER TABLE my_table ORDER BY (campaign_id, timestamp);
* How it works: This is an asynchronous operation. StarRocks will gradually re-sort your data in the background. * New Data: Any new data arriving at 80k ops/s will be sorted by the new key immediately. * Old Data: Existing data will be re-sorted during background compaction. * Warning: On a 10TB dataset, this "Background Re-sort" will consume significant IO and CPU. Monitor your "Compaction Score" and "BE CPU" closely if you trigger this. 3. When the Prefix Index "Wins" The Prefix Index (created automatically from your
ORDER BY
columns) is the most powerful index in StarRocks. * If
campaign_id
is the first column in your
ORDER BY
clause, StarRocks can jump directly to the relevant data blocks for that campaign. * This is even faster than a Bloom Filter or Bitmap Index because it avoids the overhead of loading and checking a secondary index file; it’s simply how the data is physically organized. Summary of your Options * If
campaign_id
is already in the
ORDER BY
(as the 1st column):
Do nothing. You already have the best possible index (Prefix Index). * If
campaign_id
is NOT in the `ORDER BY`:
* Low Impact: Add a Bloom Filter (No table recreation, no heavy re-sorting). * High Performance: Run
ALTER TABLE ... ORDER BY (campaign_id, ...)
to move it to the front of the sort key. This provides the best query speed but will put a temporary heavy load on your cluster as it re-sorts the 10TB of data. Recommendation: Given your current issues with high compaction scores, avoid a heavy
ALTER TABLE ... ORDER BY
right now.
Stick with the Bloom Filter as it provides ~80% of the benefit with <5% of the system overhead. Only once your ingestion is stable should you consider re-sorting the table physically. References • knowledge/usage/best-practice.md • table_design/table_types/primary_key_table.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • table_design/indexes/Prefix_index_sort_key.md References • knowledge/usage/best-practice.md • table_design/table_types/primary_key_table.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • table_design/indexes/Prefix_index_sort_key.md
m
@RockySo I must respecify the order key? If currently received, auction_id, the command is alter table requests ORDER BY (received, auction_id, campaign_id)
r
Yes, your proposed command is correct:
Copy code
sql
ALTER TABLE requests ORDER BY (received, auction_id, campaign_id);
Since the
ORDER BY
clause defines the entire physical sorting strategy for the table, you must re-specify all the columns in the order you want them. Important Technical Nuances for your Setup: 1. Is it enough to have it in the 3rd position? In StarRocks, the Prefix Index (the ultra-fast lookup index) only covers the first 36 bytes of your sort key. * If
received
is a
DATETIME
(8 bytes) and
auction_id
is a
BIGINT
(8 bytes), then
campaign_id
(4-8 bytes) will fit in the prefix index. * However, if
auction_id
is a long
VARCHAR
(like a UUID string),
campaign_id
might fall outside the 36-byte prefix. In that case, the sort key still helps (via block-level min/max pruning), but it won't be as fast as a direct prefix lookup. 2. The Impact of 80k ops/sec As mentioned earlier, running
ALTER TABLE ... ORDER BY
on a 10TB table is an asynchronous background task. * The Good News: It won't block your 80k/sec ingestion. New data will be written using the new sort order immediately. * The Risk: Your "Compaction Score" is already high (>2000). This command will force the system to perform "Sort-on-Compaction" for all your old data. This is resource-intensive. If your BE nodes are already struggling with CPU or Disk IO, this could push the cluster over the edge. Recommendation If your primary goal is to speed up queries like
WHERE campaign_id IN (...)
without crashing your already-stressed compaction process: 1. First, try adding a Bloom Filter:
Copy code
sql
    ALTER TABLE requests SET ("bloom_filter_columns" = "campaign_id");
This is "lighter" than a full re-sort. It will help skip blocks without requiring the system to physically move 10TB of data around. 2. Only use the
ORDER BY
change if:
* You have settled your compaction issues (using Row-mode and Persistent Index as discussed). _
campaign_id
is a very frequent filter in almost _all* your queries. Summary: You do not need to recreate the table, and you do not need to include the Sort Key in your Primary Key. But given your high ingestion rate, be very cautious about triggering a full table re-sort. References • table_design/indexes/Prefix_index_sort_key.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • table_design/table_types/primary_key_table.md • knowledge/usage/best-practice.md References • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • knowledge/usage/best-practice.md • table_design/table_types/primary_key_table.md • sql-reference/sql-statements/table_bucket_part_index/CREATE_TABLE.md
m
@Rocky Since received is time based and auction_id is largely based off unixnano timestamp (with some server IDs appended), how effective really are these as order keys. Can it just be campaign_id alone? I already prune partitions down to the hour so received only helps with intra-hour lookups?
r
You're absolutely right to question this. If you are already pruning partitions down to the hour, the utility of having
received
(the timestamp) as the first column in your sort key is greatly diminished. Here is a breakdown of how to optimize your sort key for your specific data and query patterns: 1. Does
received
still help?
Since you partition by hour, StarRocks already skips all data outside that hour at the metadata level (Partition Pruning). * If you include
received
first:
It only helps sort data within that one hour. Unless your queries frequently look for "the last 5 minutes of the hour," this is likely wasting the most valuable "real estate" in your Prefix Index (the first 36 bytes). * Conclusion: If your filters are almost always on
campaign_id
, moving
received
further down or removing it from the sort key is a smart move. 2. Is
auction_id
a good Sort Key?
Since your
auction_id
is essentially a high-resolution timestamp (unixnano), it has extremely high cardinality. * The Advantage: High cardinality columns are excellent for "Point Lookups" (finding one specific auction). * The Disadvantage: If you rarely filter by a single
auction_id
, having it at the front of your sort key is counter-productive. It prevents the engine from effectively grouping
campaign_id
values together, which is what you need for your
IN (x, x, x)
queries to be fast. 3. Can it be
campaign_id
alone?
Yes. If your most frequent and performance-critical filter is
campaign_id
, you can absolutely make it the first (or even only) column in your
ORDER BY
. However, the "Sweet Spot" for your sort key is usually a combination of 2–3 columns that cover 90% of your queries. For your ad-tech use case, this usually looks like:
Copy code
sql
ALTER TABLE requests ORDER BY (campaign_id, auction_id);
Why this order? 1. Prefix Index Power:
campaign_id
(INT) is small. Putting it first ensures it is always in the 36-byte Prefix Index. StarRocks will be able to skip straight to the blocks containing the IDs in your
IN
clause. 2. Block Pruning: Even if you have 65k campaigns, within a single hour-long partition, each campaign will likely be grouped together. This makes the "Min/Max" pruning extremely efficient. 3. Secondary Filtering: If a query does search for a specific
auction_id
, it will still be fast because the data is sorted by
auction_id
within each campaign's data range. 4. Remember the 36-Byte Rule StarRocks only takes the first 36 bytes of your sort key to build the "Short Key Index" (the one kept in memory). *
campaign_id
(INT): 4 bytes *
received
(DATETIME): 8 bytes *
auction_id
(VARCHAR/BIGINT): 8–32 bytes If you use
ORDER BY (received, auction_id, campaign_id)
, and
auction_id
is a long string,
campaign_id
might fall outside the 36-byte prefix index. This would mean your
IN (campaign_id)
filter wouldn't benefit from the fastest possible lookup. Final Recommendation Given your hourly partitioning and 80k/s ingestion: 1. Move
campaign_id
to the front:
ORDER BY (campaign_id, auction_id)
. 2. Skip
received
in the sort key:
Since partition pruning already handles the time element, the timestamp doesn't need to be in the sort key unless you do a lot of sub-hour range scans. 3. Be Careful with the ALTER: Changing the
ORDER BY
on a 10TB table will trigger a massive background re-sort. Since your compaction score is already high, do not run this command yet. Stabilize your ingestion first (Row-mode + Persistent Index), then trigger the re-sort during a period of lower traffic. References • table_design/indexes/Prefix_index_sort_key.md • best_practices/table_clustering.md • best_practices/partitioning.md
m
@Rocky auction_id are 32 bytes, always. Campaign IDs are 7 digits long. what to do
r
With
auction_id
at 32 bytes and
campaign_id
at 7 digits (which fits into a 4-byte
INT
), your current or proposed order of
(received, auction_id, campaign_id)
is highly inefficient for your
campaign_id
filters. Here is why and what you should do: 1. The 36-Byte "Wall" StarRocks builds a Prefix Index using the first 36 bytes of your
ORDER BY
columns. *
received
(DATETIME):
8 bytes *
auction_id
(32 bytes):
32 bytes * Total: 40 bytes. In this scenario,
campaign_id
is completely excluded from the Prefix Index.
Because it falls past the 36-byte limit, StarRocks cannot use its fastest indexing mechanism to find your campaigns. It would have to fall back to slower methods (like scanning the min/max zone maps of every data block). 2. Recommended Order:
(campaign_id, auction_id)
You should change your sort key to put the 4-byte
campaign_id
at the very front. *
campaign_id
(INT):
4 bytes *
auction_id
(32 bytes):
32 bytes * Total: 36 bytes exactly. Why this is perfect: * Prefix Index Coverage: Both columns now fit perfectly within the 36-byte index. * Campaign Queries: A query like
WHERE campaign_id IN (1234567, 7654321)
will now be near-instant, as the engine can jump directly to the relevant data blocks. * Auction Queries: Queries for a specific
auction_id
will still be very fast because the prefix index covers most of the ID. * Dropping `received`: Since you already partition by the hour,
received
is redundant in the sort key. Partition pruning already does the work of narrowing the search to the correct 1-hour window. Sorting by
received
inside that window provides almost no extra benefit for your workload. 3. Immediate Action Plan Because your cluster is currently under high pressure (80k ops/s and >2000 compaction score), do not run the
ALTER TABLE
command yet.
Changing the sort key is a massive background IO operation. Follow this sequence: 1. Stop the bleeding: Use the Bloom Filter first. It doesn't require re-sorting the data and will significantly speed up
campaign_id
lookups.
Copy code
sql
    ALTER TABLE requests SET ("bloom_filter_columns" = "campaign_id");
2. Optimize Storage: Ensure you have enabled Row-mode and Persistent Index (as previously discussed) to bring that compaction score down from 2000 to under 100. 3. The Final Fix: Once the cluster is healthy and the compaction score is low, perform the re-sort:
Copy code
sql
    ALTER TABLE requests ORDER BY (campaign_id, auction_id);
Summary By putting
campaign_id
first, you stay under the 36-byte limit, maximize the Prefix Index, and leverage your existing hourly partitioning without redundant sorting—resulting in the best possible query performance for your 10TB dataset. References • table_design/indexes/Prefix_index_sort_key.md • table_design/table_types/primary_key_table.md • best_practices/table_clustering.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md References • table_design/indexes/Prefix_index_sort_key.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • best_practices/table_clustering.md • table_design/table_types/primary_key_table.md • knowledge/usage/best-practice.md • best-practice)
m
@Rocky If I rarely lookup a request by it's auction id, then will excluding it from the order keys improve ingestion throughput
r
Yes, excluding
auction_id
from the
ORDER BY
clause will improve your ingestion throughput, though the magnitude of the improvement depends on where your current bottleneck lies. For your specific scenario (10TB data, 80k ops/sec, high compaction score), here is how removing that 32-byte column from the sort key helps: 1. Reduced CPU Overhead in
MemTable
Every time StarRocks receives a batch of data, it sits in a
MemTable
in memory. Before that data can be flushed to disk as a "Segment," the BE (Back-End) must sort those rows. * With `auction_id`: The BE must perform string comparisons on 32-byte values to determine the order. String comparisons are significantly more CPU-intensive than integer comparisons. * Without `auction_id`: Sorting by
campaign_id
(a 4-byte
INT
) is extremely fast. The BE spends less time on CPU-bound sorting and can flush data to disk more quickly. 2. Smaller Metadata & Memory Footprint The Prefix Index (Short Key Index) is stored in memory for every segment. * If you include
auction_id
, the prefix index entries are larger (up to the 36-byte limit). * If you exclude it, the prefix index entries for each 1024-row block are much smaller. With 10TB of data, these memory savings across thousands of tablets add up, leaving more RAM available for the *Primary Key Index*—which is likely your current bottleneck. 3. Better Compression StarRocks uses various encoding techniques (like RLE or Frame-of-Reference) that work best when identical or similar values are stored together. * By sorting only by
campaign_id
, you ensure that all rows for a single campaign are physically contiguous. * Because you aren't "breaking" that group to sort by a random-looking 32-byte
auction_id
, the storage engine can compress the other columns in those rows more effectively. Smaller files on disk mean less IO during ingestion and compaction. 4. Compaction Efficiency Your "Compaction Score" of 2000+ indicates that the system cannot keep up with merging small files into larger ones. * When the BE performs compaction, it has to merge sorted streams. Merging streams sorted by a single
INT
is computationally cheaper than merging streams sorted by
(INT, VARCHAR(32))
. * While the Primary Key index is the main cost of compaction in a PK table, reducing the sort complexity still provides a "breathing room" for your CPU. Important Caveat: The Primary Key If
auction_id
is part of your Primary Key (which it likely is, to ensure uniqueness), it will still exist in the Primary Key Index (the hash map in memory). * Removing it from the
ORDER BY
does not remove it from the Primary Key Index. * The memory pressure from the Primary Key Index will remain the same. _ However, by decoupling them, you are telling StarRocks: _"Use the auction_id to ensure the row is unique, but don't waste CPU cycles trying to keep the physical files perfectly sorted by it."* Recommendation Since you rarely look up by
auction_id
, your ideal table definition should be:
Copy code
sql
-- This keeps your queries fast and speeds up ingestion
ALTER TABLE requests ORDER BY (campaign_id);
Why only one column? Because your queries are
WHERE campaign_id IN (...)
, putting it first gives you 100% of the Prefix Index benefit. Adding anything else after it in the sort key only helps with "intra-campaign" sorting, which you've stated you don't need. References • best_practices/table_clustering.md • table_design/indexes/Prefix_index_sort_key.md • table_design/table_types/primary_key_table.md • knowledge/usage/best-practice.md References
m
@Rocky Are there any optimization for array string values? How does array_contains perform versus arrays_overlap
r
Optimizing array string values in StarRocks involves understanding how the storage engine handles semi-structured data. Here is the breakdown of optimizations and the performance comparison you requested. 1. Optimizations for
ARRAY<STRING>
StarRocks does not currently support a "GIN" (Inverted Index) specifically for
ARRAY
elements (though it does for
VARCHAR
full-text search in v3.3+). However, it uses other mechanisms to speed up array queries: * Zonemap Index (Automatic): StarRocks automatically maintains Min/Max statistics for every data block. For
ARRAY
columns, it stores the Min/Max of the elements within that block. _ _Effect:* If you query
WHERE array_contains(tags, 'promo')
, StarRocks can skip entire data blocks if
'promo'
is lexicographically smaller than the Min element or larger than the Max element in that block. * Bloom Filter Index: You can create a Bloom Filter on an array column. However, be aware that in many StarRocks versions, the Bloom Filter hashes the entire array as a single value rather than the individual elements. This makes it useful for
WHERE tags = ['a', 'b']
but not for
array_contains
. * Flat JSON (v3.3+): If your arrays are stored within JSON objects, enabling "Flat JSON" will automatically extract and store these fields in a columnar format, significantly improving scan speed. 2.
array_contains
vs.
arrays_overlap
Feature |
array_contains(arr, val)
|
arrays_overlap(arr1, arr2)
Logic | Checks if a single value exists in the array. | Checks if any element exists in both arrays. Performance | Higher. It is a 1-to-N comparison. | Lower. It is an N-to-M comparison (set intersection). Zonemap Pruning | Effective. It can easily check if
val
falls within the Min/Max range of a block. | Less Effective. Pruning only works if the Min/Max ranges of both arrays are completely disjoint. Performance Tip: If you are checking for a constant value, always use
array_contains
. Even if you have a list of values to check, multiple
OR array_contains(...)
or using
array_contains_any
(introduced in v3.1) is generally more performant than
arrays_overlap
because the execution engine can better optimize single-value lookups against the columnar array data. 3. Recommendations for Large Datasets If you are dealing with a massive number of tags/strings in arrays: 1. Low Cardinality: If the strings in your arrays have low cardinality (e.g., status codes, categories), StarRocks handles them very efficiently via dictionary encoding. 2. High Cardinality / Full-Text: If you find yourself searching for "keywords" within long strings inside arrays, consider if a
VARCHAR
with the new Full-text Inverted Index (v3.3+) is better. It uses the
MATCH
predicate and is significantly faster than any array function for keyword searches. 3. Data Modeling: If
array_contains
is still too slow, the most common optimization is to "normalize" the data into a separate mapping table (e.g.,
item_id
,
tag
) and use a standard Join or a Bitmap Index on the tag column. References • sql-reference/sql-functions/array-functions/arrays_overlap.md • sql-reference/sql-functions/array-functions/array_contains_all.md • table_design/indexes/inverted_index.md • table_design/indexes/Bitmap_index.md References • table_design/indexes/Bitmap_index.md • sql-reference/data-types/semi_structured/Array.md
m
@Rocky Does changing the order key to just campaign id affect general performance since the data will be re-arranged completely, hence even though I insert my data chronologically, it will be physically out of order? WIll querying data in past 30 minutes be 2x slower since no index within hourly partitions? Should i include received and campaign id as order key? How can I keep track or progress of changing the order jey
r
Changing the order key to just
campaign_id
is a strategic move that favors your most frequent query filter. Here is how it impacts your system and how to manage the transition. 1. Will it affect general performance? Yes, it will improve ingestion throughput. * Faster Ingestion: Sorting an
INT
(
campaign_id
) is significantly cheaper for the CPU than sorting a 32-byte
auction_id
. * Physical Re-ordering: You are correct that the data will be re-arranged. Instead of being stored in the order it arrived, it will be grouped by
campaign_id
on disk. This actually improves general performance for your campaign-based queries because it allows for much better data compression and sequential reads. 2. Will 30-minute queries be 2x slower? Unlikely, because you already have Partition Pruning. Since your table is partitioned by the hour, StarRocks uses the Partition Key to immediately narrow down the search to a specific 1-hour folder. * The "30-minute" query: If you query for a 30-minute window, the engine first opens the 1-hour partition. * Inside the Partition: Even if
received
isn't in the sort key, StarRocks still has Zone Maps (Min/Max values) for the
received
column on every data page. Because your ingestion is naturally chronological, these Zone Maps will remain highly effective at skipping irrelevant pages, even if the primary sort order is
campaign_id
. * Conclusion: You won't see a 2x slowdown because the partition key is doing 90% of the heavy lifting for time-based filtering. 3. Should you include
received
in the sort key?
Recommendation:
ORDER BY (campaign_id)
is likely best, but
(campaign_id, received)
is a safe middle ground.
* Option A:
ORDER BY (campaign_id)
_ _Pros:* Max ingestion speed; absolute fastest campaign lookups. _ Cons: Slightly less efficient time-skipping _inside* an hourly partition. * Option B:
ORDER BY (campaign_id, received)
_ _Pros:* Fits easily within the 36-byte prefix index limit (4 bytes for INT + 8 bytes for DATETIME = 12 bytes). It gives you the best of both worlds. _ _Cons:* Slightly more CPU work during ingestion than Option A (but still much faster than using
auction_id
). If your queries always include
campaign_id
, use Option A. If you sometimes query time ranges without a campaign filter, use Option B. 4. How to track the progress of the change The
ALTER TABLE ... ORDER BY
operation is asynchronous and runs in the background. You can track it using these commands: * Check overall progress:
Copy code
sql
    SHOW ALTER TABLE COLUMN;
Look at the
State
column. It will show
RUNNING
while it's working and
FINISHED
when done. The
Progress
column will show a percentage (e.g.,
10/100
). * Check specific tablet status (advanced):
Copy code
sql
    SHOW TABLET FROM requests WHERE State="ALTER";
Important Safety Note: Since your cluster currently has a high compaction score (>2000), wait until you have enabled the Persistent Index and the score has stabilized below 100 before running this
ALTER
. Re-sorting a 10TB table is a heavy I/O operation. References • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • table_design/indexes/Prefix_index_sort_key.md • best_practices/table_clustering.md • faq/Sql_faq.md References • best_practices/table_clustering.md • knowledge/usage/best-practice.md • table_design/table_types/primary_key_table.md
m
@Rocky MySQL [requests]> show alter table column; +----------+-----------+---------------------+---------------------+-----------+----------+---------------+----------------+---------------+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+---------+ | JobId | TableName | CreateTime | FinishTime | IndexName | IndexId | OriginIndexId | SchemaVersion | TransactionId | State | Msg | Progress | Timeout | +----------+-----------+---------------------+---------------------+-----------+----------+---------------+----------------+---------------+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+---------+ | 11722256 | requests | 2026-07-01 174322 | 2026-07-01 180007 | requests | 11722257 | 7829331 | 535:1605019969 | 6439062 | CANCELLED | Alter job conflicts with partition creation, for more details please check https://docs.starrocks.io/docs/faq/Others#how-can-i-prevent-expression-partition-conflicts-caused-by-concurrent-execution-of-loading-tasks-and-partition-creation-tasks | NULL | 86400 | | 11735429 | requests | 2026-07-01 202228 | NULL | requests | 11735430 | 7829331 | 535:134255784 | 6444285 | RUNNING | | 2/3200 | 86400 | +----------+-----------+---------------------+---------------------+-----------+----------+---------------+----------------+---------------+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+---------+ I previously ran a bloom filter index creation job that got cancelled because a new partition was created at the start of the hour. How can I solve this
r
The error occurred because Expression Partitioning (automatic partition creation) and
ALTER TABLE
jobs (like adding a Bloom Filter or changing the Sort Key) cannot happen at the same time. In StarRocks, both operations modify the table's metadata. When the top of the hour arrived (
18:00:07
), a loading task triggered the creation of a new hourly partition. Since loading tasks are given priority, StarRocks automatically cancelled your
ALTER
job to allow the new partition to be created. The Solution: Pre-create Partitions To prevent this conflict, you must manually trigger the creation of future partitions before you start the
ALTER
job. This ensures that no "partition creation" events happen while the job is running. 1. Cancel the current job (if still running) Since your current job is only at
2/3200
progress and will likely be cancelled at the next hour, it is better to stop it now.
Copy code
sql
CANCEL ALTER TABLE COLUMN FROM requests;
2. Pre-create partitions for the next 48 hours Use the following "dry run" insert trick. This triggers the metadata logic to create the partitions but uses
EXPLAIN ANALYZE
so that no actual data is written and no transaction is committed. Replace
received
with your actual partition column name:
Copy code
sql
-- This creates partitions for the next 48 hours
EXPLAIN ANALYZE
INSERT INTO requests (received)
SELECT DATE_ADD(NOW(), INTERVAL d hour)
FROM table(generate_series(1, 48)) AS g(d);
3. Verify the partitions exist Check that you see partitions for the upcoming hours:
Copy code
sql
SHOW PARTITIONS FROM requests;
4. Restart your Order Key change Now that the partitions for the next two days are pre-created, you can safely restart your operation:
Copy code
sql
ALTER TABLE requests ORDER BY (campaign_id);
Why is it so slow (2/3200)? Your progress indicates that your cluster is struggling to process the 10TB of data for this change. * Persistent Index: As mentioned before, ensure
persistent_index
is enabled for this Primary Key table. If the BEs are currently memory-starved or the CPU is pinned by compaction (score 2000+), this
ALTER
job will take days. * IO Priority: You can increase the priority of the alter job by setting:
Copy code
sql
    -- Execute this before running the ALTER command
    SET admin_replica_column_scan_threads = 8;
Summary of Strategy Because your table is 10TB and the progress is slow, pre-create at least 72 hours of partitions. If the
ALTER
job takes 3 days to finish and you only pre-created 24 hours, it will be cancelled again when it hits the 25th hour. References • faq/Others.md • table_design/data_distribution/expression_partitioning.md • sql-reference/sql-statements/table_bucket_part_index/SHOW_ALTER.md References • faq/Others.md • table_design/data_distribution/expression_partitioning.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • administration/management/Best_practice_primary_key_table.md table) References • table_design/data_distribution/expression_partitioning.md • faq/Others.md
m
@Rocky PRIMARY KEY(
received
,
auction_id
,
imp_key
) PARTITION BY date_trunc('hour', received) DISTRIBUTED BY HASH(
auction_id
) BUCKETS 128 ORDER BY(
received
,
auction_id
) PROPERTIES ( "compression" = "LZ4", "enable_persistent_index" = "true", "fast_schema_evolution" = "true", "partition_live_number" = "24", "replicated_storage" = "true", "replication_num" = "1" ); | This is my table property. Will pre-creating 48 hour of partitions cause existing partitions to be deleted since partition live number is 24?
r
Based on your table properties and the StarRocks documentation for Expression Partitioning, here is how the
partition_live_number
will behave and how you should proceed. 1. Will pre-creating future partitions delete your data? No. Your current data is safe. In StarRocks, the
partition_live_number
defines a "look-back" window relative to the current time, not a strict total count of partitions. The logic works as follows: _ It calculates a "cutoff" time:
Current Time - (partition_live_number _ hour)
. * Any partition whose range is older than this cutoff is deleted. * Partitions in the future (relative to the current time) are never considered "older" than the cutoff, so they are not deleted. Example: * Current time: July 1st, 12:00 PM. * `partition_live_number`: 24. * Cutoff: June 30th, 12:00 PM. * If you pre-create partitions for July 2nd and July 3rd, they are kept. The system will only delete partitions from before June 30th, 12:00 PM. 2. Recommended Safety Strategy Since your
ALTER TABLE
job is moving slowly and you have 10TB of data, you should handle this in two steps to be 100% safe and avoid another cancellation: Step A: Increase the TTL temporarily Increase the
partition_live_number
so that your historical data is guaranteed to stay during the long-running re-sort process.
Copy code
sql
ALTER TABLE requests SET ("partition_live_number" = "96");
Step B: Pre-create the partitions Now, trigger the creation of future partitions. This ensures that when the "top of the hour" arrives, StarRocks doesn't try to create a new partition (which would kill your
ALTER
job).
Copy code
sql
-- Create partitions for the next 48 hours
EXPLAIN ANALYZE
INSERT INTO requests (received)
SELECT DATE_ADD(NOW(), INTERVAL d hour)
FROM table(generate_series(1, 48)) AS g(d);
Step C: Restart the Order Key change
Copy code
sql
ALTER TABLE requests ORDER BY (campaign_id);
Why this works The error you saw (
Alter job conflicts with partition creation
) occurs specifically during the metadata modification phase when StarRocks tries to add a new partition to the catalog. By pre-creating the partitions, the load jobs at the start of the hour will see that the partition already exists and will simply write data into it without needing to modify the catalog, thus avoiding the conflict with your
ALTER
job. After the job finishes Once the
ALTER
state is
FINISHED
(check with
SHOW ALTER TABLE COLUMN
), you can set your TTL back to 24:
Copy code
sql
ALTER TABLE requests SET ("partition_live_number" = "24");
References • table_design/data_distribution/expression_partitioning.md • faq/Others.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md References • table_design/data_distribution/expression_partitioning.md • faq/Others.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • table_design/data_distribution/dynamic_partitioning.md • sql-reference/sql-statements/table_bucket_part_index/CREATE_TABLE.md • table_design/data_distribution/Data_distribution.md
m
@Rocky MySQL [requests]> EXPLAIN ANALYZE -> INSERT INTO requests.requests (received, auction_id, imp_key) -> SELECT DATE_ADD(NOW(), INTERVAL d hour), 'auction', 'imp' -> FROM table(generate_series(1, 10)) AS g(d); ERROR 5609 (22000): number of partition's index is not equal with schema's, num_part_indexes=1, num_schema_indexes=2, num_shadow_indexes=0 MySQL [requests]>
r
The error
5609 (number of partition's index is not equal with schema's)
confirms that you cannot pre-create partitions while an
ALTER TABLE
job is currently
RUNNING
. Why this is happening When you run
ALTER TABLE ... ORDER BY
, StarRocks creates a "shadow index" to hold the re-sorted data. At this moment, your table metadata says there are 2 indexes (Original + Shadow), but the existing partitions only have 1 index. When you try to run the
INSERT
(even with
EXPLAIN ANALYZE
), StarRocks detects this mismatch and blocks the operation to prevent data inconsistency. The Correct Sequence You must clear the "shadow" state before you can pre-create the partitions. Follow these steps exactly: 1. Cancel the current slow job Since your current job is only at 2/3200 and will eventually fail at the next hour anyway, stop it now:
Copy code
sql
CANCEL ALTER TABLE COLUMN FROM requests;
Wait 1 minute for the state to return to
FINISHED
or
CANCELLED
in
SHOW ALTER TABLE COLUMN;
.
2. Pre-create the partitions Now that the table is back to a "single index" state, the
EXPLAIN ANALYZE
trick will work:
Copy code
sql
-- This creates partitions for the next 72 hours to give you plenty of time
EXPLAIN ANALYZE
INSERT INTO requests.requests (received, auction_id, imp_key)
SELECT DATE_ADD(NOW(), INTERVAL d hour), 'dry_run_id', 'dry_run_key'
FROM table(generate_series(1, 72)) AS g(d);
3. Verify the partitions Ensure you see the future partitions listed (e.g., if it's 8 PM now, you should see partitions for 9 PM, 10 PM, and tomorrow):
Copy code
sql
SHOW PARTITIONS FROM requests;
4. Restart the Order Key change Now that the "future" is secured, restart the heavy lifting:
Copy code
sql
ALTER TABLE requests ORDER BY (campaign_id);
Pro-Tip: Making it finish faster Since you have 10TB of data and a high compaction score, the re-sorting will take a long time. You can give the
ALTER
job more resources by running this before Step 4:
Copy code
sql
-- Increase threads for the background scan/sort (default is usually 1 or 2)
SET admin_replica_column_scan_threads = 4;
Summary: You were stuck in a "Catch-22" where the running job prevented the fix for the job's failure. Cancelling the job first breaks that breaks the cycle. References • faq/Others.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • table_design/data_distribution/expression_partitioning.md • administration/management/Best_practice_primary_key_table.md breaks the cycle. References • best_practices/query_tuning/schema_tuning.md • knowledge/admin_and_operation/shared_data_compaction.md
m
@Rocky Do I need to pre-create partitions? Since changing order key is different operation from bloom filter index creation?
r
Yes, you still need to pre-create partitions. Even though changing the Order Key (Sort Key) is a different operation from adding a Bloom Filter, they both follow the same underlying "Schema Change" mechanism in StarRocks. Why the conflict persists: 1. Shadow Indexes: Both operations (Bloom Filter and
ORDER BY
) create a "shadow index" (a hidden version of your table) to rewrite and re-sort the data in the background. 2. Metadata Lockdown: When an
ALTER TABLE
job is running, the table’s metadata is in a "transition" state. 3. The Conflict: When your hourly loading task arrives, it realizes the partition for the next hour doesn't exist. It tries to create it. However, StarRocks cannot create a new partition while the table has "shadow indexes" because the new partition wouldn't know whether to follow the old schema/order or the new one. 4. Priority: StarRocks always prioritizes data loading. To allow the new partition to be created so the load doesn't fail, it automatically cancels your running
ALTER
job. The logic remains the same: Because your table uses
PARTITION BY date_trunc('hour', received)
, a new metadata event (partition creation) happens every 60 minutes. If your 10TB re-sort job takes longer than an hour, it will be killed every single time a new hour starts. To solve this for an
ORDER BY
change:
1. Cancel the current job. 2. Pre-create enough partitions (e.g., the next 72 hours) using the
EXPLAIN ANALYZE INSERT
trick so that the loading tasks don't trigger a "partition creation" event. 3. Restart the
ALTER TABLE ... ORDER BY
job. This ensures that for the next 3 days, no new partitions need to be created, giving your re-sort job a "clear window" to finish without being interrupted. References • faq/Others.md • table_design/table_types/primary_key_table.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md References • knowledge/usage/best-practice.md • table_design/table_types/primary_key_table.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • best_practices/query_tuning/schema_tuning.md • table_design/data_distribution/Data_distribution.md
m
@Rocky | 11744817 | requests | 2026-07-01 203442 | 2026-07-01 204200 | requests | 11744818 | 7829331 | 535:995479382 | 6444690 | CANCELLED | schema change task failed: task type: ALTER, status_code: RUNTIME_ERROR, alter failed,status: STARROCKS_ERROR,get next chunk failed, status:Memory limit exceeded: Memory of schema change task exceed limit. read and decompress page Backend: 172.30.75.179, Used: 2160542336, Limit: 2147483648. You can change the limit by modify BE config [memory_limitation_per_thread_for_schema_change] be/src/storage/rowset/page_io.cpp:311 CurrentThread::mem_tracker()->check_mem_limit("read and decompress page") be/src/storage/rowset/scalar_column_iterator.cpp:397 _reader->read_page(_opts, iter.page(), &handle, &page_body, &footer) be/src/storage/rowset/scalar_column_iterator.cpp:347 _read_data_page(_page_iter) be/src/storage/rowset/scalar_column_iterator.cpp:289 _load_next_page(&eos) be/src/storage/rowset/segment_iterator.cpp:435 _column_iterators[i]->next_batch(range, col.get()) be/src/storage/rowset/segment_iterator.cpp:1485 _context->read_columns(chunk, range) be/src/storage/rowset/segment_iterator.cpp:1629 _read(chunk, rowid, chunk_capacity - chunk_start), backendId: 95876, signature: 11745971 | NULL | 86400 | +----------+-----------+---------------------+---------------------+-----------+----------+---------------+----------------+---------------+-----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+---------+
r
The error confirms that the background re-sorting task for your 10TB table exceeded the default 2GB memory limit per thread. This is common when re-sorting large volumes of data or tables with many columns. Because
memory_limitation_per_thread_for_schema_change
is a mutable parameter, you can increase it on the fly without restarting your nodes. Step 1: Increase Memory Limit on all Backends (BEs) Run this command for every BE in your cluster (replace
<be_ip>
and the port, usually
8040
):
Copy code
bash
curl -X POST http://<be_ip>:8040/api/update_config?memory_limitation_per_thread_for_schema_change=8
*I recommend set it to 8 (8GB) or 16 depending on your available RAM. This gives the sorter more headroom to handle the large data chunks.* Step 2: Clear the "Shadow" State Before you can fix the partition issue, you must ensure the failed job is fully cleaned up: 1. Run
SHOW ALTER TABLE COLUMN;
and verify the state is
CANCELLED
. 2. (Optional but recommended) Run
CANCEL ALTER TABLE COLUMN FROM requests;
just to be certain metadata is unlocked. Step 3: Pre-create the Partitions (The "Dry Run" Insert) Now that the memory limit is raised and the table is unlocked, pre-create the partitions so the job isn't killed by the "next hour" event:
Copy code
sql
EXPLAIN ANALYZE
INSERT INTO requests.requests (received, auction_id, imp_key)
SELECT DATE_ADD(NOW(), INTERVAL d hour), 'dry_run', 'dry_run'
FROM table(generate_series(1, 72)) AS g(d);
Step 4: Restart the Order Key change
Copy code
sql
ALTER TABLE requests ORDER BY (campaign_id);
Why this happened: * Memory: The
ORDER BY
operation reads data into memory to sort it before writing new segments. With 10TB of data, the default 2GB buffer was too small for the specific data distribution or row size in your segments. * Cancellation: If you don't pre-create the partitions (Step 3), StarRocks will still cancel this job at the start of the next hour to allow the new partition to be created, regardless of how much memory you give it. Summary: Raise the memory to 8GB+, pre-create 72 hours of partitions, and then restart the
ALTER
. References • administration/management/BE_configuration.md • faq/Sql_faq.md • administration/management/monitoring/Monitor_and_Alert.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md References • administration/management/BE_configuration.md • administration/management/monitoring/metrics.md • faq/Sql_faq.md • administration/management/FE_configuration.md • administration/management/monitoring/Monitor_and_Alert.md
m
@Rocky memory_limitation_per_thread_for_schema_change How many threads are there for change. I set it to 8 GB and same error occur
r
If you set the limit to 8GB but the error message still shows
Limit: 2147483648
(2GB), the configuration change was not successfully applied or was not applied to the specific Backend (BE) that failed. 1. Verify the configuration change StarRocks BE configurations updated via
curl
take effect immediately but must be applied to every BE in the cluster. Check the current effective limit on the BE that failed (from your error:
172.30.75.179
):
Copy code
bash
curl <http://172.30.75.179:8040/api/show_config?conf=memory_limitation_per_thread_for_schema_change>
If it still says
2
, your previous
curl -X POST
command failed.
2. Why it might have failed * Wrong Port: Ensure
8040
is the correct
be_http_port
. * Multiple BEs: The schema change task is distributed. If you only updated one BE, the task might fail when it migrates or processes tablets on a different BE. Run the update on all backends. * Syntax: Use the integer
8
without "GB" suffix:
Copy code
bash
    curl -X POST "http://<be_ip>:8040/api/update_config?memory_limitation_per_thread_for_schema_change=8"
3. How many threads are involved? There are two relevant thread pool settings: *
alter_tablet_worker_count
(Default: 3):
This is the number of worker threads per BE dedicated to schema change tasks. Each of these threads can consume up to the
memory_limitation_per_thread_for_schema_change
limit. * Total Memory Impact: If you have 3 threads and a limit of 8GB, a single BE could use up to 24GB just for schema change. Ensure your BE has enough total RAM to support this. 4. If the error persists with
Limit: 8589934592
(8GB)
If the error returns and says
Used: 8.1GB, Limit: 8GB
, it means your data pages are exceptionally large or complex (common with many wide string columns or high compression). 1. Increase to 16GB: If you have the RAM, set the limit to
16
. 2. Adjust the Sorting Buffer: The actual buffer used for sorting is
memory_limitation_per_thread_for_schema_change * memory_ratio_for_sorting_schema_change
. * Default ratio is
0.8
. * You can try reducing this ratio (e.g., to
0.5
) to leave more "overhead" memory for the decompression phase that is currently failing:
Copy code
bash
        curl -X POST "http://<be_ip>:8040/api/update_config?memory_ratio_for_sorting_schema_change=0.5"
Summary Checklist: 1. Verify the failed BE's IP from the error message. 2. Run
show_config
via curl to confirm it says
8
. 3. If it says
2
, re-run the
update_config
POST command. 4. If the re-sort still fails, increase the limit further or reduce
memory_ratio_for_sorting_schema_change
. References • administration/management/BE_configuration.md • administration/management/monitoring/alert.md • faq/Sql_faq.md • administration/management/FE_configuration.md • administration/management/monitoring/Monitor_and_Alert.md References • administration/management/BE_configuration.md • administration/management/monitoring/metrics.md • faq/Sql_faq.md • administration/management/monitoring/Monitor_and_Alert.md • knowledge/usage/best-practice.md
m
@Rocky Is there a way to speed up this process? Maybe delete half the old partitions? More threads?
r
Yes, you can speed this up. For a 10TB table, relying on the background schema change process (which defaults to only 3 threads) can be very slow. Here are the three most effective ways to accelerate the re-sorting: 1. Delete Old Partitions (Highly Recommended) Since this is a re-sorting operation (
Sorted Schema Change
), StarRocks must read, sort, and rewrite every single row. * Impact: If you delete 50% of the old partitions, the job will finish in roughly half the time. * Action: If you don't need the historical data, drop the partitions before starting the
ALTER
job.
Copy code
sql
    ALTER TABLE requests DROP PARTITION p20240101; -- Repeat for old data
2. Increase Parallelism (Threads) By default, each Backend (BE) only processes 3 tablets at a time for schema changes. If you have many CPU cores and high I/O throughput, you can increase this: * Parameter:
alter_tablet_worker_count
* Action: Increase this to 6 or 10 (depending on your CPU).
Copy code
bash
    curl -X POST "http://<be_ip>:8040/api/update_config?alter_tablet_worker_count=10"
Note: Increasing threads increases memory usage. Since you already hit memory limits at 2GB/8GB, ensure your BE has enough total RAM (Threads × Memory Limit per Thread). 3. The "Shadow Table" Strategy (Fastest for 10TB) For massive tables, the background
ALTER TABLE
can be slower than a manual migration because the background process is throttled to avoid impacting user queries. A manual migration uses the full power of the query engine. 1. Create a new table with the exact same schema but the new
ORDER BY
key. 2. Migrate data in chunks (e.g., day by day) using `INSERT INTO ... SELECT`:
Copy code
sql
    INSERT INTO requests_new
    SELECT * FROM requests
    WHERE received >= '2024-01-01 00:00:00' AND received < '2024-01-02 00:00:00';
3. Swap the tables once finished:
Copy code
sql
    ALTER TABLE requests SWAP WITH requests_new;
Why this is faster: * It uses the SQL execution engine (fully parallelized across all cores and nodes). * You can monitor progress much more easily. * It doesn't "lock" the metadata in a way that blocks partition creation. Important Note on Memory The error you saw (
Limit: 2147483648
) means the 8GB change you tried earlier did not take effect. * Check if you have a
be.conf
file that is overriding the
curl
command on restart. * Ensure you ran the
curl
command on the specific BE mentioned in the error (
172.30.75.179
). * Verify the current limit with:
curl <http://172.30.75.179:8040/api/show_config?conf=memory_limitation_per_thread_for_schema_change>
. Recommendation: If you can afford to delete old data, do that first. Then, either use the
SWAP
strategy for maximum speed or increase
alter_tablet_worker_count
to 6-8 and restart the
ALTER
job. References • administration/management/BE_configuration.md • sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md • best_practices/query_tuning/schema_tuning.md • administration/management/monitoring/alert.md • table_design/data_distribution/Temporary_partition.md References • administration/management/BE_configuration.md • best_practices/query_tuning/schema_tuning.md • faq/operation_maintenance_faq.md
m
@Rocky I have another use case of an array that is purely integer values, all of them only 5 digits long. Right now we are using array lookups. Very inefficient. I think there is a much easier way to optimize this since they are all numbers?
r
Since your array is strictly 5-digit integers, you can achieve a massive performance boost and reduce storage costs by switching from
ARRAY<INT>
to the
BITMAP
data type (which uses the Roaring Bitmap implementation). Why
BITMAP
is the best fit:
1. Storage Efficiency: For 5-digit integers (range 0–99,999), a Roaring Bitmap is extremely compact. It stores these as bitsets rather than an array of 4-byte or 8-byte integers. 2. Lookup Speed:
ARRAY_CONTAINS
must scan the entire array linearly.
BITMAP_CONTAINS
is a constant-time bitwise operation. 3. Set Operations: If you ever need to find the intersection or union of these arrays across rows (e.g., "users who have both ID 12345 and 67890"), Bitmaps do this using CPU-friendly bitwise logic. ────────── How to Implement It 1. Table Schema Instead of
ARRAY<INT>
, define the column as `BITMAP`:
Copy code
sql
CREATE TABLE user_tags (
    user_id BIGINT,
    tag_bits BITMAP -- Instead of ARRAY<INT>
) 
DUPLICATE KEY(user_id)
DISTRIBUTED BY HASH(user_id);
2. Ingesting Data When loading data, use the
to_bitmap()
or
bitmap_from_string()
functions:
Copy code
sql
-- If inserting from an existing array column
INSERT INTO user_tags
SELECT user_id, bitmap_from_array(my_old_array_column) FROM old_table;

-- If inserting individual values (e.g., in a Stream Load or INSERT)
INSERT INTO user_tags VALUES (1, to_bitmap(12345));
3. Efficient Lookups Replace your
array_contains
calls with `bitmap_contains`:
Copy code
sql
-- Old way (Slow)
SELECT * FROM table WHERE array_contains(my_array, 12345);

-- New way (Fast)
SELECT * FROM table WHERE bitmap_contains(tag_bits, 12345);
Advanced Optimization: The Aggregate Key Model If your "array" is actually a collection of IDs that grows over time (e.g., a list of products a user viewed), you can use an
AGGREGATE KEY
table with the
BITMAP_UNION
function. This automatically "merges" integers into the bitmap as you load them, so you don't have to manage the array updates manually.
Copy code
sql
CREATE TABLE user_behavior (
    user_id BIGINT,
    viewed_ids BITMAP BITMAP_UNION
)
AGGREGATE KEY(user_id)
DISTRIBUTED BY HASH(user_id);

-- Every time you insert a single ID, it's added to the "set" for that user
INSERT INTO user_behavior VALUES (1, to_bitmap(55555));
Summary of Benefits * No Linear Scans: Instant membership check. * Deduplication: Bitmaps automatically handle uniqueness (no duplicate integers). * Parallelism: StarRocks is highly optimized for Roaring Bitmap operations at the CPU instruction level. References • sql-reference/sql-functions/bitmap-functions/bitmap_contains.md • using_starrocks/distinct_values/Using_bitmap.md • sql-reference/sql-functions/bitmap-functions/bitmap_from_array.md • sql-reference/sql-functions/bitmap-functions/unnest_bitmap.md References • sql-reference/sql-functions/bitmap-functions/bitmap_contains.md • using_starrocks/distinct_values/Using_bitmap.md • sql-reference/sql-functions/aggregate-functions/bitmap.md • sql-reference/sql-functions/bitmap-functions/bitmap_subset_in_range.md • sql-reference/sql-functions/bitmap-functions/bitmap_intersect.md
m
@Rocky I have golang code which stream loads json object to table. So my understanding is create a bitmap column and programmatically convert my array of ints to bitmaps? How should I go about it?
r
Since you are already using Golang and Stream Load for JSON, the most efficient approach is to let StarRocks handle the conversion during ingestion. This avoids complex bitmask logic in your Go code. You can use the
array_to_bitmap
function (available since v2.3) directly in your Stream Load headers. 1. Update your Table Schema Ensure your target column is of type
BITMAP
.
Copy code
sql
CREATE TABLE my_table (
    user_id BIGINT,
    tags BITMAP -- This will store your array of 5-digit ints
) 
DUPLICATE KEY(user_id)
DISTRIBUTED BY HASH(user_id);
2. Stream Load Mapping (Headers) When your Go code sends the JSON (e.g.,
{"user_id": 1, "raw_tags": [12345, 67890]}
), you need to map the JSON array to a temporary variable and then convert it. The logic: • `jsonpaths`: Extract
$.raw_tags
from the JSON. • `columns`: Map that extracted array to a temporary variable (e.g.,
tmp_array
), then set the real column
tags = array_to_bitmap(tmp_array)
. 3. Golang Implementation Example Using the standard
net/http
package, you would set the headers like this:
Copy code
go
func StreamLoad(jsonData []byte) {
    url := "http://<FE_HOST>:<FE_HTTP_PORT>/api/my_db/my_table/_stream_load"
    req, _ := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData))
    
    // Auth
    req.SetBasicAuth("admin", "password")
    
    // Required StarRocks Headers
    req.Header.Set("Expect", "100-continue")
    req.Header.Set("format", "json")
    req.Header.Set("strip_outer_array", "true") // If sending a list of objects
    
    // The Conversion Magic
    // 1. Tell SR where to find the array in your JSON
    req.Header.Set("jsonpaths", `["$.user_id", "$.raw_tags"]`)
    
    // 2. Map JSON fields to table columns + conversion
    // user_id: maps directly
    // tmp_array: a temporary name for the raw JSON array
    // tags: the actual table column, populated by the function
    req.Header.Set("columns", `user_id, tmp_array, tags=array_to_bitmap(tmp_array)`)

    client := &http.Client{}
    resp, err := <http://client.Do|client.Do>(req)
    // ... handle response
}
Why this is better: 1. Simplicity: Your Go code just sends raw JSON arrays. You don't need a Roaring Bitmap library in Go. 2. Performance: StarRocks performs the
array_to_bitmap
conversion in parallel across its BE nodes. 3. Automatic Deduplication: If your JSON array has duplicates (e.g.,
[12345, 12345]
), the
BITMAP
type automatically handles it, storing only unique values. Alternative (Pre-processing in Go) If you prefer to convert to Bitmap in Go (to reduce network payload size for extremely large arrays): 1. Use a Roaring Bitmap library in Go to create the bitmap. 2. Serialize it to a byte array. 3. Base64 encode that byte array. 4. In StarRocks Stream Load, use
tags = base64_to_bitmap(tmp_base64_string)
. Recommendation: Stick to the
array_to_bitmap
method first. It is significantly easier to maintain and usually fast enough for 5-digit integer arrays. References • sql-reference/sql-functions/array-functions/array_to_bitmap.md • sql-reference/sql-statements/loading_unloading/STREAM_LOAD.md • loading/Json_loading.md • sql-reference/sql-functions/bitmap-functions/base64_to_bitmap.md • using_starrocks/distinct_values/Using_bitmap.md • sql-reference/sql-functions/aggregate-functions/bitmap.md
m
@Rocky Can't I just add a generated column instead? So no need change stream load code. Whenever data loaded, generated column automatically converts array int to bitmap
r
Yes, you can use a Generated Column (available since v3.1) to automate this conversion without touching your Go code. This is exactly what generated columns were designed for: pre-computing a complex transformation during ingestion. However, there is one major requirement: your table must be a Primary Key or Unique Key table. StarRocks currently does not support the
BITMAP
data type in
DUPLICATE KEY
tables. The "No-Code-Change" Solution: Generated Columns If you define a generated column, StarRocks will see the incoming JSON array, run the conversion function, and store the result in a bitmap column automatically. 1. Create the table with a Generated Column:
Copy code
sql
CREATE TABLE user_tags (
    user_id BIGINT,
    -- This column receives the raw JSON array from your Go code
    tags_raw ARRAY<INT>, 
    -- This generated column automatically converts the array to a bitmap
    tags_bitmap BITMAP AS array_to_bitmap(tags_raw)
) 
PRIMARY KEY(user_id) -- Must be Primary or Unique Key to support BITMAP
DISTRIBUTED BY HASH(user_id);
2. How it works with your Go code: * Your Go code continues sending:
{"user_id": 1, "tags_raw": [10001, 10002]}
. * StarRocks writes the array to
tags_raw
. * StarRocks automatically calculates the bitmap and stores it in
tags_bitmap
. * Querying: You now query the bitmap column for instant lookups:
Copy code
sql
    SELECT * FROM user_tags WHERE bitmap_contains(tags_bitmap, 10001);
────────── Important Considerations 1. Storage Overhead By using a generated column, you are storing the data twice (once as an array and once as a bitmap). If you have 10TB of data, this might be expensive. * Optimization: If you don't need the
ARRAY
column for anything else, the best practice is actually to change the Stream Load Headers (specifically the
columns
header) as mentioned before. This allows you to convert the array into the bitmap while loading and discard the raw array, saving 50% of your storage. 2. Duplicate Key Tables If your 10TB table is a
DUPLICATE KEY
table (which is common for logs), you cannot use the
BITMAP
data type. In that case, you have two options for 5-digit integers: * Option A: Change to a Primary Key Table. This allows Bitmaps but adds overhead for primary key management. * Option B: Stick with Array but use a Bloom Filter. You can add a Bloom Filter index to an
ARRAY
column in some versions, but it's less effective than a Bitmap for exact lookups. 3. Performance The
array_to_bitmap
function is highly optimized. Even though it runs during the load process, for 5-digit integers, the bottleneck is usually the network or I/O, not the CPU conversion. Summary * If you can change the table model to Primary Key: Use a Generated Column to avoid changing Go code. * If you can change the Stream Load config: Use Stream Load Mapping (
tags=array_to_bitmap(tags_json)
) to save storage by not keeping the raw array. * If you must stay on a Duplicate Key table: You cannot use Bitmaps; you should look into optimizing your
array_contains
queries with better partitioning or sort keys. References • sql-reference/sql-statements/generated_columns.md • sql-reference/sql-functions/array-functions/array_to_bitmap.md • using_starrocks/distinct_values/Using_bitmap.md • table_design/table_types/table_capabilities.md • sql-reference/sql-statements/table_bucket_part_index/CREATE_TABLE.md References
m
@Rocky
Copy code
select rule_group_ids_bitmap from requests where rule_group_ids_bitmap is not null;
bitmap type couldnt as predicate
r
The error you're seeing and the "empty" results are because the
BITMAP
data type in StarRocks is a specialized binary format (Roaring Bitmap). It behaves differently than standard types like
INT
or `STRING`: 1. Why you can't use it as a predicate The error
bitmap type couldnt as predicate
occurs because StarRocks does not allow a
BITMAP
column to be evaluated directly in a
WHERE
clause. Even
IS NOT NULL
can sometimes trigger issues in specific versions or contexts if the engine expects a bitmap-specific function. To check if a bitmap has any values, use the
bitmap_count()
function:
Copy code
sql
-- This checks if the bitmap contains at least one integer
SELECT rule_group_ids_bitmap 
FROM requests 
WHERE bitmap_count(rule_group_ids_bitmap) > 0;
2. Why the results look "NULL" When you
SELECT
a bitmap column directly, StarRocks (and most CLI clients) will display
NULL
by default. This is a safety measure because bitmaps are complex binary objects that can contain millions of IDs; printing the raw binary would be unreadable and could crash the terminal. To actually see the IDs stored inside the bitmap, you must convert it to a string:
Copy code
sql
SELECT bitmap_to_string(rule_group_ids_bitmap) 
FROM requests 
WHERE bitmap_count(rule_group_ids_bitmap) > 0;
3. Proper way to query Bitmaps Here are the common patterns for your 5-digit integer use case: * Check if a specific ID exists:
Copy code
sql
    SELECT * FROM requests WHERE bitmap_contains(rule_group_ids_bitmap, 12345);
* Get the number of items in the bitmap:
Copy code
sql
    SELECT bitmap_count(rule_group_ids_bitmap) FROM requests;
* Check for intersection (common items between two bitmaps):
Copy code
sql
    SELECT bitmap_count(bitmap_intersect(col1, col2)) FROM table;
Summary of what to change: Change your query to this to verify your data:
Copy code
sql
SELECT 
    bitmap_count(rule_group_ids_bitmap) as qty, 
    bitmap_to_string(rule_group_ids_bitmap) as contents
FROM requests 
WHERE rule_group_ids_bitmap IS NOT NULL 
  AND bitmap_count(rule_group_ids_bitmap) > 0;
References • using_starrocks/distinct_values/Using_bitmap.md • table_design/indexes/Bitmap_index.md • knowledge/usage/best-practice.md
m
@Rocky bitmap seemed to perform worse that array_contains. My column is guaranteed <= 30 INT values. Is this why? bitmap only has value at scale where array is large
r
Your observation is correct. For a set as small as 30 integers, an
ARRAY
will typically outperform a
BITMAP
in StarRocks for simple "contains" checks. The
BITMAP
type in StarRocks uses Roaring Bitmaps, which are highly optimized for massive datasets but carry specific overheads that only make sense at scale. Why ARRAY is faster for small sets (≤ 30 elements) 1. Deserialization Overhead:
BITMAP
is a complex binary object. Every time you call
bitmap_contains
, StarRocks must "open" (deserialize) that binary object to understand its internal structure (containers). For an array of 30 integers, the time spent just preparing the bitmap to be searched often exceeds the time it takes to simply scan 30 raw integers in an
ARRAY
. 2. Simple Linear Scans: The
array_contains
function is highly optimized for vectorized execution. Scanning 30 integers is trivial for modern CPUs—they can often do it in a single clock cycle using SIMD instructions. The "sophisticated" lookup logic of a bitmap (binary searches through containers) is overkill for such a small list. 3. Internal "Array" Containers: Even internally, Roaring Bitmaps store small sets in an "Array Container" (a sorted list of shorts). So by using
BITMAP
, you are essentially wrapping an array inside a complex metadata layer. Using the
ARRAY
type directly removes that extra layer. When does BITMAP start to win?
BITMAP
is not designed to replace small arrays; it is designed for Set Theory and Count Distinct at scale. You should switch to
BITMAP
when: * Massive Cardinality: Your sets grow to thousands or millions of IDs per row. * Set Operations: You need to perform
UNION
or
INTERSECT
across millions of rows (e.g., "Find users who have Tag A AND Tag B").
BITMAP
can do this using extremely fast bitwise math. * Precise Count Distinct: You need to calculate unique values (
bitmap_union_count
) across billions of rows instantly. * Memory Efficiency: You have very "dense" data (e.g., IDs 1 to 1,000,000). A bitmap can store this in just a few bits, whereas an array would require 4MB of RAM. Recommendation For your specific case (guaranteed ≤ 30 elements and 5-digit integers): * Stick with
ARRAY<INT>
. It is simpler to manage in your Go code, uses less CPU for membership checks (
array_contains
), and doesn't require the Primary Key table restriction that the
BITMAP
type does. * Optimization Tip: If you frequently query these IDs, ensure your
ARRAY
column is not the first column in your table unless it's part of the sort key, and consider if you can use a Bloom Filter index on the array if your queries are highly selective. References • sql-reference/sql-functions/array-functions/array_contains.md • using_starrocks/distinct_values/Using_bitmap.md • sql-reference/sql-functions/bitmap-functions/bitmap_contains.md • table_design/indexes/Bitmap_index.md • sql-reference/sql-functions/bitmap-functions/bitmap_union.md • table_design/indexes/Bloom_filter_index.md References • table_design/indexes/Bitmap_index.md