agentsclimarketplace

Starrocks join optimization

Skill ivanshamaev/de-agent-skills/group_skills/starrocks_group_skills/starrocks_join_optimization

Профессиональные Data Engineering Agent Skills для разработки AI Agentic Data Platform

Install
npx -y skills add ivanshamaev/de-agent-skills --skill starrocks_join_optimization

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

StarRocks join optimization — broadcast join (small dimension to all BEs), shuffle/hash join (repartition both sides), colocate join (no network transfer, same distribution key), bucket shuffle join, nested loop join (cross join/non-equi), join strategy selection rules, runtime filter effect on joins, join hints, EXPLAIN join strategy verification, skew join handling

SKILL.md

28.2 KB, ~7.1k tokens by cl100k_base, as published. Nobody here has run it

StarRocks Join Optimization

When to Use

Load this skill when the user needs to:

  • Diagnose slow join queries in StarRocks (high network traffic, OOM, skewed execution)
  • Choose or force a join strategy (broadcast, shuffle, colocate, bucket shuffle)
  • Set up colocate join groups to eliminate network shuffle between frequently joined tables
  • Understand and tune runtime filters for join probe-side scan reduction
  • Read and interpret EXPLAIN output to verify join strategy selection
  • Handle data skew in join keys causing uneven BE workloads
  • Control join order and join reorder via hints or session variables
  • Troubleshoot wrong strategy caused by stale or missing table statistics

Join Strategies Overview

StrategyWhen StarRocks Uses ItNetwork CostMemory RequirementEXPLAIN Keyword
BroadcastBuild side row count < broadcast_row_limitLow — build side sent once to all BEsMust fit build side in each BE memoryBROADCAST in HashJoinNode
Shuffle (Hash Partition)Both tables large, no colocation or bucket matchHigh — both sides repartitioned by hashDistributed across all BEsPARTITIONED in ExchangeNode
ColocateBoth tables in same colocate group, same distribution key and bucket countZero — no data movementMinimal — local join onlyCOLOCATE in HashJoinNode
Bucket ShuffleJoin key matches distribution key of one sideLow — only one side redistributedModerate — one side local, other shuffled to matchBUCKET_SHUFFLE in ExchangeNode
Nested LoopNon-equi predicate, CROSS JOIN, cartesian productVery high if largeVery high — O(N*M)NESTLOOP in NestLoopJoinNode

Broadcast Join

How It Works

The build side (smaller table) is fully replicated to every BE. Each BE then probes the local copy using its local fragment of the probe side. No probe-side rows cross the network.

FE plan:
  probe side (large fact) → each BE scans local tablets
  build side (small dim)  → broadcast to ALL BEs
  each BE: hash_join(local_probe, in-memory_build)

Threshold: broadcast_row_limit

The optimizer broadcasts the build side only when its estimated row count is below the session variable broadcast_row_limit (default 15,000,000 rows). This is a row count limit, not a byte limit — set it based on expected row width.

-- Lower threshold to force shuffle for moderately sized dimension tables
SET broadcast_row_limit = 5000000;

-- Raise threshold when small dimension tables exceed default row count
-- but still fit in BE memory (e.g., wide dimension with few columns selected)
SET broadcast_row_limit = 30000000;

The optimizer also respects max_broadcast_row_num as an absolute safety cap.

EXPLAIN Identification

EXPLAIN SELECT f.order_id, d.region
FROM   orders f
JOIN   regions d ON f.region_id = d.region_id;

Look for BROADCAST in the join node:

4:HASH JOIN
|  join op: INNER JOIN (BROADCAST)
|  build runtime filters:
|  - filter_id = 0, build_expr = (d.region_id), remote = false
|  hash predicates:
|  - f.region_id = d.region_id
|  ...
3:EXCHANGE
   distribution type: BROADCAST

Best Practices

  • Ideal for dimension tables under 100 MB build side (after projection/filter pushdown)
  • Confirm row count estimate is accurate: run ANALYZE TABLE regions if stats are stale
  • If BE memory is constrained, lower broadcast_row_limit to prevent OOM

Hint Syntax

SELECT /*+ JOIN(regions BROADCAST) */
       f.order_id, d.region
