Key parameters
This topic describes key parameters for SQL development and provides explanations and usage examples.
table.exec.sink.keyed-shuffle
To address out-of-order issues when writing data to a table with a primary key, you can use the table.exec.sink.keyed-shuffle parameter to perform a hash shuffle. This operation ensures that records with the same primary key are routed to the same operator instance, which mitigates out-of-order issues.
Usage notes
-
The hash shuffle operation is effective only if the upstream operator can guarantee the order of update records for the same primary key. Otherwise, this operation cannot resolve the out-of-order issue.
-
If you modify the parallelism of an operator in expert mode, the following parallelism rules do not apply.
Available values
-
AUTO (default): If the sink parallelism is not 1 and differs from the upstream operator's parallelism, Flink automatically performs a hash shuffle on the primary key when data flows to the sink.
-
FORCE: If the sink parallelism is not 1, Flink forces a hash shuffle on the primary key when data flows to the sink.
-
NONE: Flink does not perform a hash shuffle based on the parallelism of the sink and the upstream operator.
Examples
-
Set the parameter to AUTO
-
Create an SQL streaming job, copy the following SQL code, and deploy the job. The code explicitly sets the sink parallelism to 2.
CREATE TEMPORARY TABLE s1 ( a INT, b INT, ts TIMESTAMP(3) ) WITH ( 'connector'='datagen', 'rows-per-second'='1', 'fields.ts.kind'='random','fields.ts.max-past'='5s', 'fields.b.kind'='random','fields.b.min'='0','fields.b.max'='10' ); CREATE TEMPORARY TABLE sink ( a INT, b INT, ts TIMESTAMP(3), PRIMARY KEY (a) NOT ENFORCED ) WITH ( 'connector'='print', --You can directly specify the sink parallelism by using the sink.parallelism parameter. 'sink.parallelism'='2' ); INSERT INTO sink SELECT * FROM s1; --You can also specify the sink parallelism by using dynamic table options. --INSERT INTO sink /*+ OPTIONS('sink.parallelism' = '2') */ SELECT * FROM s1; -
On the Job O&M page, on the Deployment Details tab, in the Resource Configuration section, set Parallelism to 1. In the Running Parameter Configuration section under Other Configurations, do not set the
table.exec.sink.keyed-shuffleparameter or explicitly addtable.exec.sink.keyed-shuffle: AUTO(both have the same effect). -
Start the job. On the Status tab, the data connection from the upstream operator to the sink is HASH.

-
-
Set the parameter to FORCE
-
Create an SQL streaming job, copy the following SQL code, and deploy the job. This code does not explicitly specify the sink parallelism.
CREATE TEMPORARY TABLE s1 ( a INT, b INT, ts TIMESTAMP(3) ) WITH ( 'connector'='datagen', 'rows-per-second'='1', 'fields.ts.kind'='random','fields.ts.max-past'='5s', 'fields.b.kind'='random','fields.b.min'='0','fields.b.max'='10' ); CREATE TEMPORARY TABLE sink ( a INT, b INT, ts TIMESTAMP(3), PRIMARY KEY (a) NOT ENFORCED ) WITH ( 'connector'='print' ); INSERT INTO sink SELECT * FROM s1; -
In the Resource Configuration section on the Deployment Details tab of the Job O&M page, set Parallelism to 2. In the Runtime Parameter Configuration section, add
table.exec.sink.keyed-shuffle: FORCEto Other Configurations. -
After you start the job, go to the Status tab. The parallelism for both the sink and the upstream operator is 2, and the data connection has changed to HASH.

