All Products
Search
Document Center

Data Lake Formation:Paimon primary key tables

Last Updated:May 15, 2026

Learn about the core configurations of DLF-managed Paimon primary key tables, including bucketing, merge engine, changelog producer, and DLF storage optimization modes, to help you choose the right settings for your workload.

Quick reference

Category

Scenario

Recommended configuration

Bucketing

  • Acceptable latency of 1–3 minutes normally, occasionally 5–10 minutes.

  • Partition sizes vary significantly.

  • Prefer automatic bucket calculation.

Postponed bucket (default)

Requires immediate data visibility after commit in streaming jobs.

  • Fixed bucket ('bucket' = '<num>'), and

  • 'deletion-vectors.enabled' = 'false' (default)

Primary key does not fully contain the partition key and table size < 100 GB.

Dynamic bucket ('bucket' = '-1')

Deletion vector

High-performance queries with StarRocks or Hologres.

'deletion-vectors.enabled' = 'true'

Merge engine

New data fully replaces old data.

'merge-engine' = 'deduplicate' (default)

  • Only some columns are updated per record.

  • Multi-stream table join is involved.

'merge-engine' = 'partial-update'

Data aggregation is required.

'merge-engine' = 'aggregation'

Only the first record per primary key should be kept.

'merge-engine' = 'first-row'

Changelog producer

Downstream does not consume the stream.

'changelog-producer' = 'none' (default)

'merge-engine' = 'deduplicate' is set (default), and downstream only needs the latest state.

'changelog-producer' = 'none' (default)

'scan.remove-normalize' = 'true'

'merge-engine' = 'partial-update' or 'merge-engine' = 'aggregation' is set, and downstream only needs the delta, not full rows.

'changelog-producer' = 'none' (default)

Upstream provides complete changelog (e.g. database binlog), 'merge-engine' = 'deduplicate' is set, and downstream needs complete changelog.

'changelog-producer' = 'input'

All other scenarios where downstream needs complete changelog.

'changelog-producer' = 'lookup'

Storage optimization mode

Acceptable latency of 1–3 minutes normally, occasionally 5–10 minutes.

Resource-latency balanced (default)

Willing to use more resources to reduce latency from resource adjustments.

Latency-first

Acceptable latency of about 30 minutes, priority is saving resources.

Resource-first

Create a primary key table

A Paimon table with a defined primary key is a Paimon primary key table. The following SQL creates a partitioned primary key table with partition key dt and primary key columns dt, shop_id, and user_id.

Flink SQL

CREATE TABLE T (
  dt STRING,
  shop_id BIGINT,
  user_id BIGINT,
  num_orders INT,
  total_amount INT,
  PRIMARY KEY (dt, shop_id, user_id) NOT ENFORCED
) PARTITIONED BY (dt);

Spark SQL

CREATE TABLE T (
  dt STRING,
  shop_id BIGINT,
  user_id BIGINT,
  num_orders INT,
  total_amount INT
) PARTITIONED BY (dt) TBLPROPERTIES (
  'primary-key' = 'dt,shop_id,user_id'
);

Each row in a Paimon primary key table has a unique primary key. When multiple records with the same primary key are written, they are merged according to the configured merge engine.

The following sections describe the configuration and usage of each core feature: bucketing, deletion vectors, merge engines, and changelog producers.

Bucketing

A bucket is the smallest unit for read and write operations on Paimon primary key tables. Data in a non-partitioned table, or data within each partition of a partitioned table, is divided into multiple buckets to enable parallel reads and writes.

Postponed bucket (default)

To create a postponed-bucketing table, omit the bucket property or set 'bucket' = '-2'.

Data is first written to a bucket-postpone staging directory of a partition. DLF then determines the optimal bucket count for each partition based on data volume, moves the data into the target bucket directories, and compacts small files.

Postponed bucketing is a DLF-enhanced feature with the following advantages:

  • Automatic bucketing: Bucket count is calculated automatically based on data volume and throughput, with no manual configuration required.

  • Dynamic adjustment: Bucket count adjusts automatically when data characteristics change significantly.

  • Partition-level bucketing: Different partitions can use different bucket counts to accommodate varying partition sizes.

  • Concurrent writes: Multiple jobs can write concurrently without conflicts.

Bucket count calculation

The following formulas describe how DLF determines bucket counts.

# Row size factor prevents issues with high compression ratios (e.g. many null values)
row_size_factor = min(1, total_rows / total_file_size / column_count)