FROM   orders f
JOIN   regions d ON f.region_id = d.region_id;

The hint table name refers to the build-side table. The hint overrides the optimizer's strategy; use it only when you are certain about table sizes.


Shuffle (Hash Partition) Join

How It Works

Both the build side and the probe side are repartitioned across all BEs using the same hash function on the join key. After shuffling, each BE performs a local hash join on its slice of both inputs.

FE plan:
  build side → HASH partition by join_key → each BE receives 1/N of rows
  probe side → HASH partition by join_key → each BE receives matching 1/N of rows
  each BE: local hash_join

Network Cost

Shuffle is the most expensive strategy in terms of network I/O because both sides cross the network. For a 1 TB fact-to-fact join, expect significant cluster-internal traffic. Ensure your network fabric is not the bottleneck.

EXPLAIN Identification

EXPLAIN SELECT a.user_id, b.event_type, SUM(a.revenue)
FROM   daily_sales a
JOIN   user_events b ON a.user_id = b.user_id
GROUP BY 1, 2;

Look for PARTITIONED ExchangeNode on both sides:

5:HASH JOIN
|  join op: INNER JOIN (PARTITIONED)
|  ...
4:EXCHANGE
|  distribution type: SHUFFLE
|  partition exprs: [b.user_id]
...
2:EXCHANGE
   distribution type: SHUFFLE
   partition exprs: [a.user_id]

Best Practices

  • Accepted fallback when neither colocate nor bucket shuffle applies
  • If used repeatedly for the same table pair, consider promoting to colocate group
  • Ensure join key columns have high cardinality to avoid post-shuffle skew
  • For very large inputs, enable enable_spill = true to spill hash tables to disk and avoid OOM

Hint Syntax

SELECT /*+ JOIN(daily_sales SHUFFLE) */
       a.user_id, b.event_type
FROM   daily_sales a
JOIN   user_events b ON a.user_id = b.user_id;

Colocate Join

How It Works

When two tables belong to the same colocate group and share the same distribution key, bucket count, and replica count, rows with the same join key reside in the same tablet on the same BE. The FE generates a plan where each BE joins its local tablets — zero network transfer.

BE_1: tablet_0(orders) JOIN tablet_0(order_items) → local hash join
BE_2: tablet_1(orders) JOIN tablet_1(order_items) → local hash join
...  no data crosses BEs

Setup

Both tables must be created with the same colocate_with group name, the same distribution key column(s), and the same bucket count:

CREATE TABLE orders (
    order_id   BIGINT   NOT NULL,
    user_id    BIGINT   NOT NULL,
    amount     DECIMAL(18, 2),
    created_at DATETIME
)
ENGINE = OLAP
DUPLICATE KEY(order_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
    "replication_num" = "3",
    "colocate_with"   = "order_group"
);

