zbarry
05/14/2026, 11:03 PMCREATE TABLE IF NOT EXISTS table (
`customer_id` varchar(256),
`event_id` varchar(256),
) ENGINE=OLAP
PRIMARY KEY(`customer_id`, `event_id`)
PARTITION BY CONCAT(pmod(xx_hash3_64(`customer_id`), 256))
DISTRIBUTED BY HASH(`customer_id`, `event_id`) BUCKETS 32
If I query the table for a given customer ID, EXPLAIN ANALYZE indicates all 8192 tablets were scanned. If I calculate the pmod(xx_hash3_64(<customer_id>), 256) and filter explicitly on __generated_partition_column_0 only 32 are scanned (i.e. 1 partition)
I'm wondering if this is expected and whether there's any elegant way to abstract the generated partition column from the client querying Starrocks 🙏
Starrocks version: 3.5.14Rocky
05/14/2026, 11:04 PMcustomer_id) to a partition expression is currently restricted to a specific set of monotonic functions—primarily date and time functions (e.g., date_trunc, time_slice, from_unixtime).
For complex or non-monotonic expressions like `pmod(xx_hash3_64(...), 256)`:
* Partition Pruning: The optimizer does not automatically calculate the hash value of your filter constant and inject a predicate for the hidden partition column. It only prunes if the query explicitly filters on the expression or the internal generated column.
* Bucket Pruning: In your example, even if partition pruning worked, bucket pruning would still fail for a query on customer_id alone because your distribution key is HASH(customer_id, event_id). StarRocks requires all columns of the hash distribution key to be present in the WHERE clause to prune buckets.
2. Is there an elegant way to abstract this?
Since you are on v3.5.14, you can use Explicit Generated Columns to make the schema more readable, though the query requirements remain similar.
Option A: Use a named Generated Column (Recommended)
Instead of relying on the hidden __generated_partition_column_0, define the column explicitly. This allows you to give it a meaningful name and potentially use it in a VIEW to simplify the client-side logic.
sql
CREATE TABLE my_table (
`customer_id` varchar(256),
`event_id` varchar(256),
-- Define the expression as a named generated column
`p_id` INT AS pmod(xx_hash3_64(`customer_id`), 256)
) ENGINE=OLAP
PRIMARY KEY(`customer_id`, `event_id`)
PARTITION BY (`p_id`) -- Partition by the generated column
DISTRIBUTED BY HASH(`customer_id`) BUCKETS 32; -- Simplified distribution
To get pruning, the client still needs to provide p_id. If you want to abstract this from the end-user, you could use a View or a Prepared Statement where the hash logic is encapsulated, but the database won't currently "auto-derive" it for this specific hash expression.
Option B: Optimize for "Bucket Pruning" instead
In StarRocks, partitioning is typically used for lifecycle management (TTL) or coarse-grained pruning (Date/Region). For fine-grained distribution of a high-cardinality ID like customer_id, Bucketing is usually more efficient and "elegant" because it handles pruning automatically.
If you change your distribution to just customer_id, the pruning happens at the bucket level without needing a complex partition expression:
sql
CREATE TABLE my_table (
`customer_id` varchar(256),
`event_id` varchar(256)
) ENGINE=OLAP
PRIMARY KEY(`customer_id`, `event_id`)
-- Use a single partition or simple date partitioning
DISTRIBUTED BY HASH(`customer_id`) BUCKETS 8192;
Why this is better:
1. Automatic Pruning: WHERE customer_id = 'A' will automatically prune to exactly 1 tablet out of 8192.
2. No Hidden Columns: You don't need to manage pmod or hash expressions in your DDL or DML.
3. Performance: StarRocks handles thousands of tablets per table/partition very efficiently.
Summary
If you need the 8192-way split for performance and the primary query key is customer_id, the most "StarRocks-native" approach is to use a high bucket count on customer_id rather than manual hash partitioning. If you must use partitioning (e.g., for TTL), the client must explicitly filter on the partition column or the exact expression for pruning to occur.
References
• table_design/data_distribution/expression_partitioning.mdzbarry
05/14/2026, 11:10 PMRocky
05/14/2026, 11:11 PMmax_bucket_number_per_partition, which defaults to 1024 in recent versions), but it carries significant operational risks.
While StarRocks is designed to handle high tablet counts, 30,000 tablets in a single partition is considered an extreme configuration. Here are the primary concerns:
1. FE Metadata and Planning Overhead
The Frontend (FE) manages metadata at the tablet level.
* Memory: Every tablet (and its 3 replicas, totaling 90,000 tablets) must be tracked in the FE’s JVM heap. This increases the FE's memory footprint and can lead to longer GC pauses.
* Query Planning: When you query the table, the FE must calculate the distribution for all 30,000 buckets. If a query doesn't perfectly prune to a few buckets, the FE has to generate a plan that manages thousands of scan ranges, increasing RPC overhead and planning latency.
2. BE Memory and Ingestion Pressure (Critical for Primary Key)
Since you are using a Primary Key table, each tablet maintains its own Primary Key Index and Memtable.
* Index Memory: The Primary Key index is stored in BE memory. 30,000 tablets mean 30,000 separate index structures. This is significantly less memory-efficient than having fewer, larger indexes.
* Write Amplification: During a load, if your data is spread across all 30,000 buckets, each BE must open thousands of file writers and memtables. This can lead to very small files being flushed, causing "small file syndrome" and hitting BE memory limits quickly.
3. Compaction and Background Tasks
* Compaction Frequency: Compaction is performed per tablet. With 30,000 tablets, the BE has to manage 30,000 separate compaction queues.
* Scheduling: StarRocks 3.x schedules compaction tasks by partition. A single partition with 30,000 tablets creates a massive scheduling unit, which can lead to compaction "starvation" or extremely long queues that struggle to keep up with high-frequency writes.
* Replication/Repair: If a BE node goes down, the cluster must repair 1/Nth of those 90,000 replicas. Managing the state and transfer of tens of thousands of small metadata objects is much slower than moving a few hundred large ones.
4. Comparison to Best Practices
The general "sweet spot" for StarRocks is:
* Tablet Size: 1 GB to 10 GB (compressed).
* Tablet Count per BE: Keep the total tablet count (across all tables) below 200,000 per BE for stability.
* Partition Granularity: For the 8,192 tablets in your previous example, that is usually manageable. Moving to 30,000+ buckets in a single partition for one table uses up a large portion of your cluster's "metadata budget."
Recommendation
If you need 30,000 buckets because you have 300 TB+ of data in that single partition, the design is justified. However, if your data volume is smaller (e.g., a few Terabytes), you are better off with a much lower bucket count:
1. Reduce Buckets: Aim for a number that results in ~5GB per tablet. For a 5TB partition, 1,000 buckets is plenty.
2. Use Partitioning for Lifecycle: Use Date partitioning to break the 30,000 tablets into smaller sets (e.g., 100 partitions with 300 buckets each). This allows the FE to prune tablets more effectively.
3. FE Config: If you proceed, you must set ADMIN SET FRONTEND CONFIG ("max_bucket_number_per_partition" = "32768"); on the leader FE.
References
• faq/operation_maintenance_faq.md
• best_practices/partitioning.md
• table_design/data_distribution/Data_distribution.md
References
• faq/Deploy_faq.mdzbarry
05/14/2026, 11:13 PM__generated_partition_column_0, define the column explicitly. This allows you to give it a meaningful name and potentially use it in a VIEW to simplify the client-side logic.
sql
CREATE TABLE my_table (
`customer_id` varchar(256),
`event_id` varchar(256),
-- Define the expression as a named generated column
`p_id` INT AS pmod(xx_hash3_64(`customer_id`), 256)
) ENGINE=OLAP
PRIMARY KEY(`customer_id`, `event_id`)
PARTITION BY (`p_id`) -- Partition by the generated column
DISTRIBUTED BY HASH(`customer_id`) BUCKETS 32; -- Simplified distribution
To get pruning, the client still needs to provide p_id. If you want to abstract this from the end-user, you could use a View or a Prepared Statement where the hash logic is encapsulated, but the database won't currently "auto-derive" it for this specific hash expression.Rocky
05/14/2026, 11:13 PMcustomer_id) and your generated partition column (p_id).
When a user queries the View with a constant filter on customer_id, the StarRocks Cost-Based Optimizer (CBO) can perform constant propagation. It sees that customer_id is a constant, calculates the hash value, and effectively injects a filter on the partition column.
Step 1: Create the Table with a Named Generated Column
sql
CREATE TABLE my_table (
`customer_id` varchar(256),
`event_id` varchar(256),
`p_id` INT AS pmod(xx_hash3_64(`customer_id`), 256) -- Named generated column
) ENGINE=OLAP
PRIMARY KEY(`customer_id`, `event_id`)
PARTITION BY (`p_id`)
DISTRIBUTED BY HASH(`customer_id`) BUCKETS 32;
Step 2: Create the View
sql
CREATE VIEW my_table_view AS
SELECT customer_id, event_id
FROM my_table
-- This clause tells the optimizer: "p_id is always the hash of customer_id"
WHERE p_id = pmod(xx_hash3_64(customer_id), 256);
Step 3: Query the View
When the client runs this query:
sql
SELECT * FROM my_table_view WHERE customer_id = 'user_123';
The optimizer performs these steps internally:
1. Inlining: SELECT ... FROM my_table WHERE customer_id = 'user_123' AND p_id = pmod(xx_hash3_64(customer_id), 256)
2. Constant Substitution: The optimizer replaces customer_id with 'user_123' inside the pmod function.
3. Constant Folding: It evaluates pmod(xx_hash3_64('user_123'), 256) which results in a constant (e.g., 42).
4. Pruning: The final plan includes WHERE p_id = 42, allowing StarRocks to prune all but one partition.
──────────
2. Prepared Statements
Prepared statements abstract the complexity from the client's application code by allowing you to define the logic once on the server side. This is useful if you don't want to manage a View but want the client to only send the customer_id.
Definition:
sql
PREPARE customer_lookup FROM
'SELECT * FROM my_table WHERE customer_id = ? AND p_id = pmod(xx_hash3_64(?), 256)';
Execution:
The client only needs to pass the ID twice (or the driver may handle it):
sql
SET @uid = 'user_123';
EXECUTE customer_lookup USING @uid, @uid;
Even though the client still provides the value twice, the logic (the hash function and the reference to p_id) is hidden within the prepared statement on the StarRocks FE. The FE will evaluate the pmod function during the execution phase and prune partitions accordingly.
Summary of Benefits
* Encapsulation: The end-user or BI tool just sees customer_id and doesn't need to know that p_id exists.
* Correctness: If you ever change your hashing logic (e.g., change 256 to 512), you only update the Table and the View/Prepared Statement in one place.
* Performance: Both methods ensure that the EXPLAIN plan shows only the necessary partitions being scanned, preventing the "8192 tablet scan" issue you're seeing" scan" were seeing.
Note: For the View method to work most reliably, ensure your StarRocks enable_cbo is set to true (default in 3.x).
References
• table_design/data_distribution/expression_partitioning.md
• table_design/data_distribution/Data_distribution.mdzbarry
05/14/2026, 11:32 PM_tmp appended, insert the data, then swap the table names. How would both of these solutions work with needing to update the Smart View or Prepared Statement simultaneously?Rocky
05/14/2026, 11:33 PMsql
-- New table with 512 partitions
CREATE TABLE my_table_v2 ( ... ) PARTITION BY (pmod(..., 512)) ...;
-- New "Logic View" containing the 512-hash predicate
CREATE VIEW my_table_v2_internal AS
SELECT * FROM my_table_v2
WHERE p_id = pmod(xx_hash3_64(customer_id), 512);
2. Perform the Migration
Load your data into my_table_v2. Once ready, you can perform a nearly-atomic cutover using a "Router View."
If your application queries a generic view name (e.g., customer_events_view), you simply redefine it:
sql
-- Atomic metadata update in the Frontend
CREATE OR REPLACE VIEW customer_events_view AS
SELECT * FROM my_table_v2_internal;
This single command points all incoming traffic to the new table and the new hashing logic at the exact same instant.
3. Cleanup
Once the cutover is verified, you can drop the old table and the old internal view.
──────────
Comparison Summary
Feature | Prepared Statement | View (Recommended)
Scope | Current Session Only | Global (All Users)
Updates | Client must re-prepare | Admin updates once in SR
Schema Swap | High risk of logic mismatch | Can be updated atomically
Transparency | Client must know the SQL | Client just selects from a name
Conclusion: Use the View approach. It provides a clean abstraction layer that allows you to change partition counts, hash functions, or even underlying table names without the client application ever knowing a change occurred.
References
• sql-reference/sql-statements/View/CREATE_VIEW.md
• sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md
• table_design/data_distribution/expression_partitioning.md
References
• sql-reference/sql-statements/generated_columns.md
• table_design/data_distribution/expression_partitioning.md
• sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.mdzbarry
05/14/2026, 11:35 PM_v2 prefix I'd like to avoid thatRocky
05/14/2026, 11:36 PM_v2 or _staging) in your final table names, you can use the ALTER TABLE ... SWAP WITH ... command.
This command is atomic and swaps the underlying data and metadata between two tables. Here is the workflow to upgrade your hashing logic while keeping your production table and view names clean.
The "Clean Name" Workflow
1. Setup the Staging Table
Create a temporary table with your new partitioning logic (e.g., 512 partitions). You can name it anything temporary, like my_table_staging.
sql
CREATE TABLE my_table_staging (
`customer_id` varchar(256),
`event_id` varchar(256),
`p_id` INT AS pmod(xx_hash3_64(`customer_id`), 512) -- New 512 logic
) ENGINE=OLAP
PRIMARY KEY(`customer_id`, `event_id`)
PARTITION BY (`p_id`)
DISTRIBUTED BY HASH(`customer_id`) BUCKETS 32;
2. Load Data
Populate my_table_staging from your current my_table.
sql
INSERT INTO my_table_staging SELECT * FROM my_table;
3. The Atomic Swap
Run the SWAP command. This immediately makes my_table_staging become my_table, and the old production table becomes my_table_staging.
sql
ALTER TABLE my_table SWAP WITH my_table_staging;
_At this point: my_table has the 512 partitions, but the View logic is still using the old 256 hash._
4. Update the View Logic
Immediately update your View to reflect the new hashing logic. Since this is a metadata-only change in the Frontend, it takes milliseconds.
sql
CREATE OR REPLACE VIEW customer_view AS
SELECT customer_id, event_id
FROM my_table -- This name remains the same
WHERE p_id = pmod(xx_hash3_64(customer_id), 512); -- Update logic here
5. Cleanup
Drop the staging table ( which now contains the old 256-partition data).
sql
DROP TABLE my_table_staging;
Why this works well:
1. Name Persistence: Your application continues to query customer_view, and that view continues to point to my_table. No code changes are required on the client side.
2. No Version Bloat: You don't end up with my_table_v1, v2, etc. After the cleanup step, your database schema looks exactly as it did before, just with updated logic.
3. Minimal Downtime: The SWAP and CREATE OR REPLACE VIEW commands are metadata operations. The only "heavy" part is the INSERT INTO ... SELECT which happens in the background before you cut over.
Pro-Tip: Transactional Safety
While StarRocks doesn't currently support multi-statement DDL transactions (to wrap the SWAP and VIEW update together), the window of time between the two commands is extremely small. If a query hits the View in the 100ms between the SWAP and the VIEW update, it might simply scan more data than necessary or return 0 rows for a specific customer_id (because it's looking in the wrong partition), but it will not crash.
References
• sql-reference/sql-statements/table_bucket_part_index/ALTER_TABLE.md
• sql-reference/sql-statements/View/CREATE_VIEW.md
• table_design/data_distribution/Data_distribution.mdzbarry
05/14/2026, 11:49 PMCREATE VIEW test_view AS
SELECT *
FROM my_table
WHERE __generated_partition_column_0 = pmod(xx_hash3_64(customer_id), 256);
But then when I explain analyze a query on the view with a customer_id filter, it shows the query uses pmod(xx_hash3_64(customer_id), 256) directly in the WHERE clause instead of __generated_partition_column_0 = <literal>. Here's what the plan tells us:
The critical node is SELECT (id=2):
Predicates: [CAST(29: __generated_partition_column_0 AS DECIMAL128(38,9)) = CAST(pmod(xx_hash3_64(1: customer_id), 256) AS DECIMAL128(38,9)), ...]
This shows the planner pushed the predicate down as a post-scan filter, not as a partition pruning hint. The pmod(xx_hash3_64(customer_id), 256) expression is being evaluated after the OLAP_SCAN — meaning:
1. OLAP_SCAN scans all 8,192 tablets (no partition pruning)
2. SELECT node filters the results by comparing __generated_partition_column_0 to the computed hash
Performance: 427ms, 42.8 GB memory — worse than both previous queries. The planner cannot use an expression like WHERE __generated_partition_column_0 = pmod(xx_hash3_64(customer_id), 256) for partition pruning — it needs a literal integer value like WHERE __generated_partition_column_0 = 200.Rocky
05/14/2026, 11:50 PMCAST (in your case, to DECIMAL128). StarRocks' partition pruner is very strict: if it sees CAST(partition_column AS ...) = ... instead of the raw partition_column = ..., it cannot identify the partition to scan and falls back to a full table scan.
Why it's failing in your test:
1. Expression Type: xx_hash3_64 returns a BIGINT. Consequently, pmod(xx_hash3_64(...), 256) also returns a BIGINT.
2. Column Type: If you defined your p_id (or __generated_partition_column_0) as an INT, the comparison becomes INT = BIGINT.
3. The Pruning Killer: To compare an INT to a BIGINT, StarRocks casts both sides to a common type (often DECIMAL128 or BIGINT). As soon as your explain shows CAST(__generated_partition_column_0 AS ...), partition pruning is disabled.
The Solution: Perfect Type Alignment
You must ensure that the generated column and the expression result are the exact same type.
1. Re-create the Table with BIGINT
Change the generated column type to BIGINT to match the return type of xx_hash3_64. Also, use a named column to avoid relying on internal __generated names.
sql
CREATE TABLE my_table (
`customer_id` varchar(256),
`event_id` varchar(256),
-- Match the return type of the function (BIGINT)
`p_id` BIGINT AS pmod(xx_hash3_64(`customer_id`), 256)
) ENGINE=OLAP
PRIMARY KEY(`customer_id`, `event_id`)
PARTITION BY (`p_id`)
DISTRIBUTED BY HASH(`customer_id`) BUCKETS 32;
2. Update the View with Explicit Typing
In your View, ensure the logic uses the same types. It is often helpful to wrap the expression in an explicit CAST to the column's type to "force" the optimizer to see the equality clearly.
sql
CREATE OR REPLACE VIEW test_view AS
SELECT *
FROM my_table
-- Explicitly cast the expression to the column's type (BIGINT)
WHERE p_id = CAST(pmod(xx_hash3_64(customer_id), 256) AS BIGINT);
How to verify it works
Run EXPLAIN (not necessarily ANALYZE yet) on your query:
sql
EXPLAIN SELECT * FROM test_view WHERE customer_id = 'user_123';
Look for the OlapScanNode. You should see:
* partitions=1/8192 (or however many partitions you have).
* The PREDICATES section should show p_id = <some_integer_literal> (e.g., p_id = 142) without any CAST on the p_id side.
Why the "View Trick" is still better than literals
If you can get the types to align, the View trick is powerful because:
* Application Simplicity: Your app only sends WHERE customer_id = '...'.
* CBO Folding: The StarRocks CBO performs "Constant Folding" (calculating the hash of the literal string) and then "Predicate Propagation" (applying that result to the p_id column) before the pruning phase.
Note on Version: This constant propagation behavior is most reliable in StarRocks v3.1 and later where the CBO has better support for generated column rewrites.
References
• sql-reference/sql-functions/hash-functions/xx_hash3_64.md
• sql-reference/sql-statements/generated_columns.md
• table_design/data_distribution/expression_partitioning.md
References
• sql-reference/sql-functions/math-functions/pmod.md
• sql-reference/sql-functions/hash-functions/xx_hash3_64.mdRocky
05/14/2026, 11:50 PMRocky
05/15/2026, 9:43 PMCAST to DECIMAL128 (which killed partition pruning) is that StarRocks is extremely sensitive to type mismatches in the partition pruner. Even a BIGINT vs INT comparison can trigger a promotion to DECIMAL to ensure precision, which the pruner cannot handle.
To fix this and ensure the Smart View triggers partition pruning, use this exact DDL and View definition.
1. The Correct Table DDL
Use BIGINT for the partition column because xx_hash3_64 returns a BIGINT. Also, use 256 as a BIGINT literal by using the L suffix or an explicit cast to ensure the pmod return type is stable.
sql
CREATE TABLE my_table (
`customer_id` varchar(256),
`event_id` varchar(256),
-- Define as BIGINT to match the hash function output
`p_id` BIGINT AS pmod(xx_hash3_64(`customer_id`), 256)
) ENGINE=OLAP
PRIMARY KEY(`customer_id`, `event_id`)
PARTITION BY (`p_id`)
DISTRIBUTED BY HASH(`customer_id`) BUCKETS 32;
2. The Fixed View Logic
In the View, you must ensure the expression on the right matches the p_id type exactly. By wrapping the entire expression in CAST(... AS BIGINT), you tell the optimizer "this is a BIGINT," allowing it to match the p_id column without an implicit cast.
sql
CREATE OR REPLACE VIEW test_view AS
SELECT
customer_id,
event_id
FROM my_table
WHERE p_id = CAST(pmod(xx_hash3_64(customer_id), 256) AS BIGINT);
3. How to verify the fix
Run EXPLAIN (not EXPLAIN ANALYZE) on a query with a filter:
sql
EXPLAIN SELECT * FROM test_view WHERE customer_id = 'user_123';
What to look for in the output:
1. OlapScanNode: Check the partitions field. It should say 1/256 (or however many partitions you have). If it says 256/256, the pruning failed.
2. PREDICATES: It should show 29: p_id = <some_number> (where 29 is the column ID). If you see CAST(29: p_id AS ...) or any function wrapping p_id, the pruning is broken.
Why this works
When you query the view with WHERE customer_id = 'user_123', the StarRocks Optimizer performs Constant Folding. It calculates the hash of 'user_123' during the planning phase. Because the types match exactly, it simplifies the View's WHERE clause from a function call to a simple constant (e.g., p_id = 142), which the partition pruner can then use to skip all other 255 partitions.
References
• sql-reference/sql-statements/generated_columns.md
• sql-reference/sql-functions/hash-functions/xx_hash3_64.md
• table_design/data_distribution/expression_partitioning.md
References
• using_starrocks/accelerate-cardinality-preserving-joins.md
• sql-reference/sql-statements/generated_columns.md
• release_notes/release-3.3.md
• table_design/data_distribution/expression_partitioning.mdzbarry
05/15/2026, 9:44 PM`partition_id` bigint(20) NULL AS pmod(xx_hash3_64(customer_id), 256) COMMENT "" )
...
PARTITION BY (`partition_id`)
and then a view
CREATE OR REPLACE VIEW customers_view AS
SELECT *
FROM customers
WHERE partition_id = CAST(pmod(xx_hash3_64(customer_id), 256) AS BIGINT);
but there is no partition pruning in the explain resultRocky
05/15/2026, 9:45 PMexplain results is due to a known limitation in how the StarRocks optimizer handles constant folding for partition pruning, combined with a potential type-mismatch issue that triggers an implicit cast.
Based on the explain output you provided (CAST(... AS DECIMAL128)), here is what is happening and how to fix it:
1. The "Type Mismatch" Killer
StarRocks' partition pruner is extremely sensitive. If the optimizer sees a CAST on the partition column (e.g., CAST(partition_id AS DECIMAL128)), it disables partition pruning immediately because it can no longer guarantee a direct mapping to the physical partition boundaries.
Your explain shows both sides being cast to DECIMAL128(38,9). This often happens because:
* The literal 256 or the hash result is being promoted to a high-precision decimal.
* The generated column partition_id was inferred as a different type in an earlier iteration.
The Fix: Ensure every component is strictly BIGINT.
* Use 256L (the L suffix forces BIGINT).
* Check that customer_id is a VARCHAR.
2. Version-Specific Bug (#54543)
There was a specific bug in StarRocks (fixed in versions 3.2.13+, 3.3.3+, and 3.4.0+) where constant folding for List partitions using generated columns would fail due to these exact type mismatches. If you are on an older version (like 3.1.x or early 3.2.x), the optimizer will fail to turn pmod(xx_hash3_64('literal'), 256) into a constant (like 142) before the partition pruning phase.
3. The "Smart View" limitations
For the View trick to work, the optimizer must perform Constant Folding before Partition Pruning. If the optimizer sees partition_id = pmod(xx_hash3_64('literal'), 256), it must calculate the result first.
If your explain still shows the function pmod(...) in the PREDICATES section of the OlapScanNode, it means the optimizer has not folded the function. StarRocks generally only folds "Immutable" functions. While xx_hash3_64 is deterministic, it may not be handled by the constant folder in the specific plan phase where partition pruning occurs in your version.
Recommended Action Plan
Try this exact DDL and Query to verify if your version supports this optimization:
Step 1: Strictly Typed Table
sql
CREATE TABLE customers (
`customer_id` varchar(256),
`event_id` varchar(256),
-- Ensure p_id is BIGINT and the expression uses a BIGINT literal (256L)
`p_id` BIGINT AS pmod(xx_hash3_64(`customer_id`), 256L)
) ENGINE=OLAP
PRIMARY KEY(`customer_id`, `event_id`)
PARTITION BY (`p_id`)
DISTRIBUTED BY HASH(`customer_id`) BUCKETS 32;
Step 2: Simplified View
Avoid the CAST in the View if possible, as it can sometimes confuse the folder.
sql
CREATE OR REPLACE VIEW customers_view AS
SELECT customer_id, event_id
FROM customers
WHERE p_id = pmod(xx_hash3_64(customer_id), 256L);
Step 3: Verification Query
sql
EXPLAIN SELECT * FROM customers_view WHERE customer_id = 'user123';
If you still see `partitions=256/256`:
It means your StarRocks version does not support propagating constants through a hash function into a partition pruner. In this case, the only way to get partition pruning is to have the application pass the p_id directly:
sql
-- Application logic:
-- 1. Calculate hash_val = pmod(xx_hash3_64('user123'), 256) in your app code
-- 2. Query with the literal:
SELECT * FROM customers
WHERE customer_id = 'user123' AND p_id = 142;
Check your version: Run SELECT version();. If you are below 3.3.3, I highly recommend upgrading to take advantage of the fixes for generated column pruning (#54543).
References
• sql-reference/sql-statements/generated_columns.md
• release_notes/release-3.3.mdRocky
05/15/2026, 9:45 PMzbarry
05/15/2026, 9:47 PMPREDICATES: 1: customer_id = 'abc', 29: partition_id = 29: partition_idRocky
05/15/2026, 9:47 PMpartition_id = partition_id in your explain output is a side effect of a feature called Generated Column Rewrite (introduced in v3.1).
Why the "Smart View" is backfiring
The StarRocks optimizer recognizes that the expression pmod(xx_hash3_64(customer_id), 256) is the exact definition of your generated column partition_id. To "simplify" the plan, it rewrites the expression in your View to the column itself.
1. View Logic: WHERE partition_id = pmod(xx_hash3_64(customer_id), 256)
2. Optimizer Rewrite: "I see you're comparing partition_id to its own formula. That's just partition_id = partition_id."
3. Result: The filter becomes a tautology (always true), the literal value of customer_id is never used to calculate a partition ID, and pruning fails.
The Modern Solution (StarRocks 3.2+)
In recent versions of StarRocks (especially v3.3.3+ and v3.4+), you no longer need the "Smart View" trick. The optimizer is now designed to handle this automatically on the base table.
1. Simplify your Table and View
Remove the manual equality check from the view. If you are on a recent version, StarRocks will perform "Predicate Derivation": when it sees customer_id = 'abc', it will automatically derive partition_id = pmod(xx_hash3_64('abc'), 256) behind the scenes.
Correct View:
sql
CREATE OR REPLACE VIEW customers_view AS
SELECT * FROM customers; -- No special WHERE clause needed!
2. Check for "Constant Folding" Support
If automatic pruning still doesn't work after removing the View trick, it usually means the version you are on doesn't support deriving a partition key from the xx_hash3_64 function yet.
To verify if your version supports this, run:
sql
EXPLAIN SELECT * FROM customers WHERE customer_id = 'abc';
If partitions still shows all partitions, your version lacks the automatic derivation for this specific hash function.
How to force it (The "Wrap" Trick)
If you are on an older version and must use the View trick, you have to prevent the optimizer from rewriting the expression to the column name. You can do this by slightly modifying the expression so it doesn't match the DDL exactly, but still calculates the same value:
sql
CREATE OR REPLACE VIEW customers_view AS
SELECT *
FROM customers
-- Adding + 0 prevents the "Generated Column Rewrite"
-- but still allows the constant folder to calculate the result
WHERE partition_id = (CAST(pmod(xx_hash3_64(customer_id), 256) AS BIGINT) + 0);
Summary of Requirements
1. Version: Ensure you are on StarRocks 3.3.3 or higher. Many bugs related to generated column pruning were fixed in this release (Ref: Issue #54543).
2. Types: As mentioned before, ensure partition_id is BIGINT and you use 256L in the DDL.
3. Variables: Ensure the following session variable is enabled (it is by default):
sql
SET enable_predicate_propagation = true;
If you provide your StarRocks version (SELECT version();), I can give you the exact compatibility status for this optimization.
References
• sql-reference/sql-statements/generated_columns.md
• table_design/data_distribution/expression_partitioning.md
• release_notes/release-3.3.md
References
• sql-reference/sql-statements/generated_columns.md
• release_notes/release-3.3.md
• table_design/data_distribution/expression_partitioning.md
• faq/Sql_faq.mdRocky
05/15/2026, 9:47 PMzbarry
05/15/2026, 9:53 PMenable_predicate_move_around set to true and enable_predicate_reorder set to false, but no enable_predicate_propagation. the version I'm on is 3.5.14
also the "wrap" trick didn't work the predicate became 29: partition_id = 29: partition_id + 0Rocky
05/15/2026, 9:54 PMexplain result partition_id = partition_id (or partition_id = partition_id + 0) confirms that the optimizer recognizes the formula in your View matches the definition of your generated column. To "simplify" the query, it replaces the calculation with the column name itself.
* Original: WHERE partition_id = pmod(xx_hash3_64(customer_id), 256)
* Rewritten: WHERE partition_id = partition_id
Since partition_id = partition_id is always true, it provides no constant value for the partition pruner to use, and pruning fails.
The Solution for StarRocks 3.5+
In version 3.5.x, you should not need the "Smart View" trick at all. StarRocks now supports Predicate Derivation. If you have a generated column used for partitioning, the optimizer can automatically "derive" the partition filter from the base column filter.
1. The Correct Table DDL
Ensure you use the L suffix for the 256 literal to force BIGINT and avoid the DECIMAL cast you saw earlier.
sql
CREATE TABLE customers (
`customer_id` varchar(256),
`event_id` varchar(256),
`p_id` BIGINT AS pmod(xx_hash3_64(`customer_id`), 256L)
) ENGINE=OLAP
PRIMARY KEY(`customer_id`, `event_id`)
PARTITION BY (`p_id`)
DISTRIBUTED BY HASH(`customer_id`) BUCKETS 32;
2. The Correct View (Simplest is best)
Do not add the manual equality check. Let the optimizer handle the derivation.
sql
CREATE OR REPLACE VIEW customers_view AS
SELECT * FROM customers;
3. Enable Predicate Derivation
The variable you were looking for is likely enable_deriving_constraints_from_partition_columns or simply ensuring that the optimizer's rewrite doesn't interfere. In 3.5.14, try setting this session variable if pruning doesn't happen automatically:
sql
SET enable_predicate_reorder = true;
-- This helps the optimizer move the constants into the pruner
If automatic pruning still doesn't work
If EXPLAIN SELECT * FROM customers_view WHERE customer_id = 'abc' still shows all partitions, it means xx_hash3_64 is not yet supported for automatic derivation in the 3.5.x planner.
In that case, to "break" the optimizer's rewrite and force the calculation (the "Smart View" logic), you must use an expression that the optimizer cannot map back to the column definition:
sql
CREATE OR REPLACE VIEW customers_view AS
SELECT *
FROM customers
-- Using a non-intrusive function like 'abs' or 'coalesce' often breaks the DDL match
-- but still allows constant folding
WHERE p_id = CAST(pmod(xx_hash3_64(customer_id), 256L) AS BIGINT)
AND p_id IS NOT NULL; -- This extra check often prevents the rewrite
Verification
Check the PREDICATES in your EXPLAIN. You want to see:
* Good: p_id = <literal_number>
* Bad: p_id = p_id
* Bad: p_id = pmod(...) (This means constant folding failed)
Note on `xx_hash3_64`: This function is relatively new (v3.2+). If the optimizer refuses to fold it, it is a limitation of the current version's constant folder. In that case, you must calculate the p_id in your application and pass it explicitly: WHERE customer_id = 'abc' AND p_id = 123.
References
• sql-reference/sql-statements/generated_columns.md
• using_starrocks/async_mv/use_cases/query_rewrite_with_materialized_views.md
• sql-reference/System_variable.mdzbarry
05/15/2026, 9:59 PMp_id IS NOT NULL check didn't fix it, but also the enable_deriving_constants_from_partition_columns isn't available - mysql error: Unknown system variable 'enable_deriving_constraints_from_partition_columns', the most similar variables are {'enable_connector_async_list_partitions', 'enable_rewrite_partition_column_minmax', 'enable_partition_column_value_only_optimization'}Rocky
05/15/2026, 10:00 PMexplain output you provided, the issue is a conflict between a newer optimizer feature called Generated Column Rewrite and your "Smart View" trick.
What is happening
1. The Rewrite Conflict: StarRocks 3.5 automatically recognizes that pmod(xx_hash3_64(customer_id), 256) is the exact definition of your partition_id column. To "simplify" the plan, the optimizer rewrites the View's filter to:
WHERE partition_id = partition_id
This is why you see 29: partition_id = 29: partition_id in your explain. Since this is a tautology (it's always true), the constant folder never calculates a number (like 42), and the partition pruner sees no specific partition to filter on.
2. The "Silent Killer" Cast: You previously noted a CAST(... AS DECIMAL128) in your explain. If the optimizer sees a type mismatch (even if it's implicit), it often disables partition pruning. pmod results can sometimes be promoted to DECIMAL if the literal 256 is not explicitly typed as a BIGINT.
How to fix it in 3.5.14
To get partition pruning working, you must break the pattern match that triggers the rewrite, while ensuring the types remain strictly BIGINT.
1. Use the "Breaker" Trick in the View
Modify the View to use an expression that is functionally identical but doesn't "match" the column definition exactly. This prevents the optimizer from replacing it with the column name:
sql
CREATE OR REPLACE VIEW customers_view AS
SELECT *
FROM customers
-- Adding + 0 and an extra check prevents the Generated Column Rewrite
-- while still allowing the constant folder to run.
WHERE partition_id = (CAST(pmod(xx_hash3_64(customer_id), 256L) AS BIGINT) + 0);
Note: If + 0 is still being optimized away, try COALESCE(..., -1) where the fallback is a value that doesn't exist in your partitions.
2. Strict Typing in the DDL
Re-create the table (if possible) or ensure the generated column definition uses the L suffix for the literal. This prevents the promotion to `DECIMAL128`:
sql
ALTER TABLE customers MODIFY COLUMN `partition_id`
BIGINT AS pmod(xx_hash3_64(customer_id), 256L);
3. Disable the Rewrite (Session Variable)
In some 3.5.x versions, you can explicitly disable the rule that is undoing your trick. Try setting this before running your query:
sql
SET enable_rewrite_expression_by_generated_column = false;
If this variable returns an "Unknown variable" error, stick to the "Breaker" trick in Step 1.
Verification
Run EXPLAIN SELECT * FROM customers_view WHERE customer_id = 'abc';.
* Success: You should see partitions=1/256 and the PREDICATES should show partition_id = <some_number>.
* Failure: If you see partition_id = partition_id, the rewrite is still happening. Use a more complex breaker like ABS(pmod(...)) or (pmod(...) | 0).
Why not automatic? While StarRocks 3.5 has "Predicate Propagation," it generally propagates direct equalities (like a=b). It does not currently "solve" the math of a hash function to derive a partition key from a base column automatically; hence the View trick remains necessary.
References
• sql-reference/sql-statements/generated_columns.md
• release_notes/release-3.3.md
• table_design/data_distribution/expression_partitioning.md
• sql-reference/sql-functions/hash-functions/xx_hash3_64.md
References
• sql-reference/sql-statements/generated_columns.md
• sql-reference/System_variable.mdzbarry
05/15/2026, 10:03 PMxx_hash3_64 something I could contribute to open source? is there a file you can point me to that shows which functions are supported?Rocky
05/15/2026, 10:04 PMpartition_id = partition_id is that while xx_hash3_64 is a registered function, the StarRocks optimizer doesn't yet have a Predicate Derivation rule that can "reverse" or "propagate" the logic from your base column to the generated partition column for this specific hash function.
For functions like date_trunc, the optimizer has hardcoded knowledge that if dt = '2023-01-01', it can derive date_trunc('day', dt) = '2023-01-01'. For xx_hash3_64, that derivation logic is likely missing or incomplete.
Where to look in the Source Code
The StarRocks Frontend (FE) is written in Java. Here are the key files and packages involved in this logic:
1. Function Metadata: FunctionSet.java
* Path: fe/fe-core/src/main/java/com/starrocks/catalog/FunctionSet.java
* Role: This is where xx_hash3_64 is registered. To be used for partition pruning, a function must be marked as deterministic. You can check here if it's missing any specific flags that allow the constant folder to evaluate it during the optimization phase.
2. Predicate Derivation: DerivePredicateRule.java (and similar)
* Path: fe/fe-core/src/main/java/com/starrocks/sql/optimizer/rule/transformation/DerivePredicateRule.java
* Role: This rule is responsible for "Equivalent Predicate Derivation." It sees A = B and A = constant, then derives B = constant. For generated columns, the optimizer needs to look at the expression p_id = hash(id) and, when it sees id = 'abc', derive p_id = hash('abc').
3. Generated Column Rewrite: RewriteExpressionByGeneratedColumnRule.java
* Path: fe/fe-core/src/main/java/com/starrocks/sql/optimizer/rule/transformation/materialized/RewriteExpressionByGeneratedColumnRule.java
* Role: This is the rule currently turning your view's logic into p_id = p_id. It recognizes the expression matches the column and replaces it. To support pruning, this rule (or a companion rule) needs to ensure that the literal value is preserved or recalculated.
4. The Partition Pruning Engine: PartitionPruner.java
* Path: fe/fe-core/src/main/java/com/starrocks/planner/PartitionPruner.java
* Role: This is the final stage that takes the predicates and decides which partitions to skip. If the derivation in the steps above fails, this class will never see a constant value to prune with.
How to approach the contribution
1. Reproduce in a Unit Test: Look at fe/fe-core/src/test/java/com/starrocks/sql/optimizer/rule/ for existing tests related to generated columns or partition pruning. Add a test case that fails to prune with xx_hash3_64.
2. Debug the Rule: Trace why DerivePredicateRule or the constant folder is skipping the xx_hash3_64 expression. It often comes down to the function not being recognized as "foldable" in the context of predicate propagation.
3. Check for Type Mismatches: As you saw with the DECIMAL128 issue, ensure the derivation logic correctly handles the BIGINT (Long) return type of the hash function.
If you decide to open a PR, you can tag members of the Optimizer/Planner team in the StarRocks GitHub (e.g., contributors who recently touched the files above). This kind of performance optimization is highly valued by the community!zbarry
05/15/2026, 10:05 PMcustomer_id.
Definition:
sql
PREPARE customer_lookup FROM
'SELECT * FROM my_table WHERE customer_id = ? AND p_id = pmod(xx_hash3_64(?), 256)';
Execution:
The client only needs to pass the ID twice (or the driver may handle it):
sql
SET @uid = 'user_123';
EXECUTE customer_lookup USING @uid, @uid;
Even though the client still provides the value twice, the logic (the hash function and the reference to p_id) is hidden within the prepared statement on the StarRocks FE. The FE will evaluate the pmod function during the execution phase and prune partitions accordingly.Rocky
05/15/2026, 10:05 PMp_id = pmod(xx_hash3_64(customer_id), ...) and says "I know p_id is defined as that exact expression, so I'll just simplify this to p_id = p_id." This "helpfulness" effectively kills the constant value needed for pruning.
In a Prepared Statement:
1. Preparation Phase: You write p_id = pmod(xx_hash3_64(?), 256).
2. The optimizer sees a placeholder (?), not the column customer_id.
3. Because the expression contains a placeholder, the "Generated Column Rewrite" rule cannot match it against the DDL (which specifically requires the base column reference).
4. Execution Phase: When you run EXECUTE ... USING 'abc', 'abc', the FE replaces the ? with the literal 'abc'.
5. The Constant Folder then evaluates the expression to a single number (e.g., 42), and the Partition Pruner uses that number to skip data.
Contributing to Open Source
Yes! Adding support for xx_hash3_64 to the Predicate Derivation engine would be a high-impact contribution. Currently, the optimizer knows how to derive predicates for simple functions (like date_trunc), but it doesn't have a rule to "derive" the value of a generated column from its base column when a hash function is involved.
Here are the files you should explore:
1. Function Determinism: FunctionSet.java
* Path: fe/fe-core/src/main/java/com/starrocks/catalog/FunctionSet.java
* Check: Ensure xx_hash3_64 is marked as deterministic. If it's not, the optimizer will never fold it into a constant during planning.
2. Predicate Derivation: DerivePredicateRule.java
* Path: fe/fe-core/src/main/java/com/starrocks/sql/optimizer/rule/transformation/DerivePredicateRule.java
* Role: This is the "brain" for predicate propagation. It's responsible for seeing customer_id = 'abc' and inferring that any expression involving customer_id can also be turned into a constant. You would look for how it handles generated columns or "Equivalent Predicate Derivation."
3. Constant Folding: ScalarOperatorRewriter.java
* Path: fe/fe-core/src/main/java/com/starrocks/sql/optimizer/operator/scalar/ScalarOperatorRewriter.java
* Role: This manages the actual evaluation of expressions. If xx_hash3_64 isn't being folded even when the input is a literal, the logic to trigger the FE-side implementation of this function might be missing from the rewriter's constant folding rules.
4. Rule for Generated Columns: RewriteExpressionByGeneratedColumnRule.java
* Path: fe/fe-core/src/main/java/com/starrocks/sql/optimizer/rule/transformation/materialized/RewriteExpressionByGeneratedColumnRule.java
* Role: This is the rule that "broke" your View by turning the filter into p_id = p_id. Improving this rule to ensure it only rewrites when it won't destroy a prunable constant is another area for contribution.
Recommended Next Step
If you want to try the contribution, start by adding a Unit Test in fe/fe-core/src/test/java/com/starrocks/sql/optimizer/rule/ that specifically tries to prune a table partitioned by xx_hash3_64. Debugging that test will lead you directly to where the logic falls short.
For now, the Prepared Statement is likely your cleanest path to production performance without having to wait for a code change.
References
• sql-reference/sql-statements/prepared_statement.md
• using_starrocks/caching/query_cache.mdzbarry
05/15/2026, 10:06 PMRocky
05/15/2026, 10:07 PMcustomer_id) during the initial rewrite phase, which stops the "Generated Column Rewrite" rule from turning your filter into the useless p_id = p_id.
Here is how to implement it and how your client code would change.
1. The Implementation (SQL Level)
To test this in your MySQL terminal, you would use the following syntax. Note that we must provide the customer_id *twice*—once for the actual data filter and once for the partition calculation.
sql
-- 1. Prepare the statement with placeholders (?)
PREPARE customer_lookup FROM
'SELECT * FROM customers
WHERE customer_id = ?
AND partition_id = CAST(pmod(xx_hash3_64(?), 256) AS BIGINT)';
-- 2. Set the ID variable
SET @cid = 'user_123';
-- 3. Execute passing the variable twice
EXECUTE customer_lookup USING @cid, @cid;
2. Client-Side Changes
Your client application code must change, but only slightly. Most modern database drivers (JDBC, Python, Go) handle the "Prepare" and "Execute" steps for you if you use their parameter-binding syntax.
Java (JDBC)
Ensure your connection string includes useServerPrepStmts=true.
java
String sql = "SELECT * FROM customers WHERE customer_id = ? AND partition_id = CAST(pmod(xx_hash3_64(?), 256) AS BIGINT)";
PreparedStatement pstmt = conn.prepareStatement(sql);
// You must bind the SAME value to both parameters
pstmt.setString(1, "user_123");
pstmt.setString(2, "user_123");
ResultSet rs = pstmt.executeQuery();
Python (mysql-connector)
python
query = """
SELECT * FROM customers
WHERE customer_id = %s
AND partition_id = CAST(pmod(xx_hash3_64(%s), 256) AS BIGINT)
"""
# Pass the customer_id twice in the tuple
cursor.execute(query, ("user_123", "user_123"))
3. Why this fixes the Pruning Issue
In StarRocks 3.5, the "Generated Column Rewrite" rule is very aggressive.
• The View Problem: The optimizer sees pmod(xx_hash3_64(customer_id), ...) and says "Hey, that expression matches the definition of partition_id! I'll replace the whole thing with the column name." Result: WHERE partition_id = partition_id.
• The Prepared Statement Fix: The optimizer sees pmod(xx_hash3_64(?), ...). Because it's a placeholder (?) and not the column name, the rewrite rule does not match. The expression is left alone.
• The Execution Phase: When you run EXECUTE, the FE replaces ? with 'user_123'. The Constant Folder immediately calculates pmod(xx_hash3_64('user_123'), 256) which results in a constant (e.g., 42). The Partition Pruner now sees partition_id = 42 and skips all other partitions.
4. How to verify
Run the PREPARE and SET steps as shown in Step 1, then run an EXPLAIN on the EXECUTE command:
sql
EXPLAIN EXECUTE customer_lookup USING @cid, @cid;
Look for:
1. partitions=1/256: This confirms pruning worked.
2. PREDICATES: 29: partition_id = 42: This confirms the constant was successfully derived from the hash function.
Summary of Pros/Cons
• Pros: Logic stays on the server (client doesn't need a hash library); bypasses the optimizer bug; prevents SQL injection.
• Cons: Client must send the value twice (minor bandwidth/code change).
If you are comfortable with this change, it is the most stable "non-code-fix" way to get sub-millisecond point lookups on large tables in v3.5.x.
References
• sql-reference/sql-statements/prepared_statement.md
• sql-reference/data-types/string-type/BINARY.md
• table_design/hybrid_table.md