file_based_buckets = max(
    partition_total_file_size / (1.3 * per_bucket_file_size),
    max_bucket_total_file_size / (1.7 * per_bucket_file_size)
)

# Final bucket count is rounded up to the nearest power of 2, max 2048
final_bucket_count = max(
    file_based_buckets,
    partition_total_rows / per_bucket_row_count,
    max_bucket_total_rows / (1.7 * per_bucket_row_count),
    min(throughput / per_bucket_throughput, 4 * file_based_buckets, 128)
)

per_bucket_file_size = 768MB

per_bucket_throughput = 1536MB/h

######################################################################
# Without deletion vectors
# Without 'changelog-producer' = 'lookup'
######################################################################

per_bucket_row_count = +infinity

######################################################################
# All other cases
######################################################################

per_bucket_row_count = 40_000_000

Bucket count adjustment is triggered when:

  • Partition total size > 2 × per_bucket_file_size × bucket_count

  • Any single bucket size > 2 × per_bucket_file_size

  • Current bucket count < 0.125 × target bucket count, and partition size ≤ 64 GB

Data distribution

By default, each record is assigned to a bucket based on the hash of its primary key.

Custom distribution

To customize data distribution, set the bucket-key property. For example, 'bucket-key' = 'c1,c2' distributes records based on columns c1 and c2.

Note
  • Separate column names with commas.

  • The primary key must fully contain the bucket-key.

  • Choose bucket-key columns with evenly distributed values. Skewed values concentrate data in a few buckets, degrading read and write performance.

Data visibility

Data visibility for postponed-bucketing tables depends on the write method. DLF enables batch write to fixed bucket when the following conditions are met, making data visible immediately after commit:

  • Deletion vectors are not enabled, and data is written via Flink batch jobs or Spark jobs using INSERT INTO or INSERT OVERWRITE.

  • Deletion vectors are enabled, and data is written via Flink batch jobs or Spark jobs using INSERT OVERWRITE.

In all other cases, data becomes visible after DLF processes it, typically within 1–3 minutes. If DLF is performing resource or bucket count adjustments, visibility may take 5–10 minutes. You can select a specific storage optimization mode to reduce adjustment frequency. See Storage optimization modes for details.

Note

When batch write to fixed bucket is enabled, DLF bypasses the bucket-postpone temporary directory and writes data directly into fixed buckets:

  • Control parameter: 'postpone.batch-write-fixed-bucket'. Set to true to enable the feature.

  • Supported engine versions: Realtime Compute for Apache Flink VVR 11.4 or later, EMR Serverless Spark esr-4.7.0 or later.

  • New partition bucket count: When batch write to fixed bucket is enabled and you write to a new partition, the partition bucket count is automatically set to the Flink job parallelism or Spark partition count (max 2048). DLF may adjust this later.

  • Existing partition write conflicts: When batch write to fixed bucket is enabled and you write to an existing partition, if DLF adjusts the partition's bucket count during the write, the write job will fail due to conflicts. Set 'postpone.batch-write-fixed-bucket' = 'false' to disable this feature and prevent conflicts.

  • Deletion vectors tables: For tables with deletion vectors enabled, disable 'postpone.batch-write-fixed-bucket' when performing INSERT INTO operations via Flink batch or Spark jobs.

Data consistency

  • Single-job writes: For data written by the same job, DLF processes records in write order, providing sequential consistency.

    Note

    A job restarted from a checkpoint or savepoint with unchanged parallelism is treated as the same job.

  • Multiple-job writes: For data written by different jobs, the final table state may contain a mix of both jobs’ results, but no data is lost, providing snapshot isolation consistency. To control merge order, configure a sequence field. See Handling out-of-order data.

Fixed bucket

Set 'bucket' = '<num>' to specify a fixed bucket count. <num> must be a positive integer.

Note

Too few buckets limit job parallelism and cause excessive data per bucket, degrading read/write performance. However, too many buckets create excessive small files. We recommend a target of approximately 1 GB per bucket.

Data distribution

Same as postponed bucketing. See Data distribution.

Data visibility

Visibility depends on whether deletion vectors are enabled:

  • Without deletion vectors: data is visible immediately after commit.

  • With deletion vectors: data becomes visible after the DLF optimization job processes it, typically within 1–3 minutes.

Dynamic bucket