CREATE TABLE order_items (
    order_id    BIGINT   NOT NULL,
    item_id     BIGINT   NOT NULL,
    sku_id      BIGINT,
    quantity    INT,
    unit_price  DECIMAL(18, 2)
)
ENGINE = OLAP
DUPLICATE KEY(order_id, item_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32   -- same key, same bucket count
PROPERTIES (
    "replication_num" = "3",
    "colocate_with"   = "order_group"      -- same group
);

Key requirements (all three must match exactly):

RequirementDetail
colocate_with valueIdentical string in both tables' PROPERTIES
Distribution keySame column(s) in same order
Bucket countIdentical BUCKETS N
Replica countIdentical replication_num

Verifying Group Status

SHOW PROC '/colocation_group';

Example output:

+------------------+--------------------+-----------+---------+-----------+
| GroupId          | TableIds           | BucketsNum| ReplicaNum| IsStable |
+------------------+--------------------+-----------+---------+-----------+
| 10001.order_group| 10002, 10003       | 32        | 3         | true     |
+------------------+--------------------+-----------+---------+-----------+

IsStable = true is required for colocate join to be used. If IsStable = false, StarRocks falls back to shuffle. Instability happens during BE failure, tablet migration, or bucket count mismatch after ALTER TABLE.

Session Variable

SET enable_colocate_join = true;   -- default is true; set false only for debugging

EXPLAIN Identification

EXPLAIN SELECT o.order_id, oi.sku_id, oi.quantity
FROM   orders o
JOIN   order_items oi ON o.user_id = oi.user_id;
3:HASH JOIN
|  join op: INNER JOIN (COLOCATE)
|  hash predicates:
|  - o.user_id = oi.user_id
|  ...
2:OlapScanNode  -- orders tablets (local)
1:OlapScanNode  -- order_items tablets (local)
-- NOTE: no ExchangeNode between scan and join

The absence of an ExchangeNode between the OlapScanNodes and the HashJoinNode is the definitive indicator of colocate execution.

Altering Bucket Count (Breaks Colocation Temporarily)

ALTER TABLE orders
    DISTRIBUTED BY HASH(user_id) BUCKETS 64;
-- Must also alter order_items to the same bucket count
-- Group becomes IsStable=false until both tables match and tablets are rebalanced
ALTER TABLE order_items
    DISTRIBUTED BY HASH(user_id) BUCKETS 64;

After both ALTERs, wait for SHOW PROC '/colocation_group' to show IsStable = true before expecting colocate joins.


Bucket Shuffle Join

How It Works

Bucket shuffle is a middle-ground strategy between colocate (zero network) and full shuffle (both sides moved). When the join key matches the distribution key of one table, that table's data is already partitioned by the join key. Only the other side is repartitioned to match the existing bucket layout.

probe side (orders, HASH(user_id)) → already distributed by user_id → no movement
build side (sessions)              → redistribute to match orders' bucket layout
each BE: local hash join on its slice

When It Activates

StarRocks activates bucket shuffle automatically when:

  1. The equi-join predicate includes the full distribution key of at least one table
  2. The tables are not in the same colocate group (otherwise colocate wins)
  3. The non-distribution-key side's estimated size makes full broadcast too large

EXPLAIN Identification

EXPLAIN SELECT o.order_id, s.session_duration
FROM   orders o                          -- DISTRIBUTED BY HASH(user_id)
JOIN   sessions s ON o.user_id = s.user_id;
4:HASH JOIN
|  join op: INNER JOIN (BUCKET_SHUFFLE)
|  ...
3:EXCHANGE
|  distribution type: BUCKET_SHUFFLE
|  partition exprs: [s.user_id]
|  -- sessions side is redistributed to match orders' bucket layout
2:OlapScanNode  -- orders scanned locally, no exchange needed

Limitations

  • Bucket shuffle requires the join key to be exactly the distribution key (no subset, no extra columns)
  • Does not apply across different bucket counts unless StarRocks can compute a compatible mapping
  • Falls back to shuffle if bucket mapping is incompatible

Nested Loop Join (Cross Join / Non-Equi)

How It Works

For queries without equi-join predicates — including CROSS JOIN, range predicates (a.val BETWEEN b.lo AND b.hi), and inequality predicates (a.ts > b.start_ts) — StarRocks uses a nested loop join. Every row of the outer (probe) side is compared against every row of the inner (build) side: O(N * M) complexity.

EXPLAIN Identification

EXPLAIN SELECT a.event_id, b.campaign_id
FROM   events a
CROSS JOIN campaigns b;
3:NESTLOOP JOIN
|  join op: CROSS JOIN
|  colocate: false, reason: cross join not support colocate
|  ...
2:OlapScanNode  -- campaigns (build, inner)
1:OlapScanNode  -- events (probe, outer)

Danger Signals

Any NESTLOOP JOIN on tables with more than a few thousand rows is a potential performance catastrophe. Alert immediately if EXPLAIN shows NESTLOOP on large tables and investigate whether:

  1. The join predicate can be rewritten as an equi-join (a.region_id = b.region_id) with the range filter moved to a WHERE clause
  2. The non-equi condition can be approximated with a bucketed range approach
  3. A CROSS JOIN is accidental (missing ON clause)

Hint Syntax

-- Force nested loop (useful for tiny tables with complex predicates)
SELECT /*+ USE_NL(a b) */
       a.event_id, b.label
FROM   events a, event_labels b
WHERE  a.event_ts BETWEEN b.start_ts AND b.end_ts;

Only use USE_NL when the build side is guaranteed tiny (hundreds of rows).


Join Strategy Selection Logic

The optimizer follows this decision flow for each equi-join:

1. COLOCATE CHECK
   Both tables in same colocate group?
   Group IsStable = true?
   enable_colocate_join = true?
     YES → use COLOCATE JOIN  (zero network cost)
     NO  → continue

2. BROADCAST SIZE CHECK
   Estimated build-side row count < broadcast_row_limit?
   Estimated build-side bytes < max_broadcast_row_num threshold?
     YES → use BROADCAST JOIN  (build side sent to all BEs)
     NO  → continue

3. BUCKET SHUFFLE CHECK
   Join key == distribution key of one table?
   Bucket mapping is compatible?
     YES → use BUCKET SHUFFLE JOIN  (one side redistributed)
     NO  → continue

4. SHUFFLE FALLBACK
   Use SHUFFLE (PARTITIONED) JOIN  (both sides repartitioned by hash)

The optimizer can override this flow when join hints are present. When statistics are stale, size estimates may be wrong and the optimizer may pick the wrong strategy — always run ANALYZE TABLE on tables involved in slow joins.


Runtime Filters on Joins

Concept

When the optimizer builds a hash table for the build side, it simultaneously generates a compact runtime filter (Bloom filter, IN list, or min/max range) from the build-side join key values. This filter is pushed down to the probe-side scan, allowing the storage layer to skip data blocks that cannot possibly match.

Build phase:  scan build table → build hash table → generate Bloom filter(join_key)
              → push filter to all probe-side scan nodes (possibly cross-network: "remote=true")
Probe phase:  scan probe table → apply Bloom filter per data block → skip non-matching blocks
              → probe surviving rows against hash table

The effect is a dramatic reduction in rows read from disk on the probe side, especially when the build side is highly selective.

EXPLAIN VERBOSE Identification

Use EXPLAIN VERBOSE to see filter node IDs:

EXPLAIN VERBOSE SELECT f.order_id, d.region
FROM   orders f
JOIN   regions d ON f.region_id = d.region_id
WHERE  d.country = 'US';
4:HASH JOIN
|  join op: INNER JOIN (BROADCAST)
|  build runtime filters:
|  - filter_id = 0, build_expr = (d.region_id), remote = false
|    type: BLOOM_FILTER
|  hash predicates:
|  - f.region_id = d.region_id
|  ...

2:OlapScanNode (orders)
   TABLE: orders
   runtime filters: 0[bloom] <- d.region_id
   -- filter_id=0 is applied here at scan time

The runtime filters: 0[bloom] <- d.region_id line confirms the filter is pushed down to the probe-side tablet scan. Rows that the Bloom filter eliminates are never read from disk.

Session Variables for Runtime Filters

-- Master switch (default: true)
SET enable_runtime_filter = true;

-- Filter types: BLOOM_FILTER, IN, MIN_MAX, IN_OR_BLOOM (default: IN_OR_BLOOM)
SET runtime_filter_type = 'IN_OR_BLOOM';

-- Maximum IN-list size before switching to Bloom filter (default: 1024)
SET runtime_filter_max_in_num = 1024;

-- Bloom filter size in bytes (0 = auto-size based on NDV estimate)
SET runtime_bloom_filter_size = 0;

-- Minimum row count threshold to generate a runtime filter
SET runtime_filter_scan_wait_time = 20000;  -- ms probe scan waits for filter

Filter Types Guidance

TypeBest ForLimitation
INLow NDV build side (< 1024 distinct values)Does not scale to high-NDV keys
BLOOM_FILTERHigh-NDV equi-join keys (user_id, order_id)Small false positive rate
MIN_MAXRange joins or monotonic keys (timestamps)Only effective for clustered probe data
IN_OR_BLOOMGeneral purpose — auto-selects based on NDVDefault; rarely needs to be changed

Cross-Exchange (Remote) Filters

For shuffle joins, the runtime filter must cross the ExchangeNode to reach the probe-side scan. EXPLAIN shows remote = true for such filters. Remote filters add a small synchronization overhead but are generally worth it for selective build sides.


Join Skew Handling

Detecting Skew

In a uniform shuffle join, each BE processes roughly the same number of rows. Skew occurs when one or more join key values are vastly over-represented (e.g., user_id = -1 representing anonymous users). Symptoms:

  • Query profile shows some BE instances finishing 10x faster than others
  • One or two fragment instances have operator input/output row counts orders of magnitude larger
  • OOM or spill only on specific BE nodes

Access the query profile via the StarRocks WebUI (http://<fe-host>:8030) → Query History → select query → Profile.

In the profile, look for:

HASH_JOIN_NODE (id=4)
  - RowsReturned: 980,000,000   ← one instance
  - RowsReturned: 1,200,000     ← typical instance

Salt Key Approach (Manual)

Manually salt the skewed join key by appending a random integer suffix [0, N) to the high-frequency values on the probe side and replicating the corresponding build-side rows N times.

-- Step 1: Identify skewed key values
SELECT user_id, COUNT(*) AS cnt
FROM   orders
GROUP BY user_id
ORDER BY cnt DESC
LIMIT 10;
-- Suppose user_id = -1 has 500M rows (skewed)

-- Step 2: Create salted version of the fact table (or CTE)
WITH salted_orders AS (
    SELECT
        order_id,
        CASE
            WHEN user_id = -1
                THEN CONCAT(CAST(user_id AS VARCHAR), '_', CAST(FLOOR(RAND() * 10) AS VARCHAR))
            ELSE CAST(user_id AS VARCHAR)
        END AS user_id_salt,
        amount,
        created_at
    FROM orders
),

-- Step 3: Explode the dimension table for the skewed key value
exploded_users AS (
    SELECT
        CONCAT(CAST(user_id AS VARCHAR), '_', CAST(salt AS VARCHAR)) AS user_id_salt,
        region,
        user_type
    FROM users
    CROSS JOIN (SELECT SEQUENCE(0, 9) AS arr) t
    LATERAL VIEW explode(arr) e AS salt
    WHERE user_id = -1

    UNION ALL

    SELECT CAST(user_id AS VARCHAR) AS user_id_salt, region, user_type
    FROM users
    WHERE user_id != -1
)

-- Step 4: Join on salted key
SELECT so.order_id, eu.region, eu.user_type, so.amount
FROM   salted_orders so
JOIN   exploded_users eu ON so.user_id_salt = eu.user_id_salt;

This distributes the skewed key's rows evenly across 10 hash buckets instead of piling them on one BE.

Skew Join Hint (StarRocks 3.x)

StarRocks 3.x does not expose a declarative SKEW_JOIN hint as of the 3.3 release. Use the manual salting pattern above for production workloads. Monitor future releases for native skew join support.

Preventing Skew at Design Time

  • Avoid using sentinel values (NULL, -1, 0, 'UNKNOWN') as join keys; filter them out before joining
  • Distribute tables by a composite key when a single column has a long-tail distribution
  • For star-schema joins on skewed dimension keys, use broadcast join to eliminate hash-based skew entirely

Multi-Table Join Order

CBO Join Reorder

StarRocks Cost-Based Optimizer (CBO) automatically reorders joins to minimize intermediate result set sizes. The CBO considers:

  • Table row counts and NDV from statistics (collected by ANALYZE TABLE)
  • Filter selectivity
  • Join type (inner/left outer/semi/anti)

The maximum number of tables the CBO will consider for reorder is controlled by:

SET cbo_max_reorder_node = 4;   -- default: 4, increase for complex multi-join queries

For queries with more than cbo_max_reorder_node tables, the CBO uses the original SQL join order beyond the threshold. Increase carefully — reorder search space is factorial.

Enable/Disable Reorder

SET enable_join_reorder = true;   -- default: true
-- Disable only for debugging to isolate a regression introduced by reorder
SET enable_join_reorder = false;

Leading Hint

Force a specific join order when you know the CBO will make a suboptimal choice (e.g., statistics are stale and cannot be refreshed):

SELECT /*+ LEADING(date_dim store_sales item_dim) */
       dd.d_year,
       ss.ss_quantity,
       id.i_brand
FROM   store_sales ss
JOIN   date_dim    dd ON ss.sold_date_sk = dd.d_date_sk
JOIN   item_dim    id ON ss.item_sk      = id.item_sk
WHERE  dd.d_year = 2024;

The LEADING hint specifies the left-to-right build order — StarRocks builds the join tree starting from the leftmost table in the hint. Combined with individual join strategy hints:

SELECT /*+ LEADING(date_dim store_sales item_dim) JOIN(date_dim BROADCAST) JOIN(item_dim BROADCAST) */
       ...

EXPLAIN Verification Examples

Example 1: Broadcast Join

EXPLAIN SELECT o.order_id, r.region_name
FROM   orders o
JOIN   regions r ON o.region_id = r.region_id;

Expected EXPLAIN (annotated):

PLAN FRAGMENT 0
 OUTPUT EXPRS: o.order_id | r.region_name
  PARTITION: UNPARTITIONED

  4:HASH JOIN
  |  join op: INNER JOIN (BROADCAST)         ← broadcast strategy confirmed
  |  build runtime filters:
  |  - filter_id = 0, build_expr = (r.region_id), remote = false
  |  hash predicates:
  |  - o.region_id = r.region_id
  |  cardinality: 50000000
  |
  |----3:EXCHANGE                            ← build side (regions) broadcast
  |       distribution type: BROADCAST
  |       cardinality: 5000
  |
  2:OlapScanNode                             ← probe side (orders) scanned locally
     TABLE: orders
     PREDICATES: none
     runtime filters: 0[bloom] <- r.region_id  ← runtime filter applied at scan
     cardinality: 50000000

Key indicators: BROADCAST in HashJoinNode, single EXCHANGE node (build side only), runtime filters on probe scan.

Example 2: Colocate Join

EXPLAIN SELECT o.order_id, oi.sku_id, oi.quantity
FROM   orders o
JOIN   order_items oi ON o.user_id = oi.user_id;

Expected EXPLAIN:

PLAN FRAGMENT 0
 OUTPUT EXPRS: o.order_id | oi.sku_id | oi.quantity
  PARTITION: UNPARTITIONED

  5:EXCHANGE

  PLAN FRAGMENT 1
   OUTPUT EXPRS:
    PARTITION: HASH_PARTITIONED: o.user_id

    4:HASH JOIN
    |  join op: INNER JOIN (COLOCATE)        ← colocate strategy confirmed
    |  colocate: true, reason: same group
    |  build runtime filters:
    |  - filter_id = 0, build_expr = (oi.user_id), remote = false
    |  hash predicates:
    |  - o.user_id = oi.user_id
    |  cardinality: 200000000
    |
    3:OlapScanNode                           ← build side (order_items) — local scan, no EXCHANGE
       TABLE: order_items
       cardinality: 100000000
    |
    2:OlapScanNode                           ← probe side (orders) — local scan, no EXCHANGE
       TABLE: orders
       runtime filters: 0[bloom] <- oi.user_id
       cardinality: 50000000

Key indicator: no ExchangeNode between OlapScanNodes and HashJoinNode and COLOCATE label. Both scans run on the same fragment inside each BE.

Example 3: Runtime Filter Push-Down

EXPLAIN VERBOSE
SELECT f.order_id, d.city
FROM   orders f
JOIN   cities d ON f.city_id = d.city_id
WHERE  d.country_code = 'DE';

Expected EXPLAIN VERBOSE (partial):

  3:HASH JOIN
  |  join op: INNER JOIN (BROADCAST)
  |  build runtime filters:
  |  - filter_id = 0, build_expr = (d.city_id), remote = false, type = IN_OR_BLOOM
  |  equal join conjunct: f.city_id = d.city_id
  |  output columns: 1, 2
  |
  |----2:EXCHANGE
  |       distribution type: BROADCAST
  |       cardinality: 412              ← 412 cities in DE after WHERE pushdown
  |
  1:OlapScanNode
     TABLE: orders
     PREDICATES: none
     runtime filters: 0[in_or_bloom] <- d.city_id   ← filter pushed to tablet scan
     cardinality: 50000000

     -- With filter applied, actual rows scanned:
     -- orders has 50M rows; ~2% match German cities → ~1M rows actually evaluated

The runtime filter for a highly selective build side (412 rows from cities) dramatically reduces rows read from the orders table. The VERBOSE output shows filter type and target expression.


Anti-Patterns

Missing Colocate Group

Problem: Two tables that are always joined on the same key are not in the same colocate group. Every join generates a full shuffle.

Detection: EXPLAIN shows PARTITIONED instead of COLOCATE for a high-frequency join between tables with matching distribution keys.

Fix: Re-create both tables with "colocate_with" = "same_group_name" and identical distribution key + bucket count. Plan for a one-time data reload.


Broadcasting a Large Table

Problem: Statistics are stale or missing; the optimizer underestimates the build-side row count and chooses broadcast. A large table is replicated to every BE, causing OOM or extreme memory pressure.

Detection: Query fails with Memory limit exceeded or runs very slowly. EXPLAIN shows BROADCAST on a table that actually has hundreds of millions of rows.

Fix:

  1. Run ANALYZE TABLE large_table to refresh statistics.
  2. Lower broadcast_row_limit to prevent future accidental broadcasts.
  3. If needed, use a hint to force shuffle: /*+ JOIN(large_table SHUFFLE) */.

Non-Equi Join on Large Dataset

Problem: A join uses a non-equi predicate (e.g., range overlap, !=, <), triggering nested loop join on large tables.

Detection: EXPLAIN shows NESTLOOP JOIN. Profile shows very high RowsReturned on the NestLoopJoinNode instance.

Fix:

  • Rewrite as equi-join + post-filter WHERE clause where possible.
  • Pre-filter one side to a small subset before the nested loop.
  • Use a range bucketing approach: bucket both sides by a range key, then join only within-bucket pairs using equi-join on the bucket column plus a post-filter.

Stale Statistics Causing Wrong Strategy

Problem: The CBO selects broadcast when it should shuffle, or shuffles when colocate is available, because table row counts or NDV are outdated.

Detection:

SHOW TABLE STATUS FROM my_db LIKE 'orders';
-- Check Update_time vs last ANALYZE time

Or query the information_schema:

SELECT table_name, update_time, table_rows
FROM   information_schema.tables
WHERE  table_schema = 'my_db'
  AND  table_name IN ('orders', 'order_items');

Fix: Schedule regular ANALYZE TABLE runs after bulk loads. Use ANALYZE TABLE t UPDATE HISTOGRAM ON col1, col2 for skewed columns. For very large tables use sample-based analysis:

ANALYZE TABLE orders WITH SAMPLE ROWS 1000000;
-- or percent-based
ANALYZE TABLE orders WITH SAMPLE PERCENT 10;

Ignoring Colocate Group Instability

Problem: A colocate group becomes unstable (e.g., after a BE failure and tablet rebalancing) and the optimizer silently falls back to shuffle. Join performance degrades unnoticed.

Detection:

SHOW PROC '/colocation_group';
-- IsStable = false for one or more groups

Fix: Wait for tablet rebalancing to complete (monitor SHOW PROC '/tablets_distribution'). If a BE is permanently removed, update the colocate group's replication_num to match the new cluster size.


Composite Distribution Key Partial Match

Problem: A table is distributed by HASH(order_id, user_id) (composite key). A join on only user_id cannot use colocate or bucket shuffle because the full distribution key is not present.

Fix: Choose distribution keys that are always used together in the most critical joins. If multiple join patterns are needed, prefer a single high-cardinality column as the distribution key rather than a composite.


References

  • StarRocks documentation: Join Optimization
  • StarRocks documentation: Colocate Join
  • StarRocks documentation: Runtime Filter
  • StarRocks documentation: CBO Optimizer
  • StarRocks documentation: Query Hints
  • Related skill: starrocks-bucketing — distribution key selection, colocate group setup, tablet sizing
  • Related skill: starrocks-explain-plan — reading full EXPLAIN output, fragment structure, exchange nodes
  • Related skill: starrocks-query-optimizer — CBO statistics, ANALYZE TABLE, session variables

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.