Slackbot
06/30/2026, 3:43 PMRocky
06/30/2026, 3:44 PMORDER BY (Sort Key) columns match your most frequent filter and join columns. This enables Prefix Index filtering and Zone Map pruning, allowing the engine to skip reading unnecessary data.
* Data Distribution: Check if your data is skewed. If the 10M rows are mostly on one node, you aren't getting the full benefit of your 2-node cluster.
* Analyze the Profile: You can use EXPLAIN ANALYZE or the StarRocks Manager/FE UI to view the Query Profile. Look for the ScanTime and JoinTime to see exactly where the 800ms is being spent.
3. Future Performance Plans
StarRocks continues to push performance boundaries in every release:
* StarRocks 3.3+: Introduced significant optimizations for ARM architectures and Parallel Scan improvements to increase I/O throughput.
* StarRocks 4.0: This major release delivered approximately 60% faster query execution year-over-year on TPC-DS benchmarks. It also introduced 3–15x faster out-of-the-box queries for JSON data and significant improvements in metadata parsing and file bundling for data lake scenarios.
* Ongoing JIT (Just-In-Time) Compilation: The engine uses JIT for expression evaluation, which can provide a 5x improvement for queries with heavy calculations.
In summary, for complex queries involving joins and multiple steps, StarRocks is consistently one of the fastest engines available. If you find your scans are slower than expected, it is often a matter of tuning your Sort Keys or Partitioning rather than a limitation of the engine itself.
References
• knowledge/usage/best-practice.md
• using_starrocks/accelerate-cardinality-preserving-joins.mdRocky
06/30/2026, 3:44 PMORDER BY ... LIMIT 100 is being pushed down to the scan layer. You can verify this by running EXPLAIN ANALYZE <your_query>. If the "Sort" node is processing all 10 million rows before the limit, check if you are sorting on a complex expression that prevents pushdown.
5. Runtime Filters: Check if the CBO is generating Bloom Filter or Min/Max filters to prune data on the "large" side of a join based on the "small" side.
Future Performance Plans
StarRocks is continuously closing the gap in raw scan performance through several upcoming and recent initiatives:
* Global Late Materialization: This optimization (targeted in recent/upcoming releases) reduces I/O by only reading the columns needed for filters first, and only fetching the remaining columns for the rows that actually match.
* Inverted Index Enhancements: Continuous improvements to the inverted index (introduced in 3.x) allow for much faster "search-like" filtering during scans.
* Fine-grained Parallelism: StarRocks is moving toward more granular parallel execution within tablets to better utilize multi-core CPUs during the scan phase.
* StarRocks 4.0+: Recent major versions have introduced a ~60% performance improvement year-over-year on complex benchmarks like TPC-DS by further optimizing the vectorized engine and JIT (Just-In-Time) compilation.
To pinpoint exactly why your query takes 800ms, I recommend running EXPLAIN ANALYZE and looking for the operator with the highest ActiveTime. If you share the plan, I can help you optimize it further.Rocky
06/30/2026, 3:46 PMLIMIT 100 is actually quite high for StarRocks. In many production environments, StarRocks 4.1.1 handles hundreds of millions of rows in sub-100ms for similar patterns.
Addressing the "Scan Speed" Comparison
Regarding the "cursor" claim that other engines are faster at scans:
* Raw Scans vs. Complex Queries: Some engines are highly optimized for single-table raw scans. However, StarRocks is designed to be the fastest at complex SQL (Joins, Aggregations, and window functions). If your query involves joins, StarRocks' Cost-Based Optimizer (CBO) usually provides a massive advantage over engines that rely on manual join tuning or flat-table denormalization.
* Vectorized Engine: StarRocks 4.1 utilizes a fully vectorized execution engine that is competitive with any analytical engine on the market for raw scan speed, provided the data is properly indexed and partitioned.
How to get sub-100ms in StarRocks 4.1.1
Since you are on 4.1.1, you should check if you are utilizing these specific features:
1. Global Late Materialization:
StarRocks 4.x has significantly improved late materialization. If your query has selective filters and a LIMIT, ensure the engine isn't reading all columns for all 10M rows.
_ _Check:* Run EXPLAIN ANALYZE <query> and look at the Scan node. If PushdownPredicates are high, it’s working.
2. Automatic Tablet Splitting (Shared-data):
If you are using the shared-data (cloud-native) architecture, StarRocks 4.1 introduced automatic tablet splitting and merging. If your 10M rows are concentrated in too few tablets, you might not be getting enough parallelism. Ensure your bucket count is optimized or let 4.1 handle the splitting automatically.
3. Inverted Indexes:
If your "filters" are on non-key columns, version 4.1.1 has highly optimized Inverted Indexes. These allow the engine to skip scanning entire data blocks by identifying matching rows via the index.
4. Top-N Optimization:
For ORDER BY ... LIMIT 100, StarRocks 4.1.1 uses a "Top-N" pushdown. If this isn't happening, it might be due to a complex expression in the ORDER BY. Check the execution plan to see if the Sort is being pushed down into the Scan.
5. Data Cache (if using Shared-data):
If your data is on S3/OSS/HDFS, ensure the Data Cache is warm. 800ms often indicates the first "cold" read from remote storage. Subsequent "warm" reads from the BE local disk cache should be significantly faster.
Future Performance
The StarRocks roadmap (4.x and beyond) is focused on:
* JIT (Just-In-Time) Compilation: Further reducing CPU overhead for complex expressions during scans.
* Adaptive Execution: Allowing the engine to change join strategies (e.g., from Shuffle to Broadcast) mid-query based on actual row counts.
* Refined Vectorized Operators: Continuous micro-optimizations for the C++ execution core to maximize memory bandwidth utilization.
Recommendation:
Please run EXPLAIN ANALYZE on your query and look for the operator with the highest ActiveTime. If the bottleneck is in SCAN, it’s likely a partitioning/indexing issue. If it’s in EXCHANGE, you need to look at Colocate Joins to eliminate network shuffle less data.
References
• release_notes/release-4.0.mdRocky
06/30/2026, 3:50 PMTOP_N)
The biggest CPU consumer is Node 7: TOP_N (279ms, ~46% of total time).
* What's happening: Even though you have a LIMIT 100, the engine is having to sort all 10,912,000 rows before it can pick the top 100.
* Why it's slow: Your OrderByExprs includes a CASE expression (111: case). Sorting 11 million rows using a complex expression is CPU-intensive.
* The Issue: Because the sort depends on the result of a Join (Node 5), StarRocks cannot "push down" the limit to the scan level. It must join all 11M rows, calculate the CASE result for each, and then sort them.
2. High "ScheduleTime" (546ms)
The profile shows ScheduleTime: 546.827ms. In StarRocks, this usually indicates one of two things:
* CPU Saturation: With only 2 BE nodes and a query processing 11M rows with complex sorts and joins, the worker threads may be waiting for available CPU cycles.
* Pipeline Throttling: If your BE nodes have low core counts, the pipeline engine might be queuing tasks because the sort and join are hogging the available threads.
3. Ineffective Runtime Filters
Look at Node 1 (OLAP_SCAN):
RuntimeFilter: 10.912M -> 10.912M (0.00%)
* The Problem: StarRocks tried to use a Runtime Filter from your join to prune data during the scan, but it filtered 0 rows.
* The Result: The engine was forced to pull all 10.9 million rows into memory to perform the join, rather than dropping them at the storage layer.
4. External Table Overhead (Iceberg)
You are joining with Iceberg tables (ICEBERG_SCAN id=14 and id=3).
* While the Iceberg scan itself is fast (1.6ms), joining local StarRocks data with external data prevents certain optimizations like Colocate Joins (which require both tables to be in StarRocks and distributed the same way).
──────────
How to make this query "ClickHouse-fast" (<100ms):
1. Materialize the "Sort Key":
Since the CASE expression in your ORDER BY is taking up most of the time, try to calculate that value during ingestion and store it as a column. If you sort by a plain column that is also part of your Sort Key (defined in ORDER BY during table creation), StarRocks can optimize the TOP_N significantly.
2. Optimize the Join (Colocation):
If the Iceberg table is small (it looks like only ~1000 rows), consider importing it into StarRocks as a native table and using a Colocation Group or a Broadcast Join. This will eliminate the BUCKET_SHUFFLE overhead.
3. Adjust Pipeline Parallelism:
Try reducing the degree of parallelism if your BE nodes have few cores to reduce the ScheduleTime contention:
sql
SET pipeline_dop = 8; -- Adjust based on your BE CPU cores
4. Check the CASE in Join/Filter:
If you can move filters or sort logic to happen before the join, the TOP_N can be pushed down to the scan node. Currently, the engine thinks it needs all 11M rows because the sort criteria only becomes "known" after the join.
Summary:
StarRocks is currently doing a "Heavy Join + Global Sort" on 11 million rows in ~700ms. To get to <100ms, you need to change the query or schema so that the Limit can be applied before or during the scan, rather than after joining 11 million rows.Rocky
06/30/2026, 3:56 PMwinners subquery). However, the profile and the query structure reveal three major reasons why you are seeing 700ms+ instead of <100ms.
1. Analysis of the Profile vs. Query
* The Sorting Tax (Node 7 - 279ms): The subquery winners is scanning 10.9 million rows, joining them with Iceberg, and then performing a Global Sort on all 11M rows to find the top 101.
_ Because the sort relies on a CASE statement and a COALESCE from the _joined* Iceberg table, StarRocks cannot "push down" the sort to the storage layer. It must materialize all 11 million rows in memory first.
* The Join Overhead (Node 5 - 165ms): You are performing a LEFT JOIN on 11 million rows against an external Iceberg table. Even if the Iceberg table is small, the engine has to perform a BUCKET_SHUFFLE or SHUFFLE join.
* Ineffective Runtime Filter: The profile showed the Runtime Filter filtered 0 rows. This means every single row from type1A in the last 7 days survived the join and had to be sorted.
──────────
2. How to Improve the Query
A. Query Optimization (The "One-Pass" Approach)
In StarRocks 4.1.1, the "winners" subquery pattern is often slower than a single-pass query because StarRocks has Internal Late Materialization. By splitting it, you are forcing the engine to join the tables twice.
Try this optimized version:
sql
SELECT t.*,
COALESCE(s.status, 'OPEN') AS state_status,
s.alert_start_time AS state_alert_start_time,
s.updated_at AS state_status_changed_at,
s.changed_by_user_id AS state_status_changed_by_user_id,
s.changed_by_display AS state_status_changed_by_display,
COALESCE(s.assignee_user_id, 'unassigned') AS state_assignee_user_id,
s.assignee_display AS state_assignee_display,
s.assigned_at AS state_assignment_changed_at,
s.note AS state_note
FROM hot_space.type1A AS t
LEFT JOIN iceberg.firstspace.alert_state_current AS s
ON s.tenantid = t.tenantid AND s.alert_uuid = t.eventuid
WHERE t.tenantid = '<some uuid>'
AND t.EventStartTime >= '2026-06-23 00:00:00'
AND t.EventStartTime <= '2026-06-30 18:49:00'
AND t.detectionvisibility != 'Silent'
ORDER BY
CASE
WHEN LOWER(t.eventseverity) = 'critical' THEN 15
WHEN LOWER(t.eventseverity) = 'medium' THEN 3
WHEN LOWER(t.eventseverity) = 'low' THEN 1
WHEN LOWER(t.eventseverity) = 'informational' THEN 0
ELSE 7
END DESC,
CASE
WHEN COALESCE(s.status, 'OPEN') = 'CLOSED' THEN 2
WHEN COALESCE(s.status, 'OPEN') = 'IN_PROGRESS' THEN 1
ELSE 0
END ASC,
t.EventStartTime DESC,
t.eventuid DESC
LIMIT 101;
B. Schema & Performance Tuning (The "Real" Fix)
To get this under 100ms, you need to eliminate the "Sort 11M rows" step:
1. Materialize the Severity Weight:
The CASE statement is expensive to calculate 11 million times per query.
* Action: Add a column severity_weight (TINYINT) to the type1A table. Populate it during ingestion (e.g., Critical = 15).
* Result: Sorting a numeric column is significantly faster than a LOWER(string) + CASE.
2. Use a Global Dictionary for `eventseverity`:
If you don't want to add a column, ensure eventseverity is a low-cardinality string. StarRocks will use dictionary encoding, but the CASE statement still breaks the optimization.
3. Optimize the Iceberg Join:
Your profile shows the Iceberg table is small (~1000 rows).
* Action: Ensure the join is a Broadcast Join. You can hint it: LEFT JOIN [broadcast] iceberg....
* Better Action: If the alert_state_current table is small and frequently joined, consider syncing it into a StarRocks Primary Key table. Joining StarRocks-to-StarRocks is much faster than StarRocks-to-Iceberg.
4. Inverted Index:
If you often filter by detectionvisibility != 'Silent', add an Inverted Index to that column.
```sqlRocky
06/30/2026, 3:56 PMtype1A is partitioned by EventStartTime (daily) and bucketed by eventuid. This ensures that for a 7-day query, StarRocks only touches 7 partitions.
Summary
The 800ms is coming from calculating a complex sort key for 11 million rows and joining them with an external table. If you materialize the severity_weight as a numeric column and use a single-pass query with a broadcast join, you should see the performance drop toward the 100-200ms range. To 200ms range.Rocky
06/30/2026, 4:06 PMeventseverity which only has 4-5 values), StarRocks automatically builds a dictionary mapping those strings to integers (e.g., "Critical" -> 1, "High" -> 2) at the storage level.
* The Benefit: When you filter or aggregate, StarRocks works with the small integers instead of comparing long strings. This is extremely fast.
* The "Trap": In your query, you are using LOWER(t.eventseverity). This function breaks the dictionary optimization because the engine has to execute the function on every row to get a new string before it can compare it.
* Fix: If your data is already consistent, remove the LOWER(). If not, use a Generated Column or materialize the severity as an integer during ingestion.
2. What is an "Inverted Index"?
In StarRocks 4.1.1, an Inverted Index (specifically the "Built-in" type for shared-data) acts like a search engine index.
* How it works: It maps every unique value in a column to a list of row IDs where that value appears.
* The Benefit: Without an index, StarRocks might scan 11 million rows to find the ones where detectionvisibility != 'Silent'. With an Inverted Index, it looks at the index, immediately identifies the row IDs that aren't 'Silent', and only pulls those rows from disk.
* Why it helps you: Your query has several filters. Adding an inverted index to tenantid, detectionvisibility, and eventseverity allows the engine to skip millions of rows before they even reach the join or sort stage.
──────────
3. The "Metadata Problem": How companies handle Task States
You mentioned that updating the "hot" data table is considered "bad practice" and joins are slow. In StarRocks, the "best practice" is actually different:
The Solution: The Primary Key Table (Partial Updates)
Companies using StarRocks for "Detection/Alert" systems (like yours) use the Primary Key (PK) Table model.
1. Don't Join Metadata: Instead of keeping status, assignee, and notes in a separate Iceberg table, you include those columns in your main StarRocks type1A table.
2. Use Partial Updates: When a user updates a task status in your UI, you send a "Partial Update" to StarRocks.
sql
-- This ONLY updates the status/assignee for one alert, it does NOT rewrite the 11M rows.
INSERT INTO hot_space.type1A (eventuid, tenantid, state_status, state_assignee_user_id)
VALUES ('uuid-123', 'tenant-456', 'CLOSED', 'user-789');
3. Why this is fast:
* No Join: The state_status is now a local column. Your ORDER BY and WHERE clauses no longer need to wait for a join to finish.
* Sub-100ms: StarRocks PK tables are highly optimized for this. The update happens in memory first (write-ahead log), so the "latest state" is available instantly without the "Update Tax" of traditional databases.
Why your current Join is a bottleneck:
You are joining 11 million rows (hot) against an Iceberg table (external).
* Even if the Iceberg table is small, StarRocks has to fetch that data from remote storage (S3/HDFS) and perform a shuffle join.
* In your profile, the Join Probe Time was 165ms. If you move that metadata into the main table via Partial Updates, that 165ms goes to 0ms.
Summary Recommendation for 4.1.1:
1. Switch type1A to a Primary Key table (if it isn't already).
2. Bring the Iceberg metadata columns into the StarRocks table.
3. Use Partial Updates to sync task states from your app to StarRocks.
4. Add Inverted Indexes on your filter columns (tenantid, eventseverity).Rocky
06/30/2026, 4:06 PMLOWER() and COALESCE() from your ORDER BY by ensuring those columns are populated correctly during the way you want them during ingestion/update.
This architectural shift is how security and observability platforms achieve <50ms response times on billions of rows.
References
• best_practices/primarykey_table.md
• table_design/table_types/primary_key_table.mdRocky
06/30/2026, 4:13 PMUPDATE against Iceberg every time a user clicks "Assign to me" in your UI, you will hit high latency and metadata overhead.
The Recommended Architecture: "State Proxy"
Instead of storing the task state inside the large event tables, you separate the immutable event from the mutable state.
1. Large Event Data (Split):
* events_hot (StarRocks): Last 7 days.
* events_cold (Iceberg): Older than 7 days.
_ _These tables contain only the event info (tenantid, eventuid, severity, etc.) and never change.*
2. Task State Table (StarRocks Primary Key Table):
* alert_state (StarRocks): Contains tenantid, eventuid, status, assignee, notes.
* Retention: Keep records here for as long as they are "relevant" (e.g., 90 days).
* Why: Even if you have 100 million alerts, a table with just 5-10 columns is very small for StarRocks. It stays in memory and supports Partial Updates at extremely high speeds.
How the Query and Updates Work
The Unified View
You create a view that joins your event data with the "hot" state table. Because the alert_state table is a native StarRocks Primary Key table, the join is highly optimized.
sql
CREATE VIEW v_detections AS
SELECT e.*, s.status, s.assignee
FROM (
SELECT * FROM events_hot
UNION ALL
SELECT * FROM iceberg.catalog.events_cold
) e
LEFT JOIN alert_state s
ON e.tenantid = s.tenantid AND e.eventuid = s.eventuid;
The Update Process (Always Fast)
When a user updates a task (even one that is 30 days old and sitting in Iceberg), your application always sends the update to the StarRocks alert_state table:
sql
-- This is a native StarRocks PK update. It takes <10ms.
INSERT INTO alert_state (tenantid, eventuid, status, assignee)
VALUES ('tenant-1', 'old-event-uuid', 'IN_PROGRESS', 'rocky_user');
Why this is the "Industry Standard" for 4.1.1:
1. Performance: You are no longer joining 11M rows of logs to 11M rows of metadata. You are joining 11M logs to a compact, indexed state table.
2. No Iceberg Writes: You avoid the complexity of writing updates/deletes to Iceberg, which requires v2 format and usually involves higher latency.
3. Data Integrity: When you move data from StarRocks to Iceberg (the 7-day cutoff), you don't have to move the "state." The state stays in StarRocks where it remains "live" and editable.
4. Simplicity: Your UI doesn't need to know if the data is in StarRocks or Iceberg. It just queries the view and updates the state table.
Summary of Improvements
* Use a Primary Key Table for the state metadata.
* *Avoid LOWER() in queries*; ensure data is normalized during ingestion.
* Leverage Inverted Indexes on tenantid and eventuid in the alert_state table to make the "State Proxy" join nearly instantaneous.
References
• using_starrocks/async_mv/use_cases/data_lake_query_acceleration_with_materialized_views.md
• knowledge/usage/best-practice.md
• data_source/catalog/unified_catalog.md
• data_source/feature-support-data-lake-analytics.mdRocky
06/30/2026, 4:13 PMRocky
06/30/2026, 4:18 PMSELECT t.* ... ORDER BY ... LIMIT 101, StarRocks does not read all the columns for those 11 million rows.
* Stage 1: It only reads the columns needed for the WHERE clause and the ORDER BY (e.g., eventuid, severity, timestamp).
* Stage 2: It performs the join and the sort using just these narrow columns.
* Stage 3: Only after it has identified the exact 101 "winner" `eventuid`s does it go back to the disk (or cache) to fetch the rest of the data (t.*) for only those 101 rows.
_ _Result:* Instead of 11 million "wide" rows in memory, it only handles 11 million "tiny" IDs and weights.
2. Top-N Heap Sort (Instead of a Global Sort)
In your current profile, you saw a "Global Sort" taking 270ms. When you have a LIMIT 101, StarRocks uses a Min-Heap/Max-Heap optimization:
* As the engine scans the 11 million rows, it doesn't keep them all. It keeps a "Top 101" list in memory.
* If row #102 is "worse" than the current 101 winners, it is discarded immediately.
_ _Result:* Memory usage is capped at 101 rows per CPU thread, even if you are scanning billions of rows.
3. Broadcast Join vs. Shuffle Join
This is why the "Compact State Table" matters:
* Current State (Iceberg): Because Iceberg is an external table, StarRocks often has to "Shuffle" the 11 million rows (send them over the network) to match them with the Iceberg data. This is slow.
* Optimized State (PK Table): If your alert_state table is a StarRocks Primary Key table and is relatively small (e.g., under a few hundred MBs), StarRocks performs a Broadcast Join. It copies the entire state table to every machine.
_ _Result:* Each machine joins its local slice of the 11 million logs against the local state table. Zero network movement for your 11 million log rows.
4. Primary Key Index Lookup
When you join on a Primary Key table in StarRocks, the engine uses an in-memory PK index.
* Instead of a generic hash join, StarRocks can look up the "state" of an alert in a high-speed index.
* If you use the Partial Update approach (moving the state columns into the log table), the join becomes zero, because the data is already in the same row.
Why your current query is slow (700ms+):
The reason your 11 million rows are hurting you right now is likely because:
1. External Metadata: Fetching from Iceberg adds "Remote I/O" latency.
2. Complex Sort Key: Using LOWER(string) and COALESCE on 11 million rows prevents the engine from using certain "Short-circuit" sort optimizations.
3. Broken Pushdown: Your nested subquery structure (winners) can sometimes prevent the optimizer from seeing that it can do a "Top-N" pushdown all the way to the storage layer.
Summary
By using a StarRocks Primary Key table for state and a Single-pass query, you allow the engine to use Late Materialization and Top-N Pushdown. The "work" of looking at 11 million rows is reduced to a simple, vectorized scan of a few bytes per row, which StarRocks can do in tens of milliseconds.
References
• using_starrocks/accelerate-cardinality-preserving-joins.md
• table_design/table_types/primary_key_table.md
• best_practices/query_tuning/query_hint.md
ReferencesRocky
06/30/2026, 4:18 PMRocky
06/30/2026, 4:31 PMalert_state): Put an Inverted Index on columns like status and assignee.
* On the Hot Log Table (type1A): Put an Inverted Index on tenantid and eventseverity.
* The Result: When you run a query like WHERE status = 'OPEN', StarRocks uses the Inverted Index to find the matching `eventuid`s in milliseconds. It doesn't "scan" the table; it just grabs the list of IDs.
2. The Secret Sauce: "Runtime Filter Pushdown"
This is the most critical part for your performance. Even though you are doing a LEFT JOIN between 11 million logs and your state table, StarRocks doesn't just "load everything and join it."
1. Filter the State: StarRocks first filters the alert_state table (e.g., WHERE status = 'OPEN').
2. Generate a Runtime Filter: It creates a small, highly compressed "Bloom Filter" or "In-list" of the `eventuid`s that matched that status.
3. Pushdown to Scan: It "pushes" this filter down into the scan of the 11 million log rows.
4. Index-Accelerated Pruning: The log table uses its own index (or the Runtime Filter) to discard rows at the storage level before they ever enter the join engine or memory.
This turns your "11 million row join" into a "1,000 row join" (or however many active alerts you actually have).
3. What about the "Cold" Iceberg data?
Iceberg does not support StarRocks' native Inverted Indexes. However, StarRocks is still extremely fast with Iceberg because of two things:
* Min/Max Pruning: StarRocks uses Iceberg’s metadata to skip entire files that don't match your tenantid or timestamp.
* Data Cache: StarRocks 4.1.1 has a sophisticated local disk cache for Iceberg data. The first time you query 30 days of data, it’s pulled from S3. The second time, it’s read from local NVMe/SSD at local-disk speeds, making it nearly as fast as native tables.
How to implement this effectively:
1. Create a Primary Key Table for alert_state.
2. Add Built-in Inverted Indexes (since you are on 4.1.1):
sql
-- Example for your state table
ALTER TABLE alert_state ADD INDEX idx_status (status) USING GIN;
3. Ensure columns are normalized: As mentioned before, avoid LOWER(eventseverity) in your WHERE or ORDER BY. If the index has "High", and you search for LOWER("High"), the engine may skip the index.
4. Use a Unified View:
sql
CREATE VIEW detections_v1 AS
SELECT logs.*, state.status, state.assignee, state.notes
FROM (
SELECT * FROM logs_hot -- StarRocks (Last 7 days)
UNION ALL
SELECT * FROM iceberg.logs_cold -- Iceberg (Older)
) logs
LEFT JOIN alert_state state
ON logs.eventuid = state.eventuid
AND logs.tenantid = state.tenantid;
Summary: You don't need to fear the join. With Inverted Indexes on the filter columns and Runtime Filter Pushdown, StarRocks reduces the "11 million rows" to a tiny fraction of that before the join even happens in memory. This is how you achieve <100ms latency across hot/cold queries.
References
• data_source/catalog/iceberg/iceberg_catalog.mdRocky
06/30/2026, 4:31 PMRocky
07/01/2026, 9:29 AMalert_state table is small (e.g., less than 500k active alerts), force a Broadcast join using a hint. This ensures your 11M log rows never leave the machine they were read from.
sql
SELECT /*+ SET_VAR(broadcast_row_limit = 10000000) */ logs.*, state.status
FROM detections_logs logs
LEFT JOIN [broadcast] alert_state state
ON logs.eventuid = state.eventuid;
2. Verify "Inverted Index" Pushdown
In StarRocks 4.1.1, the Inverted Index (GIN) is extremely powerful but can be "bypassed" if your query uses functions in the WHERE clause.
* The Killer: WHERE LOWER(status) = 'open' or WHERE status LIKE '%open%'. These often prevent the Inverted Index from being used.
* The Fix: Ensure you are querying the column exactly as it is indexed. Check your profile for InvertedIndexFilterRows. If it is 0, your index is not being hit.
* Session Variable: Ensure the GIN index is actually enabled for the session:
sql
SET enable_gin_filter = true;
3. "Late Materialization" and the Limit Pushdown
If your UI says "show me the last 30 days" and sorts them, StarRocks should ideally find the top 100 rows before it reads the heavy columns (the actual log data).
* The Problem: If you join a StarRocks Hot table and an Iceberg table in a UNION ALL, the optimizer sometimes fails to push the LIMIT through the UNION. It might be trying to join and sort all 11 million rows before applying the LIMIT 100.
* The Fix: Manually push the ORDER BY and LIMIT into each sub-part of the union.
sql
SELECT * FROM (
(SELECT * FROM logs_hot WHERE ... ORDER BY ts DESC LIMIT 100)
UNION ALL
(SELECT * FROM iceberg_logs WHERE ... ORDER BY ts DESC LIMIT 100)
) joined_logs
LEFT JOIN alert_state state ON ...
ORDER BY ts DESC LIMIT 100;
This forces the engine to only ever join 200 rows to your state table, rather than 11 million.
4. Iceberg Specific: Data Cache hit-ratio
If the query is slow on the first run but fast on the second, it is a Data Cache issue.
* Check the profile for DataCacheReadBytes vs RemoteReadBytes.
* If RemoteReadBytes is high, StarRocks is waiting on S3/Object Storage.
* Solution: For a "Last 30 Days" UI, you can "warm up" the cache for the older Iceberg data using:
sql
SET TABLE cache_hot_period = "30 days"; -- In StarRocks 4.1.1
Summary of what to look for in your Profile:
1. JoinType: SHUFFLE: If you see this, use the [broadcast] hint.
2. InvertedIndexFilterRows: 0: Your index isn't being used; check for LOWER() or data type mismatches.
3. ScanRows for Iceberg vs Hot: If ScanRows is 11,000,000 and the LIMIT is 100, the "Limit Pushdown" optimization is failing (use the manual subquery approach in #3).
If you can share the specific Operator names (e.g., EXCHANGE_NODE, HASH_JOIN_NODE) that are taking the most time in your profile, I can pinpoint which of these three is the culprit.Rocky
07/01/2026, 9:34 AMLogs (Iceberg/Hot) LEFT JOIN State (PK Table), StarRocks 4.1.1 should generate a Runtime Filter.
* The Issue: If the filter on State (e.g., status = 'OPEN') is not being pushed into the Iceberg scan, StarRocks is scanning all 11 million rows from S3/Cold storage instead of just the ~1,000 rows that have open alerts.
* What to check in your profile: Look for RuntimeFilterPublish on the Join and RuntimeFilter under the OLAP_SCAN or ICEBERG_SCAN node. If it's missing, the pushdown is broken.
2. Inverted Index "Silent Fail"
On version 4.1.1, inverted indexes (GIN) have specific requirements:
* Built-in vs. CLucene: If you are on a Shared-data cluster, you must use the built-in index implementation. If you used the default, it might not be active.
* Predicates: If you are using LOWER(column) or complex expressions, the index is bypassed.
* What to check in your profile: Search for GinFilterRows. If this is 0, your inverted index is not doing anything.
3. Join Distribution (Shuffle vs. Broadcast)
If the profile shows a SHUFFLE_JOIN for that 11M row table, it means StarRocks is re-partitioning and sending 11 million rows across the network.
* The Fix: You must see BROADCAST in the join operator. This keeps the 11M rows local and only moves the tiny "State" table.
4. Late Materialization
If the profile shows RowsRead is 11,000,000 but your LIMIT is 100, the Top-N Pushdown is failing. This usually happens if the query structure (like nested subqueries or certain window functions) "hides" the limit from the scanner.
Please paste the profile text here, and I will tell you exactly which line is causing the delay. (Specifically the parts starting with PIPELINE or the operator breakdown HASH_JOIN_NODE, OLAP_SCAN_NODE).
References
• table_design/indexes/inverted_index.mdEliezer Talker
07/01/2026, 9:37 AMiceberg.firstspace.alert_state_current (1,010 rows)
BE config: late_materialization_ratio = 1000
---
## Comparison Table (Server-side TotalTime)
| Query Variant | type1A | type2A | Notes |
|---|---|---|---|
| Rocky V1 — Single-pass, CASE severity | 3s250ms | 5s793ms | Full table scan + all columns + CASE sort |
| Rocky V2 — Single-pass, severity_weight column | 3s203ms | 4s875ms | Full table scan + all columns + column sort |
| Cursor V1 — Two-phase, CASE severity | 688ms | 2s127ms | Lightweight Phase 1, heavy Phase 2 on 101 rows |
| Cursor V2 — Two-phase, severity_weight column | 696ms | 2s109ms | Best of both optimizations |
### Key Finding: type2A is significantly SLOWER than type1A
The ORDER BY(tenantid, severity_weight, eventstarttime, eventuid) in type2A's DDL changes the physical sort order on disk. This breaks zone map filtering for the time-range predicate (EventStartTime >= ... AND EventStartTime <= ...), causing:
- 2x slower OLAP_SCAN (data pages not ordered by time within a partition)
- 6x slower TOP_N sort in two-phase queries (data arrives in severity_weight order, which defeats the heap-based TopN early-termination)
Recommendation: Use type1A (standard PK sort) — it is clearly superior for this workload.
---
## Detailed Operator Breakdown
### type1A Results
| Operator | Rocky V1 | Rocky V2 | Cursor V1 | Cursor V2 |
|---|---|---|---|---|
| TotalTime | 3s250ms | 3s203ms | 688ms | 696ms |
| OLAP_SCAN (full) | 3s118ms (65%) | 3s25ms (67%) | — | — |
| OLAP_SCAN (Phase 1) | — | — | 65ms (11%) | 66ms (11%) |
| OLAP_SCAN (Phase 2) | — | — | 62ms (10%) | 64ms (10%) |
| TOP_N | 1s337ms (28%) | 1s263ms (28%) | 277ms (45%) | 285ms (46%) |
| HASH_JOIN (LEFT OUTER) | 257ms (5%) | 222ms (5%) | 165ms (27%) | 166ms (27%) |
| PROJECT | 46ms (1%) | 23ms (1%) | 14ms (2%) | 9ms (1%) |
| ICEBERG_SCAN | 1.5ms | 1.4ms | 1.5ms | 1.4ms |
| Memory | 48.2 GB | 48.2 GB | 2.6 GB | 2.5 GB |
### type2A Results
| Operator | Rocky V1 | Rocky V2 | Cursor V1 | Cursor V2 |
|---|---|---|---|---|
| TotalTime | 5s793ms | 4s875ms | 2s127ms | 2s109ms |
| OLAP_SCAN (full) | 5s622ms (73%) | 4s662ms (69%) | — | — |
| OLAP_SCAN (Phase 1) | — | — | 149ms (7%) | 141ms (7%) |
| OLAP_SCAN (Phase 2) | — | — | 18ms (1%) | 19ms (1%) |
| TOP_N | 1s831ms (24%) | 1s827ms (27%) | 1s724ms (82%) | 1s717ms (83%) |
| HASH_JOIN (LEFT OUTER) | 209ms (3%) | 205ms (3%) | 167ms (8%) | 166ms (8%) |
| PROJECT | 33ms (0.4%) | 24ms (0.4%) | 15ms (1%) | 9ms (0.5%) |
| ICEBERG_SCAN | 1.5ms | 1.5ms | 1.4ms | 1.5ms |
| Memory | 53.6 GB | 52.5 GB | 2.6 GB | 2.5 GB |
---
## Key Observations for Rocky
1. Two-phase pattern is the biggest win (4.7x faster on type1A: 3.2s → 688ms)
- Phase 1 selects only lightweight columns (eventstarttime, eventuid, severity_weight) + does the sort/limit
- Phase 2 joins back to get all columns for only 101 rows
- Memory drops from 48 GB → 2.5 GB
2. severity_weight column vs CASE has minimal impact on the two-phase query (688ms vs 696ms — within noise)
- The PROJECT cost drops from 14ms to 9ms, but TOP_N sort time stays the same (~277ms)
- The reason: in Phase 1 the sort still includes a CASE for status_weight (from the Iceberg join), which dominates
3. For single-pass queries, severity_weight saves ~50ms (3250ms vs 3203ms)
- The PROJECT drops from 46ms to 23ms
- TOP_N drops from 1337ms to 1263ms (74ms savings from not evaluating CASE during sort)
4. HASH_JOIN (Iceberg LEFT OUTER) is ~165ms — this is the constant overhead per query
- Probing 10.9M rows against 1,010 Iceberg rows
- SearchHashTableTime dominates at ~156ms
5. The real bottleneck in the two-phase query (688ms) is:
- TOP_N sort: 277ms (45%) — sorting 10.9M rows by severity + status + time
- HASH_JOIN: 165ms (27%) — joining 10.9M rows with Iceberg
- OLAP_SCAN Phase 1: 66ms (11%) — reading lightweight columns for 10.9M rows
- OLAP_SCAN Phase 2: 62ms (10%) — reading all columns for 101 rows (with RuntimeFilter)
6. type2A's ORDER BY DDL causes massive regression — the physical sort by severity_weight prevents effective zone map pruning on EventStartTime and changes data arrival order, defeating TopN early termination.
---
## Cluster Configuration
- 2 BEs (starrocks-be-0 BackendId:10001, starrocks-be-1 BackendId:628325), both Alive
- 8 buckets per partition, HASH by eventuid
- late_materialization_ratio = 1000
- enable_persistent_index = true
- compression = LZ4
- replication_num = 1
- StarRocks 4.1.1-14b7e3f
---
## Table DDLs
### type1A (standard PK sort — WINNER)
sql
CREATE TABLE `type1A` (
`tenantid` varchar(65533) NOT NULL,
`eventstarttime` datetime NOT NULL,
`eventuid` varchar(65533) NOT NULL,
`dvcos` varchar(65533) NULL,
`eventcount` bigint(20) NULL,
`eventowner` varchar(65533) NULL,
`eventtype` varchar(65533) NULL,
`dvchostname` varchar(65533) NULL,
`dvcdomain` varchar(65533) NULL,
`table_name` varchar(65533) NULL,
`type` bigint(20) NULL,
`eventendtime` varchar(65533) NULL,
`dvcaction` varchar(65533) NULL,
`dvcipaddr` varchar(65533) NULL,
`dvcmacaddr` varchar(65533) NULL,
`dvcosversion` varchar(65533) NULL,
`dvcid` varchar(65533) NULL,
`eventseverity` varchar(65533) NULL,
`agentid` varchar(65533) NULL,
`alertname` varchar(65533) NULL,
`eventtenant` varchar(65533) NULL,
`eventproduct` varchar(65533) NULL,
`eventresult` varchar(65533) NULL,
`eventmessage` varchar(5000) NULL,
`eventvendor` varchar(65533) NULL,
`eventschemaversion` varchar(65533) NULL,
`dvcidtype` varchar(65533) NULL,
`received_ts` bigint(20) NULL,
`detectionvisibility` varchar(65533) NULL,
`domainou` varchar(65533) NULL,
`dvcdomaintype` varchar(65533) NULL,
`dvctype` varchar(65533) NULL,
`eventproductversion` varchar(65533) NULL,
`eventschema` varchar(65533) NULL,
`eventsubtype` varchar(65533) NULL,
`severity_weight` smallint(6) NULL,
`additionalfields` struct<...> NULL
) ENGINE=OLAP
PRIMARY KEY(`tenantid`, `eventstarttime`, `eventuid`)
PARTITION BY date_trunc('day', eventstarttime)
DISTRIBUTED BY HASH(`eventuid`) BUCKETS 8
PROPERTIES (
"compression" = "LZ4",
"enable_persistent_index" = "true",
"replication_num" = "1"
);
### type2A (ORDER BY severity_weight — SLOWER)
sql
CREATE TABLE `type2A` (
_-- same columns as type1A --_
) ENGINE=OLAP
PRIMARY KEY(`tenantid`, `eventstarttime`, `eventuid`)
PARTITION BY date_trunc('day', eventstarttime)
DISTRIBUTED BY HASH(`eventuid`)
ORDER BY(`tenantid`, `severity_weight`, `eventstarttime`, `eventuid`)
PROPERTIES (
"compression" = "LZ4",
"enable_persistent_index" = "true",
"replication_num" = "1"
);
---
## Query SQL
### Rocky V1 — Single-pass with CASE severity
sql
SELECT t.*,
COALESCE(s.status, 'OPEN') AS state_status,
s.alert_start_time AS state_alert_start_time,
s.updated_at AS state_status_changed_at,
s.changed_by_user_id AS state_status_changed_by_user_id,
s.changed_by_display AS state_status_changed_by_display,
COALESCE(s.assignee_user_id, 'unassigned') AS state_assignee_user_id,
s.assignee_display AS state_assignee_display,
s.assigned_at AS state_assignment_changed_at,
s.note AS state_note
FROM hot_space.type1A AS t
LEFT JOIN iceberg.firstspace.alert_state_current AS s
ON s.tenantid = t.tenantid AND s.alert_uuid = t.eventuid
WHERE t.tenantid = '<uuid>'
AND t.EventStartTime >= '2026-04-01 00:00:00'
AND t.EventStartTime <= '2026-07-01 18:49:00'
AND t.detectionvisibility != 'Silent'
ORDER BY
CASE WHEN LOWER(t.eventseverity) = 'critical' THEN 15
WHEN LOWER(t.eventseverity) = 'medium' THEN 3
WHEN LOWER(t.eventseverity) = 'low' THEN 1
WHEN LOWER(t.eventseverity) = 'informational' THEN 0
ELSE 7 END DESC,
CASE WHEN COALESCE(s.status, 'OPEN') = 'CLOSED' THEN 2
WHEN COALESCE(s.status, 'OPEN') = 'IN_PROGRESS' THEN 1
ELSE 0 END ASC,
t.EventStartTime DESC,
t.eventuid DESC
LIMIT 101;
### Rocky V2 — Single-pass with severity_weight column
sql
_-- Same as Rocky V1 but ORDER BY uses t.severity_weight instead of CASE:_
ORDER BY
t.severity_weight DESC,
CASE WHEN COALESCE(s.status, 'OPEN') = 'CLOSED' THEN 2
WHEN COALESCE(s.status, 'OPEN') = 'IN_PROGRESS' THEN 1
ELSE 0 END ASC,
t.EventStartTime DESC,
t.eventuid DESC
LIMIT 101;
### Cursor V1 — Two-phase with CASE severity
sql
SELECT t.*,
COALESCE(s.status, 'OPEN') AS state_status,
s.alert_start_time AS state_alert_start_time,
s.updated_at AS state_status_changed_at,
s.changed_by_user_id AS state_status_changed_by_user_id,
s.changed_by_display AS state_status_changed_by_display,
COALESCE(s.assignee_user_id, 'unassigned') AS state_assignee_user_id,
s.assignee_display AS state_assignee_display,
s.assigned_at AS state_assignment_changed_at,
s.note AS state_note
FROM hot_space.type1A AS t
LEFT JOIN iceberg.firstspace.alert_state_current AS s
ON s.tenantid = t.tenantid AND s.alert_uuid = t.eventuid
INNER JOIN (
SELECT t.eventstarttime, t.eventuid
FROM hot_space.type1A AS t
LEFT JOIN iceberg.firstspace.alert_state_current AS s
ON s.tenantid = t.tenantid AND s.alert_uuid = t.eventuid
WHERE t.tenantid = '<uuid>'
AND t.EventStartTime >= '2026-04-01 00:00:00'
AND t.EventStartTime <= '2026-07-01 18:49:00'
AND t.detectionvisibility != 'Silent'
ORDER BY
CASE WHEN LOWER(t.eventseverity) = 'critical' THEN 15
WHEN LOWER(t.eventseverity) = 'medium' THEN 3
WHEN LOWER(t.eventseverity) = 'low' THEN 1
WHEN LOWER(t.eventseverity) = 'informational' THEN 0
ELSE 7 END DESC,
CASE WHEN COALESCE(s.status, 'OPEN') = 'CLOSED' THEN 2
WHEN COALESCE(s.status, 'OPEN') = 'IN_PROGRESS' THEN 1
ELSE 0 END ASC,
t.EventStartTime DESC,
t.eventuid DESC
LIMIT 101
) winners ON t.eventstarttime = winners.eventstarttime AND t.eventuid = winners.eventuid
WHERE t.tenantid = '<uuid>'
ORDER BY
CASE WHEN LOWER(t.eventseverity) = 'critical' THEN 15
WHEN LOWER(t.eventseverity) = 'medium' THEN 3
WHEN LOWER(t.eventseverity) = 'low' THEN 1
WHEN LOWER(t.eventseverity) = 'informational' THEN 0
ELSE 7 END DESC,
CASE WHEN COALESCE(s.status, 'OPEN') = 'CLOSED' THEN 2
WHEN COALESCE(s.status, 'OPEN') = 'IN_PROGRESS' THEN 1
ELSE 0 END ASC,
t.EventStartTime DESC,
t.eventuid DESC;
### Cursor V2 — Two-phase with severity_weight column
sql
_-- Same as Cursor V1 but ORDER BY uses t.severity_weight instead of CASE_
_-- (in both the subquery and the outer query)_
ORDER BY
t.severity_weight DESC,
CASE WHEN COALESCE(s.status, 'OPEN') = 'CLOSED' THEN 2
WHEN COALESCE(s.status, 'OPEN') = 'IN_PROGRESS' THEN 1
ELSE 0 END ASC,
t.EventStartTime DESC,
t.eventuid DESC
---Rocky
07/01/2026, 9:38 AMalert_state from Iceberg to a Native StarRocks PK Table
Your profile shows HASH_JOIN is taking 165ms, mostly in SearchHashTableTime. While 1,010 rows is small, probing 11 million rows against an external Iceberg catalog adds overhead that prevents sub-100ms speeds.
* Action: Create alert_state_current as a Primary Key table inside StarRocks.
* Why: This allows for Colocated Joins and Global Runtime Filters, which can prune the 10.9M rows during the scan, before they ever reach the Join or Sort operators.
2. Implement a Colocated Join
This is the "silver bullet" for your 165ms join delay.
* Action: Ensure both type1A and your new native alert_state table are in the same Colocation Group and use the exact same distribution.
sql
-- On both tables, add this to PROPERTIES:
"colocate_with" = "alert_group"
* Why: In a colocated join, StarRocks knows that eventuid 'X' in the log table is on the same physical BE as eventuid 'X' in the state table. It performs a local join with zero network shuffle and zero global hash table overhead.
3. Add an Inverted Index (GIN) to detectionvisibility
Your query filters by t.detectionvisibility != 'Silent'. Currently, StarRocks is scanning 10.9M rows and then discarding the "Silent" ones.
* Action:
sql
ALTER TABLE type1A ADD INDEX idx_visibility (detectionvisibility) USING GIN;
* Why: An inverted index will allow the OLAP_SCAN to immediately identify the row IDs that are NOT 'Silent'. This reduces the number of rows passed to the TOP_N sort and the HASH_JOIN, which are your two biggest costs.
4. Optimize the TOP_N (The 277ms Bottleneck)
The reason TOP_N is slow is that the sort order starts with severity_weight. In type1A, the physical data is sorted by tenantid, eventstarttime. Because the sort order in your SQL doesn't match the storage order, StarRocks must perform a full sort of all 11M filtered rows.
The Fix: You correctly identified that type2A (sorting by severity in DDL) was a failure because it broke time-pruning. Stick with type1A, but apply the Inverted Index from Step 3. If the Inverted Index reduces the 11M rows to, say, 1M rows, your TOP_N time will drop proportionally (e.g., from 277ms to ~30ms).
5. Final Query Refinement (The "Phase 1" Join)
In your Cursor V1 query, the inner subquery (Phase 1) is still joining 11M rows to the Iceberg table just to get the status for the ORDER BY.
If you move the state table to StarRocks and use the Inverted Index, your Phase 1 will look like this:
1. Index Scan: Find rows matching tenantid and visibility != 'Silent'. (Very Fast)
2. Colocated Join: Join the remaining rows locally to get status. (Very Fast)
3. Top-N: Sort the significantly reduced rowset. (Very Fast)
Summary of Expected Gains:
Operator | Current (Cursor V1) | Post-Optimization
OLAP_SCAN (Phase 1) | 65ms | <20ms (via Inverted Index)
HASH_JOIN | 165ms | <30ms (via Colocation + Native Table)
TOP_N | 277ms | <40ms (Sorting fewer rows from Index)
Total | 688ms | ~90-110ms
One final question: Does your alert_state_current table have an Inverted Index on the status column? If not, adding one there will also help the Join probe significantly speed up the Join probe.