Set 'bucket' = '-1' to enable dynamic bucket.

Note

Limitations:

  • Dynamic bucket mode does not support concurrent writes from multiple jobs.

  • For tables larger than 100 GB, dynamic bucketing incurs significant overhead and is not recommended.

Data distribution

Dynamic-bucketing tables write data to existing buckets and automatically create new buckets when a bucket exceeds its capacity. The following properties control behavior:

Property

Type

Default

Description

dynamic-bucket.target-row-num

Long

2000000

Maximum rows per bucket.

dynamic-bucket.initial-buckets

Integer

-

Initial bucket count. If unset, defaults to the writer operator parallelism.

When dynamic bucket is configured, update performance and resource usage depend on whether the primary key fully includes the partition key:

Non-cross-partition updates

When the primary key fully contains the partition key, DLF can determine the partition but not the bucket for each key, so it maintains an in-memory index mapping keys to bucket IDs. This consumes approximately 1 GB of heap memory per 100 million primary keys. Only actively written partitions consume heap memory.

Apart from heap memory usage, non-cross-partition dynamic bucketing shows no significant performance difference from other bucketing modes.

Cross-partition updates

When the primary key does not fully contain the partition key, DLF uses RocksDB to maintain a mapping from keys to partitions and bucket IDs. This may cause notable performance overhead for large tables, and job startup is slower due to full index loading.

Additionally, the merge engine affects cross-partition update behavior:

  • deduplicate: data is deleted from the old partition and inserted into the new partition.

  • aggregation and partial-update: data is updated in the old partition, ignoring the new partition key.

  • first-row: new data is discarded if a record with the same primary key already exists.

Data visibility

Same as fixed bucket. See Data visibility.

Deletion vectors

Set 'deletion-vectors.enabled' = 'true' when creating the Paimon primary key table.

Deletion vectors are generated during small-file compaction to improve query performance on primary key tables. This setting is ideal for query-sensitive or read-heavy workloads.

Note

Merge engines

When multiple records with the same primary key are written, Paimon merges them based on the merge-engine property. Supported values: deduplicate (default), partial-update, aggregation, and first-row.

Handling out-of-order data

By default, Paimon uses input order to determine merge priority — the last written record is treated as the latest. For out-of-order data, set 'sequence.field' = '<column-name>' to merge records by the specified column in ascending order.

Supported sequence field types: TINYINT, SMALLINT, INTEGER, BIGINT, TIMESTAMP, TIMESTAMP_LTZ.

deduplicate (default)

With 'merge-engine' = 'deduplicate', only the latest record per primary key is retained. If the latest record is a delete message, all records with that key are removed.

Example:

CREATE TABLE T (
  k INT,
  v1 DOUBLE,
  v2 STRING,
  PRIMARY KEY (k) NOT ENFORCED
) WITH (
  'merge-engine' = 'deduplicate' -- deduplicate is the default, can be omitted
);

Write the following records in order:

  • +I(1, 2.0, 'apple')

  • +I(1, 4.0, 'banana')

  • +I(1, 8.0, 'cherry')

SELECT * FROM T WHERE k = 1 returns (1, 8.0, 'cherry').

Write the following records in order:

  • +I(1, 2.0, 'apple')

  • +I(1, 4.0, 'banana')

  • -D(1, 4.0, 'banana')

SELECT * FROM T WHERE k = 1 returns no rows.

aggregation

With 'merge-engine' = 'aggregation', records with the same primary key are aggregated using specified functions. Each non-key column requires fields.<field-name>.aggregate-function; the default is last_non_null_value.

Example:

CREATE TABLE T (
  product_id BIGINT,
  price DOUBLE,
  sales BIGINT,
  PRIMARY KEY (product_id) NOT ENFORCED
) WITH (
  'merge-engine' = 'aggregation',
  'fields.price.aggregate-function' = 'max',
  'fields.sales.aggregate-function' = 'sum'
);

Write the following records in order:

  • +I(1, 23.0, 15)

  • +I(1, 30.2, 20)

SELECT * FROM T WHERE product_id = 1 returns (1, 30.2, 35).

