Minn Fong
07/01/2026, 4:56 PM[0mSummary[0m
[0mQueryId: 019f1e96-edac-7453-8f94-ebc09f1c3026[0m
[0mVersion: 4.0.7-b75f536[0m
[0mState: Finished[0m
[0mTotalTime: 4s186ms[0m
" [0mExecutionTime: 4s174ms [Scan: 3s974ms (95.22%), Network: 14.429ms (0.35%), ResultDeliverTime: 0ns (0.00%), ScheduleTime: 4s87ms (97.92%)][0m"
[0mCollectProfileTime: 5ms[0m
[0mFrontendProfileMergeTime: 2.038ms[0m
" [0mQueryPeakMemoryUsage: ?, QueryAllocatedMemoryUsage: 750.677 GB[0m"
[0mTop Most Time-consuming Nodes:[0m
[1m[31m1. OLAP_SCAN (id=0) : 4s19ms (91.23%)[0m
" [0m2. TOP_N (id=2) [ROW_NUMBER, TOP-N]: 225.153ms (5.11%)[0m"
[0m3. MERGE_EXCHANGE (id=3) [GATHER]: 105.998ms (2.41%)[0m
[0m4. RESULT_SINK: 52.632ms (1.19%)[0m
[0m5. PROJECT (id=4) : 2.154ms (0.05%)[0m
[0m6. PROJECT (id=1) : 648.265us (0.01%)[0m
[0mTop Most Memory-consuming Nodes:[0m
[0mNonDefaultVariables:[0m
[0mcharacter_set_results: utf8 -> utf8mb4[0m
[0menable_adaptive_sink_dop: false -> true[0m
[0menable_async_profile: true -> false[0m
[0menable_profile: false -> true[0m
[0mnet_write_timeout: 60 -> 600[0m
[0msql_mode_v2: 32 -> 2097184[0m
[0mFragment 0[0m
│ [0mBackendNum: 1[0m
"│ [0mInstancePeakMemoryUsage: 45.501 MB, InstanceAllocatedMemoryUsage: 258.779 MB[0m"
│ [0mPrepareTime: ?[0m
└──[0mRESULT_SINK[0m
│ [0mTotalTime: 52.632ms (1.19%) [CPUTime: 52.632ms][0m
│ [0mOutputRows: 50.000K (50000)[0m
│ [0mSinkType: MYSQL_PROTOCAL[0m
└──[0mPROJECT (id=4) [0m
" │ [0mEstimates: [row: ?, cpu: ?, memory: ?, network: ?, cost: ?][0m"
│ [0mTotalTime: 2.154ms (0.05%) [CPUTime: 2.154ms][0m
│ [0mOutputRows: 50.000K (50000)[0m
" │ [0mExpression: [1: received, 2: auction_id, 3: imp_key, 25: app_bundle, 50: app_name, ...][0m"
└──[0mMERGE_EXCHANGE (id=3) [GATHER][0m
" [0mEstimates: [row: 50000, cpu: ?, memory: ?, network: ?, cost: 1.6216928022179553E11][0m"
" [0mTotalTime: 105.998ms (2.41%) [CPUTime: 91.568ms, NetworkTime: 14.429ms][0m"
[0mOutputRows: 50.000K (50000)[0m
" [0mPeakMemory: ?, AllocatedMemory: ?[0m"
[0m
[0mFragment 1[0m
│ [0mBackendNum: 12[0m
"│ [0mInstancePeakMemoryUsage: 390.470 MB, InstanceAllocatedMemoryUsage: 750.424 GB[0m"
│ [0mPrepareTime: ?[0m
└──[0mDATA_STREAM_SINK (id=3)[0m
│ [0mPartitionType: UNPARTITIONED[0m
" └──[0mTOP_N (id=2) [ROW_NUMBER, TOP-N][0m"
" │ [0mEstimates: [row: 50000, cpu: ?, memory: ?, network: ?, cost: 1.6213408022179553E11][0m"
│ [0mTotalTime: 225.153ms (5.11%) [CPUTime: 225.153ms][0m
│ [0mOutputRows: 600.000K (600000)[0m
" │ [0mPeakMemory: ?, AllocatedMemory: ?[0m"
" │ [0mOrderByExprs: [<slot 1> 1: received, <slot 2> 2: auction_id, <slot 3> 3: imp_key][0m"
└──[0mPROJECT (id=1) [0m
" │ [0mEstimates: [row: ?, cpu: ?, memory: ?, network: ?, cost: ?][0m"
│ [0mTotalTime: 648.265us (0.01%) [CPUTime: 648.265us][0m
│ [0mOutputRows: 10.069M (10068877)[0m
" │ [0mExpression: [1: received, 2: auction_id, 3: imp_key, 25: app_bundle, 50: app_name, ...][0m"
└──[1m[31mOLAP_SCAN (id=0) [0m
" [1m[31mEstimates: [row: 377016000, cpu: ?, memory: ?, network: ?, cost: 3.2423376044359104E10][0m"
" [1m[31mTotalTime: 4s19ms (91.23%) [CPUTime: 44.528ms, ScanTime: 3s974ms][0m"
[1m[31mOutputRows: 10.069M (10068877)[0m
[1m[31mRuntimeFilter: 10.069M (10068877) -> 10.069M (10068877) (0.00%)[0m
[1m[31mTable: : requests[0m
[1m[31mSubordinateOperators: [0m
[1m[31mNOOP[0m
[1m[31mOLAP_SCAN_PREPARE[0m
[1m[31mDetail Timers: [ScanTime = IOTaskExecTime + IOTaskWaitTime][0m
" [1m[31mIOTaskExecTime: 1s433ms [min=442.831ms, max=2s843ms][0m"
" [1m[31mLateMaterialize: 773.897ms [min=193.571ms, max=1s889ms][0m"
" [1m[31mIOTaskWaitTime: 1s488ms [min=282.270ms, max=2s974ms][0m"
[0m
Explain why for this query profile which uses keyset pagination, it actually scanned more data (hence more I/O contention) that a previous one that used offset pagination.Rocky
07/01/2026, 4:56 PMOLAP_SCAN (id=0) node output 10.069M rows.
* Ideally, keyset pagination should use the Prefix Index to jump directly to the first row of the page, scanning only the rows needed for that page (plus a small overhead).
* Outputting 10 million rows indicates that the storage engine was unable to "seek" to the starting point. Instead, it performed a massive scan and applied the keyset filters (received > ...) in memory or during the vectorized scan process, rather than at the index level.
2. Prefix Index (Sort Key) Mismatch
StarRocks speeds up range scans using a Prefix Index, which consists of the first 36 bytes of the table's Sort Key (defined by the ORDER BY clause in the CREATE TABLE statement).
* The Issue: If the columns used for your keyset—`received`, auction_id, and `imp_key`—are not the leading columns of the table's Sort Key, StarRocks cannot use the Prefix Index to skip data.
* Result: The engine must scan all segments in the relevant tablets and manually evaluate the filter for every row. This explains the high IOTaskExecTime (1.43s) and IOTaskWaitTime (1.48s).
3. Complexity of Tuple Comparisons (a, b, c) > (v1, v2, v3)
Keyset pagination typically involves comparing a tuple of columns. In SQL, (col1, col2) > (val1, val2) is logically equivalent to:
col1 > val1 OR (col1 = val1 AND col2 > val2)
* While StarRocks optimizes many predicates, complex OR logic is often harder for the storage engine to translate into a single range seek on disk compared to a simple col1 > val1.
* If the optimizer expanded this into a complex tree, it might have defaulted to a broader scan, especially if the columns aren't perfectly aligned with the storage order.
4. Why Offset might have been "faster" or "less I/O"
If your previous Offset query had a relatively small offset (e.g., LIMIT 50000 OFFSET 100000), it only needed to find the first 150,000 rows.
* If the Offset query was able to utilize Partition Pruning (e.g., if it filtered on a specific date) while the Keyset query lacked that filter or used a different column, the Offset query would naturally touch fewer tablets.
_ In this Keyset profile, the RuntimeFilter did zero work (0.00% efficiency), and the LateMaterialize (773ms) suggests the engine was working hard to filter rows only _after* reading the filter columns from disk.
Recommendations to Fix Performance:
1. Verify Sort Key: Check your table schema (SHOW CREATE TABLE requests). Ensure received, auction_id, and imp_key are the first three columns in the DUPLICATE/UNIQUE/PRIMARY KEY or ORDER BY clause.
2. Use Partitioning: If the table is large, ensure you are still including a partition filter (like a date range) in your WHERE clause alongside the keyset logic. Keyset pagination replaces OFFSET, but it should not replace partition pruning.
3. Simplify the Keyset: If possible, test the performance using only the first column of the keyset (e.g., WHERE received > '...') to see if it hits the Prefix Index. If that is fast, the issue is the multi-column tuple optimization.
4. Check I/O Contention: Your IOTaskWaitTime is nearly 1.5 seconds. This indicates the BE's I/O threads are saturated. Reducing the scan volume via the Sort Key will directly resolve this.
References
• best_practices/table_clustering.md
• knowledge/usage/best-practice.mdMinn Fong
07/01/2026, 4:58 PMSELECT campaign_id,
device_id_hashed,
request_duid_hashed,
UNIX_TIMESTAMP(received) AS received_ts,
bid_price,
min_bid_to_win,
bid_floor,
arrival_rate_score,
lookalike_model_power,
supply_chain_complete,
app_name,
app_bundle,
received AS cursor_received,
auction_id AS cursor_auction_id,
imp_key AS cursor_imp_key
FROM requests.requests
WHERE bid_is_loss = 1
AND bid_loss_reason = 102
AND campaign_id IN ('2711716','3138150','3218300','3225884','3054358','3054359','2711715','3150914','3138151','3150889','3218298','3225883','3218299','3225882','3218301','3138152','2711717','3054360','3150910','2711713','3054361','3150917')
AND received >= '2026-06-29 00:00:00'
AND received <= '2026-06-30 23:59:59'
AND received >= '2026-06-30 17:16:32'
AND ( received > '2026-06-30 17:16:32'
OR (received = '2026-06-30 17:16:32'
AND (auction_id > '1-8163-178283979220187815117746-1'
OR (auction_id = '1-8163-178283979220187815117746-1'
AND imp_key > '1'))) )
ORDER BY received, auction_id, imp_key LIMIT 50000
To remove the imp_key comparison? So received > last_received OR (received = last_received and auction_id > last_auction_id)Rocky
07/01/2026, 4:59 PMimp_key comparison will significantly improve performance and reduce I/O contention.
Why this change helps
The 10M rows scanned in your profile indicates that StarRocks performed a Full Segment Scan instead of a Prefix Index Seek. Here is why:
1. Prefix Index Alignment: StarRocks builds a sparse "Prefix Index" (short-key index) using the first 36 bytes of your Sort Key (received and auction_id).
2. The "Jump" Mechanism: For keyset pagination to be fast, the storage engine must "jump" (seek) to the specific data block where the last page ended. This only works if the columns in your WHERE clause comparison exactly match the leading prefix of the Sort Key.
3. The imp_key Problem: Because imp_key is not in the Sort Key, your current complex nested OR logic including imp_key prevents the optimizer from treating the filter as a simple range seek. The engine likely decides it cannot safely use the index to find the starting point, so it scans all data within the broader time range and filters the 10 million rows in memory.
Recommendation
By simplifying the logic to only use the indexed columns, the storage engine can use the Prefix Index to skip directly to the first row of your next page.
Optimized Query Pattern:
Instead of the manual OR expansion, you can use the Tuple Comparison syntax, which is cleaner and well-optimized in StarRocks:
sql
WHERE received >= '2026-06-30 17:16:32' -- Broad range for partition/index pruning
AND (received, auction_id) > ('2026-06-30 17:16:32', '1-8163-178283979220187815117746-1')
ORDER BY received, auction_id, imp_key
LIMIT 50000
Important Considerations:
* Uniqueness: Keyset pagination requires a unique key to prevent skipping or duplicating rows. If (received, auction_id) is not 100% unique (i.e., multiple rows can have the same timestamp and same auction ID), you might occasionally skip a row that has the same prefix but a different imp_key.
* The Best Fix: If you need the precision of imp_key, you should add it to the table's Sort Key. Since StarRocks v3.3+, you can modify the sort key of an existing table (Duplicate, Unique, or Aggregate key models) asynchronously:
sql
ALTER TABLE requests.requests ORDER BY (received, auction_id, imp_key);
Once this background task finishes, your original query (including imp_key in the WHERE clause) will become extremely fast because the Prefix Index will now cover all three columns.
Summary of Benefits:
* Scan Rows: Will drop from 10.069M to approximately 50,000 (the limit).
* IOTaskExecTime: Will drop from 1.4s to milliseconds.
* I/O Contention: Will virtually disappear as the engine no longer needs to read millions of unnecessary rows from disk.
References
• knowledge/usage/best-practice.md
• table_design/indexes/Prefix_index_sort_key.md
References
• knowledge/usage/best-practice.md
• table_design/indexes/Bitmap_index.md
• using_starrocks/caching/query_cache.mdMinn Fong
07/01/2026, 5:20 PMRocky
07/01/2026, 5:21 PMcampaign_id, bid_is_loss, etc.).
1. The "Sparse Match" Problem
The profile shows that the OLAP_SCAN node output 10.069M rows just to find your 50,000 results. This means only ~0.5% of the rows in that time range match your criteria.
* Why Offset was "Faster": If your previous offset query was for an early page (e.g., OFFSET 0), StarRocks started at the beginning of the segment and stopped as soon as it found the first 50,000 matches. If those matches happened to be near the start of the data, it scanned very little.
* Why Keyset is "Worse": Keyset pagination forces the engine to start at a specific point in the Sort Key (e.g., 2026-06-30 17:16:32). If the specific campaign_ids you are filtering for are sparse at that point in the table, the engine must scan millions of rows forward to find the next 50,000 matches.
2. Why this causes I/O Contention with Partial Updates
Your high IOTaskWaitTime (1.48s) confirms the BE is waiting on EBS throughput. This directly harms partial updates because:
1. Metadata Competition: Partial updates in PK tables require reading the Primary Key index and DelVectors (delete markers) from disk. These small, critical reads are getting queued behind your massive 10M-row scan.
2. Version Bloat: If you have high-frequency partial updates, you likely have many small rowsets. A 10M-row scan must check the DelVector for every rowset to ensure it's reading the latest version, multiplying the I/O load.
3. The Solution: Reduce "Rows Read" via Bitmap Indexes
Since you are filtering on campaign_id, bid_is_loss, and bid_loss_reason, and these are clearly very selective, you should add Bitmap Indexes to them.
Why this fixes it:
Without a Bitmap index, StarRocks must read the actual data for these columns from EBS to evaluate the WHERE clause. With a Bitmap index, StarRocks can determine which rows to skip using only index metadata.
Action Plan:
1. Add Bitmap Indexes:
sql
ALTER TABLE requests.requests ADD INDEX idx_campaign (campaign_id) USING BITMAP;
ALTER TABLE requests.requests ADD INDEX idx_loss (bid_is_loss) USING BITMAP;
2. Check Compaction: If your "partial updates" are frequent, check your cumulative compaction score. High scores mean too many rowsets, which slows down both scans and updates.
sql
-- Check if any tablets have a high version count (>100)
SHOW PROC '/statistic';
3. Refine the Logic: Ensure you are using the simplified tuple comparison to allow the Prefix Index to at least "seek" to the starting row quickly:
sql
WHERE (received, auction_id) > ('2026-06-30 17:16:32', '1-8163...')
By adding the indexes, the OLAP_SCAN node should skip those 10M rows at the metadata level, drastically reducing EBS I/O and leaving the bandwidth available for your partial update "Apply" tasks.
References
• knowledge/usage/best-practice.md
• table_design/indexes/Bitmap_index.md
• table_design/table_types/primary_key_table.md
• best_practices/primarykey_table.md
table)
References
• knowledge/usage/best-practice.md
• unloading/Spark_connector.md
• table_design/indexes/Prefix_index_sort_key.mdMinn Fong
07/01/2026, 5:23 PMRocky
07/01/2026, 5:24 PMreceived. This means the Zone Maps for received are extremely effective—they allow StarRocks to skip entire segments and pages that are outside your time range.
* The "Scattered" Data Problem: Columns like campaign_id, bid_is_loss, and bid_loss_reason are likely not sorted. In any given data page, there is a high probability that multiple campaign IDs exist.
* The Result: If a page's Zone Map says min_campaign_id = 100 and max_campaign_id = 5000000, and your query asks for campaign_id = 2711716, the engine must load that entire page from EBS to check the individual rows. Because your campaigns are scattered, the Zone Map "overlaps" with your filter on almost every page, forcing a "Scan" instead of a "Skip."
2. Why Keyset Pagination makes it worse than Offset
* Offset Early Exit: In some cases, OFFSET queries (especially small offsets) allow the engine to stop scanning as soon as it finds the first N matching rows from the start of a segment.
* Keyset Exhaustive Scan: Keyset pagination ((received, auction) > (...)) forces the engine to start deep within the sorted data. If the specific campaign_id values you are looking for are sparse in that specific part of the table, StarRocks has to pull page after page from EBS, checking each one, until it finally collects 50,000 matches.
3. Why this triggers EBS Contention
Because the Zone Maps aren't selective enough for your campaign_id filter, StarRocks is performing heavy I/O:
1. It reads the received and auction_id columns to find the starting point.
2. It reads the campaign_id, bid_is_loss, and bid_loss_reason columns for millions of rows to see if they match your WHERE clause.
3. This massive "Read" volume saturates the EBS bandwidth, leaving no room for the small, high-priority I/O needed for Partial Updates (reading Primary Key indexes and Delete Vectors).
How to fix it (Beyond Zone Maps)
To reduce EBS I/O, you need an index that can filter rows without reading the data pages:
1. Bitmap Indexes: Ideal for bid_is_loss or bid_loss_reason (low cardinality). It allows the engine to know exactly which rows match before touching the data.
2. Bloom Filter Indexes: Excellent for campaign_id (high cardinality). If a Bloom Filter says a campaign ID is not in a page, StarRocks skips the I/O for that page entirely.
3. Adjust the Sort Key: If campaign_id is your most common filter, consider moving it earlier in the ORDER BY clause of the table (e.g., ORDER BY (campaign_id, received, auction_id)). This clusters the data by campaign, making Zone Maps highly effective again.
Summary: Your current "Scan" is an I/O-bound process. Zone Maps are only helping you with the received time range; they are likely providing 0% filtering for your campaign and loss-reason predicates, which is why you are seeing millions of rows scanned.
References
• table_design/indexes/Prefix_index_sort_key.md
• table_design/indexes/Bitmap_index.md
• table_design/indexes/Bloom_filter_index.md
• knowledge/usage/best-practice.md