-
table.exec.mini-batch.size
This parameter controls the maximum number of records that can be buffered for a micro-batch operation. When this number is reached, it triggers the computation and emits data. This parameter takes effect only when used with table.exec.mini-batch.enabled and table.exec.mini-batch.allow-latency. For more information about MiniBatch optimizations, see MiniBatch Aggregation and MiniBatch Regular Joins.
Usage notes
Before a job starts, if you do not explicitly set this parameter in the Parameters section, managed memory is used to buffer data in mini-batch mode. Any of the following conditions triggers the final computation and data output:
-
A watermark message is received from the MiniBatchAssigner operator.
-
The managed memory is full.
-
Before a checkpoint starts.
-
The job is stopped.
Available values
-
-1 (default): Indicates that managed memory is used to buffer data.
-
Other negative Long values: Same as the default setting.
-
Other positive Long values: Indicates that heap memory is used to buffer data. When the number of buffered records reaches this value (N), the system automatically triggers the output operation.
Example
-
Create an SQL streaming job, copy the following SQL code, and deploy the job.
CREATE TEMPORARY TABLE s1 ( a INT, b INT, ts TIMESTAMP(3), PRIMARY KEY (a) NOT ENFORCED, WATERMARK FOR ts AS ts - INTERVAL '1' SECOND ) WITH ( 'connector'='datagen', 'rows-per-second'='1', 'fields.ts.kind'='random', 'fields.ts.max-past'='5s', 'fields.b.kind'='random', 'fields.b.min'='0', 'fields.b.max'='10' ); CREATE TEMPORARY TABLE sink ( a INT, b BIGINT, PRIMARY KEY (a) NOT ENFORCED ) WITH ( 'connector'='print' ); INSERT INTO sink SELECT a, sum(b) FROM s1 GROUP BY a; -
On the Deployment Details tab of the Job O&M page, in the Other Configurations field of the Runtime Parameter Configuration section, set the
table.exec.mini-batch.enabled: trueandtable.exec.mini-batch.allow-latency: 2sparameters, and do not settable.exec.mini-batch.sizeto use its default value of -1. -
Start the job. On the Status tab, the job topology includes the MiniBatchAssigner, LocalGroupAggregate, and GlobalGroupAggregate operators.
table.exec.agg.mini-batch.output-identical-enabled
When State TTL is enabled, the MinibatchGlobalAgg and MinibatchAgg nodes do not send duplicate data downstream by default if the aggregation result does not change after data is consumed. This may cause the state of downstream stateful nodes to expire because they do not receive data from upstream for an extended period. This parameter controls whether to continue sending duplicate data downstream when State TTL is enabled and the aggregation result remains unchanged. You can set this parameter to true to make the MinibatchGlobalAgg and MinibatchAgg nodes send data in this case. If the aggregation result of your job changes more frequently than the configured State TTL, you do not need to manually set this parameter. For details about the community issue, see FLINK-33936.
Usage notes
-
This parameter is effective only in VVR 8.0.8 and later. In versions before VVR 8.0.8, the behavior is equivalent to setting this parameter to false.
-
When changing the value from false to true, the amount of data sent downstream from the MinibatchGlobalAgg and MinibatchAgg operators may increase, putting more pressure on downstream operators.
Available values
-
false (default): When state TTL is enabled, the MinibatchGlobalAgg and MinibatchAgg operators do not emit data downstream if the aggregation result does not change.
-
true: When state TTL is enabled, the MinibatchGlobalAgg and MinibatchAgg operators still emit updated (duplicate) records downstream even if the aggregation result does not change.
Example
-
Create an SQL streaming job, copy the following SQL code, and deploy the job.
create temporary table src( a int, b string ) with ( 'connector' = 'datagen', 'rows-per-second' = '10', 'fields.a.min' = '1', 'fields.a.max' = '1', 'fields.b.length' = '3' ); create temporary table snk( a int, max_length_b bigint ) with ( 'connector' = 'blackhole' ); insert into snk select a, max(CHAR_LENGTH(b)) from src group by a; -
In the Other Configurations section of the Runtime Parameter Configuration area on the Deployment Details tab of the Job O&M page, set the
table.exec.mini-batch.enabled: trueandtable.exec.mini-batch.allow-latency: 2sparameters to enable Minibatch Aggregate optimization. -
Start the job. On the Status tab, the job includes a MinibatchGlobalAggregate operator. Click the "+" sign on the operator to see that the GlobalGroupAggregate operator does not send data downstream when the aggregation result is unchanged.
The operator shows RecordsIn as 19 and RecordsOut as 1, which means that 19 input records produced only 1 aggregate output.
-
Stop the job, and add the parameter
table.exec.agg.mini-batch.output-identical-enabled: trueto Other Configurations in the Running Parameter Configuration section on the Deployment Details page of the Job O&M page. -
Start the job. On the Status tab, you can see the job includes a MinibatchGlobalAggregate operator. Click the "+" sign on the operator to observe that the GlobalGroupAggregate operator now sends data downstream even when the aggregation result is unchanged. After restarting the job, the Status tab shows that the GlobalGroupAggregate operator's RecordsIn and RecordsOut are both 94. This indicates that with
table.exec.agg.mini-batch.output-identical-enabled: trueenabled, the operator sends data downstream even if the aggregation result does not change.
table.exec.async-lookup.key-ordered-enabled
When you use a dimension table join for data enrichment, enabling asynchronous mode can often improve throughput. In a dimension table join, the table.exec.async-lookup.output-mode parameter and whether the input is an update stream determine the output order of asynchronous I/O operations.
|
Output mode |
Update stream |
Non-update stream |
|
ORDERED |
ordered mode |
ordered mode |
|
ALLOW_UNORDERED |
ordered mode |
unordered mode |
As shown in the table, the combination of an update stream and ALLOW_UNORDERED ensures correctness by using ordered mode, but it sacrifices some throughput. To optimize this scenario, the table.exec.async-lookup.key-ordered-enabled parameter was introduced. It balances the correctness semantics of an update stream with the throughput performance of asynchronous I/O. Messages in a stream that have the same update key (which can be considered the primary key of a changelog) are processed in the order they enter the operator.
-
Ordered mode: This mode preserves the order of the stream. Result messages are emitted in the same order that the asynchronous requests were triggered (the order in which messages enter the operator).
-
Unordered mode: Result messages are emitted as soon as the asynchronous request finishes. The asynchronous I/O operator changes the order of messages in the stream. For more information, see Asynchronous I/O | Apache Flink.
Use cases
-
Use this optimization to preserve per-key processing order in a dimension table join when the stream has few messages with the same update key over time.
-
In a Change Data Capture (CDC) stream with a primary key, you perform data enrichment with a dimension table join and write to a sink where the sink primary key matches the source primary key. The join key for the dimension table join is different from the primary key, and the dimension-side join key is the primary key. This optimization shuffles by the CDC primary key, which is derived as the update key. Compared with enabling the SHUFFLE_HASH optimization for the same scenario, it avoids generating a SinkMaterializer operator before the sink at higher parallelism. This avoids potential performance issues from that operator, especially the large state that can accumulate during long runs. For information about SinkUpsertMaterializer, see Recommendations.
-
The join key for the dimension table join is different from the primary key, the dimension-side join key is the primary key, and a rank operator follows. This optimization shuffles by the CDC primary key, which is derived as the update key. Compared with enabling the SHUFFLE_HASH optimization for the same scenario, it prevents UpdateFastRank from degrading to RetractRank. For information about how to optimize RetractRank to UpdateFastRank, see TopN optimization techniques.
Usage notes
-
If the stream has no update key, the entire row is used as the key.
-
Throughput decreases if the same update key is updated frequently within a short period, because records for the same update key are processed in strict order.
-
Compared with the original asynchronous dimension table join, Key-Ordered mode introduces keyed state. Enabling or disabling this mode affects state compatibility.
-
This feature takes effect only for VVR 8.0.10 and later versions when the input of the dimension table join is an update stream and you configure
table.exec.async-lookup.output-mode='ALLOW_UNORDERED'andtable.exec.async-lookup.key-ordered-enabled='true'.
Available values
-
false (default): Disables Key-Ordered mode.
-
true: Enables Key-Ordered mode.
Example
-
The following example uses an asynchronous Hologres dimension table join. Create an SQL streaming job, copy the following SQL code, and deploy the job.
For more information about the Hologres connector, see Hologres.
create TEMPORARY table bid_source( auction BIGINT, bidder BIGINT, price BIGINT, channel VARCHAR, url VARCHAR, dateTime TIMESTAMP(3), extra VARCHAR, proc_time as proctime(), WATERMARK FOR dateTime AS dateTime - INTERVAL '4' SECOND ) with ( 'connector' = 'kafka', -- A non-insert-only stream connector 'topic' = 'user_behavior', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'testGroup', 'scan.startup.mode' = 'earliest-offset', 'format' = 'csv' ); CREATE TEMPORARY TABLE users ( user_id STRING PRIMARY KEY NOT ENFORCED, -- Define the primary key user_name VARCHAR(255) NOT NULL, age INT NOT NULL ) WITH ( 'connector' = 'hologres', -- A connector that supports asynchronous lookup 'async' = 'true', 'dbname' = 'holo db name', --The name of your Hologres database 'tablename' = 'schema_name.table_name', --The name of the Hologres table to receive data 'username' = 'access id', --The AccessKey ID of your Alibaba Cloud account 'password' = 'access key', --The AccessKey Secret of your Alibaba Cloud account 'endpoint' = 'holo vpc endpoint', --The VPC endpoint of your Hologres instance ); CREATE TEMPORARY TABLE bh ( auction BIGINT, age int ) WITH ( 'connector' = 'blackhole' ); insert into bh SELECT bid_source.auction, u.age FROM bid_source JOIN users FOR SYSTEM_TIME AS OF bid_source.proc_time AS u ON bid_source.channel = u.user_id; -
On the Job O&M page, on the Deployment Details tab, in the Other Configurations section of the Runtime Parameter Configuration area, set the
table.exec.async-lookup.output-mode='ALLOW_UNORDERED'andtable.exec.async-lookup.key-ordered-enabled='true'parameters. -
Start the job. On the Status tab, you can see that the job's async attribute is KEY_ORDERED:true.
table.optimizer.window-join-enabled
This parameter controls whether to enable window join operations. When enabled, Flink optimizes the corresponding execution plan as a window join. For small windows, this reduces state overhead and improves performance. Compared with a regular join, a window join can also avoid emitting update messages to downstream operators, which is useful for use cases that require joins with small time-window conditions.
Usage notes
-
A window join has additional limitations on SQL syntax compared to a regular join, and it does not support update streams.
-
A window join has higher output latency than a regular join. The latency depends on the window size and how fast source watermarks advance.
-
When enabled, an event-time window join discards late data, whereas a regular join does not.
-
After you change this parameter, you cannot resume from an existing checkpoint because the underlying state structures of the two execution methods are incompatible.
Available values
-
false (default): Statements for a window join are converted to a regular join for execution.
-
true: Enables window join. The corresponding statements are converted to a window join for execution.
Example
-
Create an SQL streaming job, copy the following SQL code, set the
table.optimizer.window-join-enabledparameter totrueby using a SET statement, and then execute the SQL text to view the execution plan.SET 'table.optimizer.window-join-enabled' = 'true'; CREATE TEMPORARY TABLE LeftTable ( id VARCHAR, row_time TIMESTAMP_LTZ(3), num INT, WATERMARK FOR row_time as row_time - INTERVAL '5' SECONDS ) WITH ( 'connector'='datagen' ); CREATE TEMPORARY TABLE RightTable ( id VARCHAR, row_time TIMESTAMP_LTZ(3), num INT, WATERMARK FOR row_time as row_time - INTERVAL '10' SECONDS ) WITH ( 'connector'='datagen' ); EXPLAIN SELECT L.num as L_Num, L.id as L_Id, R.num as R_Num, R.id as R_Id, COALESCE(L.window_start, R.window_start) as window_start, COALESCE(L.window_end, R.window_end) as window_end FROM ( SELECT * FROM TABLE(TUMBLE(TABLE LeftTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)) ) L JOIN ( SELECT * FROM TABLE(TUMBLE(TABLE RightTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)) ) R ON L.num = R.num AND L.window_start = R.window_start AND L.window_end = R.window_end;The Optimized Execution Plan section of the output shows that the plan contains a WindowJoin operator.
== Optimized Execution Plan == Calc(select=[num AS L_Num, id AS L_Id, num0 AS R_Num, id0 AS R_Id, CASE(window_start IS NOT NULL, window_start, window_start0) AS window_start, CASE(window_end IS NOT NULL, window_end, window_end0) AS window_end]) +- WindowJoin(leftWindow=[TUMBLE(win_start=[window_start], win_end=[window_end], size=[5 min])], rightWindow=[TUMBLE(win_start=[window_start], win_end=[window_end], size=[5 min])], joinType=[InnerJoin], where=[(num = num0)], select=[id, num, window_start, window_end, id0, num0, window_start0, window_end0]) :- Exchange(distribution=[hash[num]]) : +- Calc(select=[id, num, window_start, window_end]) : +- WindowTableFunction(window=[TUMBLE(time_col=[row_time], size=[5 min])]) : +- WatermarkAssigner(rowtime=[row_time], watermark=[(row_time - 5000:INTERVAL SECOND)]) : +- TableSourceScan(table=[[vvp, default, LeftTable]], fields=[id, row_time, num]) +- Exchange(distribution=[hash[num]]) +- Calc(select=[id, num, window_start, window_end]) +- WindowTableFunction(window=[TUMBLE(time_col=[row_time], size=[5 min])]) +- WatermarkAssigner(rowtime=[row_time], watermark=[(row_time - 10000:INTERVAL SECOND)]) +- TableSourceScan(table=[[vvp, default, RightTable]], fields=[id, row_time, num]) -
Modify the SET statement in the SQL code to set the
table.optimizer.window-join-enabledparameter tofalseor delete the SET statement, and then execute the SQL text to view the modified execution plan.-- set to 'false' or remove this setting clause SET 'table.optimizer.window-join-enabled' = 'false'; CREATE TEMPORARY TABLE LeftTable ( id VARCHAR, row_time TIMESTAMP_LTZ(3), num INT, WATERMARK FOR row_time as row_time - INTERVAL '5' SECONDS ) WITH ( 'connector'='datagen' ); CREATE TEMPORARY TABLE RightTable ( id VARCHAR, row_time TIMESTAMP_LTZ(3), num INT, WATERMARK FOR row_time as row_time - INTERVAL '10' SECONDS ) WITH ( 'connector'='datagen' ); EXPLAIN SELECT L.num as L_Num, L.id as L_Id, R.num as R_Num, R.id as R_Id, COALESCE(L.window_start, R.window_start) as window_start, COALESCE(L.window_end, R.window_end) as window_end FROM ( SELECT * FROM TABLE(TUMBLE(TABLE LeftTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)) ) L JOIN ( SELECT * FROM TABLE(TUMBLE(TABLE RightTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)) ) R ON L.num = R.num AND L.window_start = R.window_start AND L.window_end = R.window_end;The Optimized Execution Plan section of the output no longer contains a WindowJoin operator. The operation is now a regular join.
== Optimized Execution Plan == Calc(select=[num AS L_Num, id AS L_Id, num0 AS R_Num, id0 AS R_Id, CASE(window_start IS NOT NULL, window_start, window_start0) AS window_start, CASE(window_end IS NOT NULL, window_end, window_end0) AS window_end]) +- Join(joinType=[InnerJoin], where=[((num = num0) AND (window_start = window_start0) AND (window_end = window_end0))], select=[id, num, window_start, window_end, id0, num0, window_start0, window_end0], leftInputSpec=[NoUniqueKey], rightInputSpec=[NoUniqueKey]) :- Exchange(distribution=[hash[num, window_start, window_end]]) : +- Calc(select=[id, num, window_start, window_end]) : +- WindowTableFunction(window=[TUMBLE(time_col=[row_time], size=[5 min])]) : +- WatermarkAssigner(rowtime=[row_time], watermark=[(row_time - 5000:INTERVAL SECOND)]) : +- TableSourceScan(table=[[vvp, default, LeftTable]], fields=[id, row_time, num]) +- Exchange(distribution=[hash[num, window_start, window_end]]) +- Calc(select=[id, num, window_start, window_end]) +- WindowTableFunction(window=[TUMBLE(time_col=[row_time], size=[5 min])]) +- WatermarkAssigner(rowtime=[row_time], watermark=[(row_time - 10000:INTERVAL SECOND)]) +- TableSourceScan(table=[[vvp, default, RightTable]], fields=[id, row_time, num])
Related documentation
Why does data get stuck in the LocalGroupAggregate operator with no output?