Minn Fong
06/16/2026, 1:28 AMRocky
06/16/2026, 1:29 AMupdate_rowset_commit_apply_duration_us and update_rowset_commit_apply_total in your Prometheus/Grafana dashboard. A high duration per apply task typically indicates high write amplification or index lookup latency.
* Primary Key Index Memory: Check if your Primary Key index has exceeded the memory limit. If it spills to disk, apply duration will skyrocket.
* Check BE configuration: primary_key_limit_size (default is often 0, meaning no limit other than process memory).
* Check <http://be.INFO|be.INFO> logs for "Primary Key index exceed memory limit" or similar messages.
* Rowset Count: High apply duration is often caused by having too many uncompacted rowsets. Use:
sql
SHOW PROC '/compactions';
If the Compaction Score is consistently high (>100), the "Apply" process has to search through too many files to find keys, causing a performance death spiral.
2. Debugging Compaction Frequency
Compaction frequency increases when the system produces many small files or when the "Compaction Score" remains high.
* View Running Tasks:
sql
-- View overall compaction status
SHOW PROC '/compactions';
-- View specific tablet compaction progress (Shared-data)
SELECT * FROM information_schema.be_cloud_native_compactions WHERE STATUS != 'OK';
* Check for Ingestion Slowdown: If compaction can't keep up, FE will delay commits. Check for this in running transactions:
sql
SHOW PROC '/transactions/<db_name>/running';
Look at the ErrMsg column for: "Partition's compaction score is larger than 100.0, delay commit..."
3. Analysis of Your Change (Single Update Object)
You mentioned merging events into a "single update object" (likely a JSON or Struct column). This change may have inadvertently increased CPU for the following reasons:
* Row Mode vs. Column Mode: Partial updates in StarRocks have two modes:
* Row Mode (Default): Better for updating many columns in small batches. It reads the entire row, merges the change, and writes it back.
* Column Mode: Better for updating a few columns across many rows.
* The Impact: By merging different events into one large object, you may be forcing Row Mode to work harder. If that single object is large (e.g., a complex JSON), the CPU cost to parse, merge, and re-serialize that object during every partial update is much higher than updating separate native columns.
* Write Amplification: If your "single object" is large, every small event update now requires rewriting that entire large object into a new rowset. This creates more data for the compaction threads to merge later, doubling your CPU usage.
4. Recommended Actions
1. Check Partial Update Mode: Ensure you aren't accidentally using row mode when column mode would be more efficient. For Stream Load, check the partial_update_mode header.
_ _Note: If you only have one value column (your object), column mode won't help much.*
2. Increase Compaction Threads: If you have CPU headroom (though you are at 80%), you can try to speed up compaction:
* compact_threads (BE config)
* update_compaction_num_threads_per_disk (for Shared-nothing)
3. Batch Larger Loads: If possible, decrease the frequency of Stream Loads and increase the batch size. High frequency (e.g., every 1 second) is the #1 cause of compaction debt.Rocky
06/16/2026, 1:29 AMMinn Fong
06/16/2026, 1:36 AMRocky
06/16/2026, 1:36 AMbash
-H "partial_update: true"
-H "partial_update_mode: column"
Note: Column mode is supported in Shared-nothing from v3.1 and Shared-data from v3.3.1.
Why this is better for sparse data:
* Reduced I/O: In Column Mode, StarRocks only reads and writes the specific columns included in the Stream Load. If your update only affects 2 out of 100 columns, I/O and storage costs drop by ~90%+.
* Lower CPU: There is no expensive JSON/Object parsing and merging. The engine simply writes the new column data and updates the Delete Vector.
* Faster Compaction: Because the resulting rowsets are much smaller (only containing the updated columns), compaction has significantly less data to process.
How to verify the fix
After switching back, monitor these metrics:
* `column_partial_update_apply_duration_us`: This tracks the Apply phase specifically for column-mode updates. It should be much lower than your previous update_rowset_commit_apply_duration_us.
* Compaction Score: Check SHOW PROC '/compactions';. The score should stabilize as the write amplification decreases.
References
• administration/management/monitoring/metrics.md
• administration/management/BE_configuration.mdMinn Fong
06/16/2026, 2:10 AMRocky
06/16/2026, 2:11 AMenable_merge_commit batches many small loads into one. When using Column Mode, the system has to reconcile all those different column sets into a single commit. If the combinations are too diverse, the metadata overhead for the "Apply" phase becomes more expensive than simply writing the whole row.
2. The "NULL" Trap
In StarRocks partial updates:
* Omitted from header: If a column is not in the -H "columns:..." header, it is ignored (kept as-is in the DB).
* Present in header but NULL in data: If the column is in the header, StarRocks will update it to NULL.
* Impact: By merging everything into a "single update object" (JSON or a wide CSV schema), you are likely including all possible columns in your headers. This forces StarRocks to "update" every column in every event—either with a value or a NULL—which negates the benefits of partial updates and causes massive write amplification.
3. Recommendation: The "Event-Type" Strategy
Your intuition is correct: Designing your pipeline to reflect event types/frequency is the best path forward.
Instead of one "Global" stream load, use separate stream loads (or separate headers) for each event type.
Strategy: Group by Co-occurrence (Event Type)
1. Revert the "Single Object": Go back to separate native columns. Native columns are significantly faster to compact and scan than JSON/Structs.
2. Separate Load Headers: Create specific Stream Load configurations for your high-frequency and low-frequency events.
* Event A Load: -H "columns: pk, col_a, col_b"
* Event B Load: -H "columns: pk, col_x, col_y, col_z"
3. Why this works:
* Zero Overwrite: When you load Event A, columns col_x, col_y, col_z are not in the header, so StarRocks doesn't touch them. No NULLs are written, and no I/O is wasted on them.
* Minimal DCGs: Since Event A always updates the same set of columns, StarRocks creates a consistent Delta Column Group, which is very efficient for the engine to track and compact.
* Merge Commit Friendly: enable_merge_commit works best with Row Mode for sparse data. By keeping the row "slim" (only updating 2-3 columns), the Row Mode "read-merge-write" becomes very fast because the I/O volume is low.
Summary of the "Golden Config" for Sparse Events:
* Update Mode: Row Mode (default).
* Batching: -H "enable_merge_commit: true" (This is your best defense against high compaction scores).
* Headers: Only include the specific columns relevant to that event type in the columns header.
* Storage: Use native columns (INT, VARCHAR, etc.) instead of merging them into a single JSON/Object column.
This approach will drop your CPU utilization because the "Apply" phase will no longer have to process dozens of "junk" NULL updates for every event, and the compaction threads will have significantly less data to merge.
References
• loading/StreamLoad.md
• faq/loading/Stream_load_faq.md
• loading/Load_to_Primary_Key_tables.mdMinn Fong
06/16/2026, 3:30 AMRocky
06/16/2026, 3:30 AMctx_width, ctx_height, ctx_env, ctx_screen_height, ctx_screen_width, ctx_top_origin, ctx_viewport_width, ctx_viewport_height, ctx_viewable, ctx_viewability_on_load.
* Mode: Column Mode (partial_update_mode: column).
* Rationale: These groups (36M, 23M, 23.7M) represent the vast majority of your data. By grouping them into one load, you ensure a consistent column set.
* Why Column Mode? Since you are updating ~10 columns out of what I assume is a much larger table (likely 50+ columns), Column Mode will only write the data for these 10 columns. This reduces the data volume compaction has to handle by 80%+.
* Fixing Merge Commit: enable_merge_commit failed before because your "single update object" had varying schemas. By using a fixed, consistent header for this High-Frequency load, the engine can batch these updates efficiently without creating complex metadata pointers.
2. The "Medium-Frequency Lifecycle" Load (~115k events)
* Columns: ctx_time_to_unload, ctx_unload_source.
* Mode: Row Mode (Default) or Column Mode.
* Rationale: This occurs 300x less frequently than the core group. It doesn't drive your compaction score. Keeping it separate prevents it from "polluting" the high-frequency load with NULLs or inconsistent schemas.
3. The "Sparse Interaction" Load (<10k events)
* Columns: ctx_time_to_hover, ctx_time_to_touch, ctx_click_x, ctx_click_y, ctx_unload_persisted.
* Mode: Row Mode (Default).
* Rationale: These are extremely sparse. In Row Mode, StarRocks will read the row, update these 5 fields, and write it back. At this low volume, the "read-merge-write" overhead is negligible and avoids the metadata complexity of creating Delta Column Groups for columns that rarely change.
──────────
Key Technical Adjustments
1. Stop the "NULL" Updates: If you include a column in your Stream Load -H "columns: ..." but provide no value in the data, StarRocks treats it as an instruction to overwrite the existing value with NULL. This forces the engine to perform an update on that column. By separating your loads as suggested above, you only include the columns that actually have data, allowing StarRocks to ignore the others.
2. Stable Headers for `enable_merge_commit`: The reason your "Apply" duration climbed is that enable_merge_commit was trying to reconcile batches of data with different sets of updated columns. This leads to a massive internal metadata structure called a Delta Column Group. Keep your headers 100% identical for all loads hitting the same endpoint.
3. Use Row-Column Hybrid Storage (v3.2.3+): If you are on a Shared-nothing cluster, consider recreating your table with PROPERTIES ("store_type" = "column_with_row").
* How it helps: It stores a "row-style" copy of the data. When you perform a Row Mode update, the system can "find" the old values for the update instantly without scanning multiple columnar pages. This specifically targets high "apply duration."
_ _Note: This mode does not support Column Mode updates, so only use this if you decide to stick with Row Mode for all pipelines.*Rocky
06/16/2026, 3:30 AMenable_lazy_delta_column_compaction = true is set in your BE config. This prevents StarRocks from being too aggressive about merging small column updates, which keeps CPU usage lower during high-ingestion periods.
Summary of Rationale
By segmenting your loads, you maximize consistency and minimize write amplification. The "Apply" phase becomes faster because the metadata (where each column's latest version is located) becomes predictable and uniform rather than a fragmented map of varying event-specific updates.
References
• table_design/hybrid_table.md
• sql-reference/sql-statements/table_bucket_part_index/UPDATE.md
• loading/Load_to_Primary_Key_tables.md
• loading/Spark-connector-starrocks.md
• administration/management/BE_configuration.mdMinn Fong
06/16/2026, 3:40 AMRocky
06/16/2026, 3:40 AMenable_merge_commit and column mode. If you are on an older version, Column Mode and Merge Commit can indeed conflict in the Apply phase.
2. Low-Frequency/Sparse Events (<120k events, ~5 columns)
Recommendation: Use Row Mode (Default).
* Rationale: At only ~120k events, the 550x write amplification is a drop in the bucket compared to your 36M core events.
* Why not Column Mode here? Creating complex Delta Column Group metadata for events that happen rarely isn't worth the overhead. Row Mode is "cleaner" for the system to process in small volumes and won't contribute significantly to compaction debt.
──────────
The "Golden Strategy" for 550 Columns
If you want to minimize compaction while keeping the system stable, use this split-pipeline approach:
Feature | ***Core Pipeline (High Freq)*** | ***Sparse Pipeline (Low Freq)***
Volume | ~36M events / 3hr | <150k events / 3hr
Partial Update Mode | column | row (default)
Merge Commit | true | true
Header Strategy | Strictly Fixed. All 15 core columns must be in the header every time. | Flexible. Only include what you have.
I/O Impact | Minimal (only writes 15 cols) | High per-row, but low total volume
Why your previous "Single Update Object" failed:
By merging everything into one "update object" and one stream load, you were likely sending a variable header or a header that included all 550 columns.
1. If you used Row Mode: You were overwriting existing data with NULLs for every column not present in that specific event, effectively corrupting your data or forcing the engine to do 550 columns of work for every tiny event.
2. If you used Column Mode: The "Merge Commit" feature struggled because it was trying to batch together hundreds of different Column Mode schemas (DCGs) into a single commit, which is a metadata nightmare for the BE.
Final Verification Step
Check if your table was created with column_with_row (available in v3.2.3+ for Primary Key tables).
* If YES: Row Mode becomes much faster for the "Apply" phase, but it still writes the full 550 columns. For 36M events, this will still eventually crush your compaction.
* Recommendation: Even with column_with_row, stick to Column Mode for your 36M-event pipeline to 60M event group to save the I/O.
References
• loading/Load_to_Primary_Key_tables.md
• loading/StreamLoad.md
• table_design/hybrid_table.mdRocky
06/16/2026, 3:40 AMMinn Fong
06/16/2026, 3:42 AMRocky
06/16/2026, 3:42 AMsql
CREATE TABLE `test_merge_column` (
`id` INT,
`attr_core` STRING,
`attr_sparse` STRING,
`attr_other` STRING
) PRIMARY KEY (`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 1;
2. Seed Initial Data
Insert a base record.
sql
INSERT INTO test_merge_column VALUES (1, 'original', 'original', 'original');
3. The Experiment: Two Rapid "Inconsistent" Loads
We will fire two loads immediately after each other. Both use partial_update_mode: column and enable_merge_commit: true, but they update different columns.
Load A (Core Event):
This updates attr_core.
bash
curl --location-trusted -u root: \
-H "partial_update:true" \
-H "partial_update_mode:column" \
-H "enable_merge_commit:true" \
-H "columns:id,attr_core" \
-d "1,updated_core" \
-XPUT http://<FE_IP>:<FE_HTTP_PORT>/api/test_db/test_merge_column/_stream_load
Load B (Sparse Event):
This updates attr_sparse. Crucially, this header omits attr_core.
bash
curl --location-trusted -u root: \
-H "partial_update:true" \
-H "partial_update_mode:column" \
-H "enable_merge_commit:true" \
-H "columns:id,attr_sparse" \
-d "1,updated_sparse" \
-XPUT http://<FE_IP>:<FE_HTTP_PORT>/api/test_db/test_merge_column/_stream_load
4. Verification
Run this query:
sql
SELECT * FROM test_merge_column WHERE id = 1;
What success looks like:
If the experiment is successful, you will see:
id | attr_core | attr_sparse | attr_other
1 | updated_core | updated_sparse | original
Why this confirms your fix:
1. Column Mode working: attr_other remained "original". It wasn't overwritten by NULL.
2. Merge Commit working: If you run these two curl commands in a loop (e.g., 100 times), check the rowset count:
sql
SHOW PARTITIONS FROM test_merge_column;
If RowsetCount is much lower than the number of loads you ran (e.g., 2-5 rowsets instead of 200), Merge Commit is successfully batching your Column Mode updates.
5. What to look for if it "fails"
If you see performance degrade during this test, check the BE log (<http://be.INFO|be.INFO>):
* Search for: too many delta column groups.
* The Meaning: This means you are sending too many unique combinations of columns.
* The Solution: This confirms you must standardize your headers for your high-frequency pipeline as discussed previously. Even in Column Mode, the system prefers it if you always send the same set of columns for a specific endpoint.
Version Note: If you are on a version earlier than 3.1.x, Column Mode and Merge Commit had known stability issues. Ensure you are on 3.2.x or 3.3.x for the best results with this specific combination.Minn Fong
06/16/2026, 2:32 PMRocky
06/16/2026, 2:33 PMpartial_update_mode: column, the FE correctly parses that header. However, the Merge Commit Coordinator is designed to aggregate multiple incoming streams into a single large transaction.
2. The "TPlan" Limitation: To execute the actual write on the BE, the FE generates a TPlan (Thrift Plan). In version 4.0.7, the code path that generates the "merged" plan defaults to the standard write path (Row Mode) and does not yet support propagating the specific COLUMN_UPDATE_MODE flag from the individual stream load requests into the unified batch plan.
3. Silent Downgrade: Because Row Mode is the "safe" default (it works for any number of columns), the system falls back to it so the load doesn't fail. However, for your 550-column table, this "safe" fallback is devastating for performance because it triggers the massive write amplification I mentioned earlier.
Implications for Your 550-Column Table
Since you have 550 columns, Row Mode is effectively non-viable at your 36M event volume due to compaction debt. If merge_commit disables Column Mode, you are losing the only feature that makes these updates efficient.
Recommended Workarounds
1. Disable merge_commit and Batch at the Client (Best Performance)
The most effective way to keep Column Mode active while still reducing rowsets is to perform the batching before the data hits StarRocks:
* Buffer on your side: Instead of sending 36M individual small requests, aggregate them in your application or middleware (e.g., Flink, Spark, or a simple Go/Python buffer) for ~30–60 seconds.
* Send larger Stream Loads: Send one large Stream Load every minute with partial_update_mode: column.
* Why this works: Since there is only one load request, there is no "merging" happening at the FE, so the column mode property is preserved and sent to the BE correctly.
2. Use the Flink Connector (If applicable)
If you are using the StarRocks Flink Connector, ensure you are using a version that supports "Mini-batch" loading. By increasing the Flink sink.buffer-flush.interval-ms, you can achieve the same rowset-reduction benefit as merge_commit without triggering the FE-side merge that strips the Column Mode flag.
3. Use column_with_row Table Property (Plan B)
If you absolutely must use merge_commit and cannot batch client-side, recreate the table with:
PROPERTIES ("store_type" = "column_with_row")
* This won't stop the 550-column write amplification, but it will significantly speed up the "Apply" duration of the row-mode updates, which might keep your compaction just barely under the limit.
Conclusion
Your conclusion is confirmed: Merge Commit and Column-Mode Partial Updates are currently mutually exclusive in 4.0.7.
To maintain system stability for a 550-column table, prioritize Column Mode over Merge Commit. Turn off merge_commit and implement client-side batching to ensure you are only writing the 15-20 columns you actually care about-to-be-updated columns rather than rewriting all 550.
References
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.md
• sql-reference/sql-statements/loading_unloading/STREAM_LOAD.mdMinn Fong
06/16/2026, 2:38 PMRocky
06/16/2026, 2:38 PMsink.properties.partial_update_mode = column.
* No FE Merging Needed: Because the data is pre-batched by the connector, you don't need merge_commit, so the column mode flag is never stripped.
2. Aggressive Client-Side Batching (No Infrastructure Change)
If you cannot introduce Kafka, you must increase your client-side batching window from 10 seconds to 60 seconds.
* The Math: 12 nodes × 1 load/min = 12 transactions per minute.
* Why 12/min is safe: For a Primary Key table, 12 transactions per minute is very manageable for the background compaction threads, provided you are in Column Mode.
* The Row Mode Trap: The reason your cluster likely struggled before was that merge_commit turned those loads into Row Mode, forcing every compaction task to read/write 550 columns. In Column Mode, the compaction only has to touch the 15-20 columns being updated, making it ~30x more efficient.
3. Use an Ingestion Proxy
If you don't want to increase the memory buffer on your 12 nodes, you can set up a simple Ingestion Gateway (a small Go/Python service) in one region.
* The 12 nodes ship their small 10s batches to the Gateway.
* The Gateway aggregates everything into a single large CSV/JSON and fires a Stream Load every 60 seconds with partial_update_mode: column.
Summary Comparison
Strategy | Supports Column Mode? | Reduces Transactions? | Implementation Effort
Merge Commit | No (Downgrades to Row) | Yes | Zero
Routine Load | No (Row only) | Yes | Low
Kafka Connector | Yes | Yes | Medium (Requires Kafka)
60s Client Batching | Yes | Yes | Low
My Recommendation: Start by disabling merge_commit and increasing your client-side batching to 60 seconds. This reduces your transaction volume to 12/min. At this frequency, StarRocks can easily handle Column Mode updates even for a 550-column table. If that is still too many rowsets, move to the Kafka Connector for maximum consolidation.
References
• sql-reference/sql-statements/loading_unloading/routine_load/CREATE_ROUTINE_LOAD.md
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.md
• loading/Load_to_Primary_Key_tables.md
• loading/Spark-connector-starrocks.md
• loading/loading_introduction/feature-support-loading-and-unloading.md
• loading/Kafka-connector-starrocks.mdMinn Fong
06/16/2026, 2:51 PMRocky
06/16/2026, 2:51 PMmerge_commit.
2. The Solution: Regional Kafka Connectors
Since Routine Load (the internal Kafka consumer) does not support Column Mode, you should use the StarRocks Kafka Connector (an external Sink connector).
1. Deploy the Connector in each region: Point each connector to its local regional Kafka.
2. Configure for Column Mode: Set these properties in the connector config:
properties
sink.properties.partial_update = true
sink.properties.partial_update_mode = column
3. Configure for Batching: Set the flush interval to 60 seconds:
properties
sink.buffer-flush.interval-ms = 60000
sink.buffer-flush.max-rows = 100000
3. Why this works for your scale
* Bypasses the "Merge Commit" bug: Since each connector sends its own consolidated batch as a standard Stream Load, the FE doesn't need to "merge" anything. The column mode flag is preserved.
* Solves Write Amplification: Because you are in Column Mode, StarRocks only touches the ~20 columns being updated. Compaction for a 550-column table becomes trivial.
* Low Transaction Volume: 4 transactions per minute is so low that StarRocks will have no trouble merging those 4 rowsets into the base data.
Summary Checklist
* Disable enable_merge_commit everywhere (it is your enemy for this specific 550-column use case).
* Switch from Routine Load to the Kafka Connector.
* Ensure partial_update_mode: column is explicitly set in the connector.
* Set regional flush intervals to 60s.
If you follow this, you will have 4 high-efficiency transactions per minute that only write 3% of your table's width, effectively ending your compaction and metadata issues.
References
• loading/Kafka-connector-starrocks.md
• loading/Load_to_Primary_Key_tables.md
• sql-reference/sql-statements/loading_unloading/routine_load/CREATE_ROUTINE_LOAD.md
• sql-reference/sql-statements/loading_unloading/STREAM_LOAD.md
• loading/loading_introduction/feature-support-loading-and-unloading.md
• loading/Spark-connector-starrocks.md
References
• sql-reference/sql-statements/table_bucket_part_index/UPDATE.md
• sql-reference/System_variable.md
• loading/loading_introduction/feature-support-loading-and-unloading.md
• sql-reference/sql-statements/loading_unloading/BROKER_LOAD.mdRocky
06/16/2026, 2:51 PMMinn Fong
06/16/2026, 2:55 PMRocky
06/16/2026, 2:56 PMmerge_commit for your 550-column scale. Since you already have Kafka in each region, you are 80% of the way there.
*Complexity Assessment: Medium*
If you have experience with Kafka Connect, it’s a 15-minute task. If you are new to Kafka Connect, it involves setting up a small "Sink" worker process.
Step-by-Step Configuration for Your Use Case
To enable Column Mode and Batching via the connector, you just need to add these specific lines to your connector configuration file (sr-sink.properties):
json
{
"name": "starrocks-sink-connector",
"config": {
"connector.class": "com.starrocks.connector.kafka.StarRocksSinkConnector",
"topics": "your_topic_name",
"starrocks.http.url": "your_fe_ip:8030",
"starrocks.database.name": "your_db",
"starrocks.username": "root",
"starrocks.password": "your_password",
// 1. ENABLE COLUMN-MODE PARTIAL UPDATE
"sink.properties.partial_update": "true",
"sink.properties.partial_update_mode": "column",
// 2. CONSOLIDATE TRANSACTIONS (Batching)
// Flush every 60 seconds OR every 100MB to keep transaction volume low
"bufferflush.intervalms": "60000",
"bufferflush.maxbytes": "104857600",
// 3. COLUMN MAPPING
// If your Kafka JSON only has the 20 columns you want to update:
"sink.properties.strip_outer_array": "true"
}
}
Why this solves your specific problem:
1. Preserves Column Mode: Unlike the internal "Merge Commit" coordinator, the Kafka Connector sends an explicit Stream Load request that honors the column mode flag. This stops the 550-column write amplification.
2. Consolidates 12 Nodes: Even if your 12 elastic nodes produce millions of small messages, the Sink Connector buffers them in memory and sends one combined transaction per region every 60 seconds.
3. Handles Regional Isolation: You can run one Sink Connector instance in each of your 4 regions. This results in exactly 4 transactions per minute cluster-wide—a very "healthy" load for StarRocks.
How to install it:
1. Download the JAR: Get the starrocks-connector-for-kafka-x.y.z-with-dependencies.jar from the official GitHub releases.
2. Deploy to Kafka Connect: Drop the JAR into the plugin.path of your Kafka Connect workers.
3. Submit the Config: Use the Kafka Connect REST API to post the JSON config above.
Summary
By moving the "merging" logic from StarRocks' FE (which has the merge_commit bug) to the Kafka Connector, you regain control over the update mode. This will likely drop your cluster's IO and CPU usage by over 90% because you will finally be writing only the columns that changed.
References
• loading/Kafka-connector-starrocks.mdMinn Fong
06/16/2026, 3:30 PMRocky
06/16/2026, 3:30 PMproperties
# Increase compaction threads for PK tables (Default is often 1 or 2)
update_compaction_num_threads_per_disk = 4
# Reduce the wait time between compaction rounds for a tablet
update_compaction_per_tablet_min_interval_seconds = 10
# Increase the number of rowsets that can be merged in one go
max_update_compaction_num_singleton_deltas = 200
Note: Monitor BE CPU and IO after these changes.
Step 2: Tune the Apply Thread Pool
If your "Apply" is taking 40s, the update_apply thread pool is likely saturated. You can increase the concurrency for the commit phase in `be.conf`:
properties
# Increase concurrent apply tasks (Default: CPU cores)
transaction_apply_worker_count = 16 # (Or 2x current setting)
# Ensure enough memory for wide-column linking
partial_update_memory_limit_per_worker = 4294967296 # Increase to 4GB
Step 3: Check Persistent Index
With 550 columns and 200 rowsets, if your index is fully in-memory, you might be hitting GC or memory pressure. Ensure you are using the Persistent Index to keep apply times stable as rowsets grow.
* Check: SHOW CREATE TABLE ...
* Property: "enable_persistent_index" = "true"
Step 4: Reconsider Row Mode vs. Column Mode
There is a "sweet spot" for Column Mode. It is best when you update many rows in large batches.
If your 60-second batches are still relatively small (e.g., only a few thousand rows per batch), Row Mode might actually be more efficient even with 550 columns.
* Row Mode: Higher IO (writes 550 cols), but very simple "Apply" (one file, simple delete vector).
* Column Mode: Lower IO (writes 20 cols), but complex "Apply" (linking 20 files to 530 files).
Recommendation:
1. Try the BE tunings in Step 1 & 2 first to see if you can clear the rowset backlog.
2. If the rowsets stay high and the apply duration doesn't drop below 10s, revert to Row Mode but keep your 60-second batching.
3. With only 4 transactions per minute, Row Mode's IO penalty is usually acceptable, and its Apply Phase is much more resilient to wide tables.
To verify your progress, run this periodically to see the "Compaction Score" (MaxCS):
sql
SHOW PROC "/dbs/<your_db>/<your_table>/partitions";
If MaxCS is > 100, compaction is failing. If it's < 20, the cluster is healthy.
References
• table_design/table_types/primary_key_table.md
• administration/management/BE_configuration.mdRocky
06/16/2026, 3:30 PMMinn Fong
06/16/2026, 5:59 PMRocky
06/16/2026, 5:59 PMenable_persistent_index = true is set on the table. This moves the Primary Key index from memory to disk, which is critical for wide tables to prevent the "Apply" phase from consuming all your BE memory.
4. Monitor Compaction: Once you switch back to Row Mode, your rowset count should naturally drop back down as the "Apply" speed increases and the compaction threads can keep up.
Summary
You aren't necessarily limited by the engine, but you are limited by the Column Mode linking mechanism. For 550 columns, Row Mode + Batching (60s) is the architecturally sound way to handle partial updates in StarRocks.
References
• administration/management/monitoring/metrics.md
• sql-reference/sql-statements/table_bucket_part_index/UPDATE.md
• faq/Sql_faq.md
• loading/Load_to_Primary_Key_tables.md
• knowledge/usage/best-practice.md
• administration/management/BE_configuration.mdMinn Fong
06/16/2026, 7:28 PMRocky
06/16/2026, 7:29 PMfslib_write_io_latency (and its counterpart fslib_read_io_latency) Prometheus metrics in StarRocks are typically reported in microseconds (μs) in the raw Prometheus output.
Here are the specific details regarding these metrics:
* Raw Metric Unit: In the StarRocks Backend (BE) Prometheus exporter, duration and latency metrics that do not explicitly end in _ms (like disk_write_time_ms) or follow the Prometheus standard of seconds are almost always exported in microseconds (μs). This applies to the fslib (filesystem library) metrics used in shared-data (Starlet) clusters.
* Metric Types:
* `fslib_write_io_latency (average)`: Usually a counter that tracks the cumulative latency; to get the average, you divide the total latency by the operation count.
* `fslib_write_io_latency (quantile)`: A histogram/summary providing latency percentiles (p50, p90, etc.).
* Dashboard Representation: Most official StarRocks Grafana dashboards (such as the Starlet or Shared-data dashboards) apply a transformation (e.g., / 1000) to display these values in milliseconds (ms) for better readability.
Summary of common related units:
Metric Pattern | Typical Raw Unit
*_latency (e.g., fslib_write_io_latency) | Microseconds (μs)
*_duration (e.g., meta_request_duration) | Microseconds (μs)
*_time_ms (e.g., disk_write_time_ms) | Milliseconds (ms)
*_bytes_total | Bytes
Recommendation: To verify on your specific version, you can curl your BE metrics endpoint (curl http://<BE_IP>:<BE_HTTP_PORT>/metrics | grep fslib_write_io_latency) and compare the values to your current observed disk or S3 latency. If the numbers are in the thousands for a typical operation, they are in microseconds.
References
• administration/management/monitoring/metrics.md
• administration/management/monitoring/metrics-shared-data.mdMinn Fong
06/16/2026, 7:29 PMRocky
06/16/2026, 7:30 PM_ms suffix (like disk_write_time_ms) are recorded in microseconds (μs).
* 7–19 μs = 0.007–0.019 ms.
2. Why the values seem "too low"
You mentioned that 7–19 microseconds sounds "too fast" for a database write. The reason is explained in the StarRocks documentation for this specific metric:
"Please note that this value may appear lower because this metric monitors only data written to the buffer."In a shared-data (compute-node) architecture,
fslib_write_io_latency tracks the time it takes for the application to write data into the internal Starlet buffer (memory or local disk cache). It does not include the time taken to upload that data to the object store (like S3).
* Because you are essentially measuring a memory-to-memory or memory-to-local-cache transfer, a latency of ~10 microseconds is perfectly normal and expected.
3. How to see the "Real" Write Latency (S3/Object Store)
If you want to see the actual latency of the data being persisted to your object storage (the "slow" part of the write), you should look at the S3 IO Metrics instead:
* Metric: fslib_s3_single_upload_latency or fslib_s3_multi_upload_latency.
* Query:
promql
sum(rate(fslib_s3_single_upload_latency_sum[$interval])) / sum(rate(fslib_s3_single_upload_latency_count[$interval]))
* Expected Values: These will likely be in the range of 20,000 to 100,000+ μs (20ms to 100ms+), which reflects actual network round-trips to S3.
Summary
* Query: Correct.
* Unit: Microseconds (μs).
* Observation: The values are low because they only measure the "buffer" phase of the write, not the "upload" phase.
References
• administration/management/monitoring/metrics-shared-data.md
• administration/management/monitoring/metrics.md
References
• administration/management/monitoring/metrics.md
• administration/management/monitoring/metrics-shared-data.mdMinn Fong
06/16/2026, 7:36 PMRocky
06/16/2026, 7:37 PMCLOUD_NATIVE persistent indexes, which store the primary key index on S3. This solves the memory-exhaustion issue that wide tables often cause in shared-nothing clusters.
* Write Throughput: It can handle much higher total write volumes (GB/s) because it isn't limited by the IOPS of a single local disk.
* Compaction Isolation: You can scale Compute Nodes (CN) independently to handle the heavy compaction work without affecting your query performance.
The Real "Solution" for Wide Tables
Regardless of the cluster architecture (Shared-nothing or Shared-data), the fix for a 550-column table is usually one of these three:
1. Row Mode + Batching: As discussed, for a table this wide, Row Mode is significantly more stable. It avoids the complex "column linking" metadata. If you batch your updates to once every 60 seconds, the IO cost of rewriting the 550 columns is usually lower than the metadata cost of Column Mode.
2. Hybrid Row-Column Storage: This is a newer StarRocks feature (enabled via __row column) specifically designed for this. It stores a row-oriented copy of the data alongside the columnar one. It makes partial updates extremely fast because it only has to update one "row" block rather than linking 550 separate column files.
3. Vertical Sharding: If possible, split the 550 columns into two tables: one "Hot" table with the 20-50 columns that update frequently, and one "Cold" table for the rest.
Summary: Switching to shared-data is great for scaling and elasticity, but it won't fix the metadata overhead of updating a wide table in Column Mode. You would still need to optimize your update mode and batching interval.
References
• deployment/shared_data/feature-support-shared-data.md
• loading/Load_to_Primary_Key_tables.md
• knowledge/usage/load/shared_data_batch_ingestion.md
• table_design/table_types/primary_key_table.md
• best_practices/primarykey_table.md