Supported aggregate functions

  • sum: Computes the sum. Types: DECIMAL, TINYINT, SMALLINT, INTEGER, BIGINT, FLOAT, DOUBLE.

  • product: Computes the product. Types: DECIMAL, TINYINT, SMALLINT, INTEGER, BIGINT, FLOAT, DOUBLE.

  • max: Returns the maximum. Types: CHAR, VARCHAR, DECIMAL, TINYINT, SMALLINT, INTEGER, BIGINT, FLOAT, DOUBLE, DATE, TIME, TIMESTAMP, TIMESTAMP_LTZ.

  • min: Returns the minimum. Same types as max.

  • first_value: Returns the first input value (including null). All types.

  • first_not_null_value: Returns the first non-null input value. All types.

  • last_value: Returns the latest input value (including null). All types.

  • last_non_null_value (default): Returns the latest non-null input value. All types.

  • listagg: Concatenates strings with commas. Type: STRING.

  • bool_and / bool_or: Logical AND / OR. Type: BOOLEAN.

  • rbm32 / rbm64: Merges 32-bit / 64-bit RoaringBitmaps. Type: VARBINARY.

  • nested_update: Aggregates rows into an ARRAY. Set 'fields.<field-name>.nested-key' = 'pk0,pk1,...' to deduplicate by nested key (keeping the last entry).

  • nested_partial_update: Like nested_update but performs partial column updates within the ARRAY by nested key.

  • collect: Merges multiple ARRAYs into one. Set 'fields.<field-name>.distinct' = 'true' to deduplicate.

  • merge_map: Merges multiple MAPs, keeping the last value for duplicate keys.

Note

Only sum, product, last_value, last_non_null_value, nested_update, collect, and merge_map support retract messages (update_before and delete). Set 'fields.<field-name>.ignore-retract' = 'true' to make a column ignore retractions.

Example

Use nested_update to aggregate multiple sub orders:

-- Main orders table
CREATE TABLE orders (
  order_id BIGINT PRIMARY KEY NOT ENFORCED,
  user_name STRING,
  address STRING
);

-- Sub order table
CREATE TABLE sub_orders (
  order_id BIGINT,
  sub_order_id INT,
  product_name STRING,
  price BIGINT,
  PRIMARY KEY (order_id, sub_order_id) NOT ENFORCED
);

-- Wide table
-- Aggregate sub orders
-- And deduplicate sub orders based on sub_order_id
CREATE TABLE order_wide (
  order_id BIGINT PRIMARY KEY NOT ENFORCED,
  user_name STRING,
  address STRING,
  sub_orders ARRAY<ROW<sub_order_id BIGINT, product_name STRING, price BIGINT>>
) WITH (
  'merge-engine' = 'aggregation',
  'fields.sub_orders.aggregate-function' = 'nested_update',
  'fields.sub_orders.nested-key' = 'sub_order_id'
);

INSERT INTO order_wide

SELECT
  order_id,
  user_name,
  address,
  CAST (NULL AS ARRAY<ROW<sub_order_id BIGINT, product_name STRING, price BIGINT>>)
FROM orders

UNION ALL

SELECT
  order_id,
  CAST (NULL AS STRING),
  CAST (NULL AS STRING),
  ARRAY[ROW(sub_order_id, product_name, price)]
FROM sub_orders;

Assume the orders table has data:

  • (1, 'Alice', 'add1'),

  • (2, 'Bob', 'add2')

The sub_orders table has data:

  • (1, 11, 'apple', 10),

  • (1, 12, 'banana', 20),

  • (2, 21, 'cherry', 30),

  • (2, 22, 'peach', 40),

  • (2, 21, 'cherry', 50)

The order_wide table will have data:

  • (1, 'Alice', 'add1', [(11, 'apple', 10), (12, 'banana', 20)])

  • (2, 'Bob', 'add2', [(21, 'cherry', 50), (22, 'peach', 40)])

partial-update

With 'merge-engine' = 'partial-update', you can incrementally update a record through multiple messages. New data overwrites old data for the same primary key, except that null values in the new data do not overwrite existing values.

Note

partial-update cannot handle delete or update_before messages. Set 'ignore-delete' = 'true' to ignore them.

Example:

CREATE TABLE T (
  k INT,
  v1 DOUBLE,
  v2 BIGINT,
  v3 STRING,
  PRIMARY KEY (k) NOT ENFORCED
) WITH (
  'merge-engine' = 'partial-update'
);

Write the following records in order:

  • +I(1, 23.0, 10, NULL)

  • +I(1, NULL, NULL, 'This is a book')

  • +I(1, 25.2, NULL, NULL)

SELECT * FROM T WHERE k = 1 returns (1, 25.2, 10, 'This is a book').

Sequence groups

In addition to the global sequence field, you can use sequence groups to specify independent merge ordering for different column groups. This is useful for table-widening scenarios with multiple source tables.

Example:

CREATE TABLE T (
  k INT,
  a STRING,
  b STRING,
  g_1 INT,
  c STRING,
  d STRING,
  g_2 INT,
  PRIMARY KEY (k) NOT ENFORCED
) WITH (
  'merge-engine' = 'partial-update',
  'fields.g_1.sequence-group' = 'a,b',
  'fields.g_2.sequence-group' = 'c,d'
);

Columns a and b use g_1 as their merge sequence (higher value means newer data); columns c and d use g_2 as their merge sequence.

Table widening with aggregation

Within partial-update, you can also apply aggregate functions to specific columns by setting fields.<field-name>.aggregate-function.

Note

Example:

CREATE TABLE T (
  k INT,
  a STRING,
  b INT,
  g_1 INT,
  c STRING,
  d INT,
  g_2 INT,
  PRIMARY KEY (k) NOT ENFORCED
) WITH (
  'merge-engine' = 'partial-update',
  'fields.g_1.sequence-group' = 'a,b',
  'fields.b.aggregate-function' = 'max',
  'fields.g_2.sequence-group' = 'c,d',
  'fields.d.aggregate-function' = 'sum'
);

Columns a and b use g_1 as their merge sequence (higher value means newer data): a keeps the latest non-null value (defaults to last_non_null_value), b keeps the maximum. Columns c and d use g_2 as their merge sequence: c keeps the latest non-null value, d computes the sum.

first-row

With 'merge-engine' = 'first-row', DLF keeps only the first record per primary key. Compared to deduplicate, first-row produces only insert-type changelog and does so more efficiently.

Note

Limitations:

  • first-row cannot handle delete or update_before messages. Set 'ignore-delete' = 'true' to ignore them.

  • first-row does not support sequence fields.

Example:

CREATE TABLE T (
  k INT,
  v1 DOUBLE,
  v2 STRING,
  PRIMARY KEY (k) NOT ENFORCED
) WITH (
  'merge-engine' = 'first-row'
);

Write the following records in order:

  • +I(1, 2.0, 'apple')

  • +I(1, 4.0, 'banana')

  • +I(1, 8.0, 'cherry')

SELECT * FROM T WHERE k = 1 returns (1, 2.0, 'apple').

Changelog producers

The changelog-producer property controls how Paimon generates changelog for downstream streaming consumers. Common values: none (default), input, and lookup.

none (default)

Suitable when downstream does not consume the stream, or only needs incremental data or the latest state.

With 'changelog-producer' = 'none', downstream reads the incremental data from each Paimon snapshot. The same primary key may appear multiple times:

  • 'merge-engine' = 'deduplicate': downstream reads complete rows.

  • 'merge-engine' = 'partial-update': downstream reads only updated columns; others are null.

  • 'merge-engine' = 'aggregation': downstream reads only delta values for updated columns; others are null.

Note

When streaming with 'changelog-producer' = 'none', set 'scan.remove-normalize' = 'true' to remove the downstream Flink normalize operator, which is expensive. If you need complete changelog, use 'changelog-producer' = 'input' or 'changelog-producer' = 'lookup' instead.

input

Suitable for 'merge-engine' = 'deduplicate' when downstream needs complete changelog.

With 'changelog-producer' = 'input', Paimon passes input messages directly as changelog. This only works when the input stream is already complete changelog (e.g. database binlog).

The input mechanism involves no extra computation, making it more efficient than lookup.

lookup

Suitable for all scenarios where downstream needs complete changelog.

With 'changelog-producer' = 'lookup', the optimization job performs batch point lookups before each commit, triggering compaction and generating complete changelog from the results. Works regardless of whether the input stream is complete changelog.

The lookup mechanism requires extra computation, making it less efficient than input, but it is more broadly applicable.

Reducing unnecessary changelog

By default, Paimon generates changelog even when the updated value is identical to the previous value. Set 'changelog-producer.row-deduplicate' = 'true' to eliminate such no-op changelog. This applies only to the lookup mechanism.

Use this only when no-op changelog is common, as it introduces additional comparison overhead.

Storage optimization modes

DLF provides multiple storage optimization modes to balance data visibility and resource consumption. This setting is configured in the DLF console. See View and configure storage optimization strategies. DLF supports the following storage optimization modesL

  • Dynamic resources - Balanced (default): For postponed-bucketing tables, latency is typically 1–3 minutes; may reach 5–10 minutes during resource or bucket adjustments. For other tables with deletion vectors, latency is typically 1–3 minutes.

  • Dynamic resources - Low latency: Uses approximately 2× the resources of balanced mode to improve optimization job speed and minimize resource adjustments.

  • Dynamic resources - Low resource usage: Driven by data backpressure. Uses 1/3–1/2 of balanced mode resources, but latency may reach 30 minutes.

  • Fixed resources: The optimization job runs continuously without adjustments. You can configure resource allocation, compaction interval, and bucket count for postponed-bucketing tables. Recommended only for data catch-up when dynamic mode latency is high; not recommended for daily use.

    Note
    • The compaction interval refers to DLF optimization job's commit interval. The job continues running; it does not switch to scheduled mode.

    • After setting the bucket count for postponed-bucketing tables, new partitions use that count directly without adjustment.

Storage optimization resource consumption

The following formulas describe DLF optimization job resource consumption across different scenarios. Actual consumption is based on billing.

resource_latency_balanced, mode_factor = 1
latency_first, mode_factor = 0.5
resource_first, mode_factor = 4

# Row size factor prevents issues with high compression ratios
row_size_factor = min(1, total_rows / total_file_size / column_count)

######################################################################
# Without deletion vectors
# Without 'changelog-producer' = 'lookup'
######################################################################

parallelism = max(
    throughput / per_parallelism_throughput,
    active_buckets / (0.5 * per_parallelism_buckets)
)

per_parallelism_throughput = 12GB/h * mode_factor * row_size_factor

per_parallelism_buckets = 512 * mode_factor * row_size_factor

######################################################################
# With deletion vectors
# merge-engine unset or 'merge-engine' = 'deduplicate'
# Without 'changelog-producer' = 'lookup'
######################################################################

parallelism = max(
    throughput / per_parallelism_throughput,
    active_buckets / per_parallelism_buckets,
    active_partition_level_gt0_file_size / per_parallelism_lookup_file_size
)

per_parallelism_throughput = 3GB/h * mode_factor * row_size_factor

per_parallelism_buckets = 128 * mode_factor * row_size_factor

per_parallelism_lookup_file_size = 35GB / lookup_disk_factor
# Each CU provides 50GB local disk
lookup_file_cache_factor = min(
    1,
    active_partition_lookup_file_cache_size / active_partition_level_gt1_file_size
)

######################################################################
# All other cases
######################################################################

parallelism = max(
    throughput / per_parallelism_throughput,
    active_buckets / (0.5 * per_parallelism_buckets),
    active_partition_level_gt0_file_size / per_parallelism_lookup_file_size
)

per_parallelism_throughput = 3GB/h * mode_factor * row_size_factor

per_parallelism_buckets = 64 * mode_factor * row_size_factor

per_parallelism_lookup_file_size = max(35GB / lookup_disk_factor, 4GB)
# Typical range: 1.5 to 2.5
lookup_disk_factor = max(
    1,
    active_partition_lookup_file_cache_size / active_partition_level_gt1_file_size
)

Resource adjustment is triggered when:

  • Active buckets for a parallelism slot > per_parallelism_buckets

  • Throughput > 2 × per_parallelism_throughput × parallelism

  • Throughput < 0.5 × peak throughput for 12+ minutes

Additional notes on resource allocation

Note
  • Resource usage troubleshooting: High consumption for a single table is typically caused by overly fine-grained partitioning. Review and optimize your partition strategy.

  • Per-parallelism resources: Defaults to 1 CU. DLF may scale up to 2–4 CUs upon OOM events. Check the Health Diagnosis > Event Center on the table page.

  • Job manager resources: Each job requires an additional 1 CU job manager node (may scale to 2–4 CUs for high parallelism or OOM).

  • Small table sharing: Multiple small postpone-bucket tables may share a single optimization job (up to 32 tables) when:

    • Storage optimization mode is balanced or low resource usage.

    • Fewer than 8 active partitions.

    • Total active partition size ≤ 16 GB and don't fit in "All other cases" of Storage optimization resource consumption (or ≤ 4 GB and fit in "All other cases").

  • Low resource usage mode: Backpressure-driven; may show temporarily high usage, but overall 1/3–1/2 of balanced mode.