CREATE OR ALTER MATERIALIZED TABLE Statement in Confluent Cloud for Apache Flink
Confluent Cloud for Apache Flink® enables evolving a materialized table in place by using the
CREATE OR ALTER MATERIALIZED TABLE statement. This statement creates a new
materialized table if it does not exist, or triggers an evolution of an
existing one.
An evolution updates the continuous query, schema, or both, while keeping the same output topic. This automates the complex manual process of stopping a statement, carrying over offsets, and migrating downstream consumers.
For details on table definition options like schemas, WITH properties, and DISTRIBUTED BY, see CREATE MATERIALIZED TABLE.
Syntax
CREATE OR ALTER MATERIALIZED TABLE [catalog_name.][db_name.]table_name
[(
{ <physical_column_definition> |
<metadata_column_definition> |
<computed_column_definition> }[ , ...n]
[ <watermark_definition> ]
[ <table_constraint> ][ , ...n]
)]
[COMMENT table_comment]
[DISTRIBUTED BY (column_name1, column_name2, ...) INTO n BUCKETS]
[WITH (key1=value1, key2=value2, ...)]
[START_MODE = <start_mode_value>]
AS <select_query>
Description
When you run CREATE OR ALTER MATERIALIZED TABLE:
If no table exists with that name, a new materialized table is created, the same behavior as CREATE MATERIALIZED TABLE.
If a materialized table already exists, an evolution is triggered.
If a regular table already exists, Flink adopts it as a materialized table in place. For details, see Adopt an existing table.
How evolution works
An evolution performs an in-place migration:
The existing continuous query is stopped.
A new continuous query is created with the updated query, schema, and configuration.
The new query begins processing data according to the
START_MODEsetting.Results are written to the same output Kafka topic as before.
State handling
All existing Flink processing state, including aggregation counts, join state,
and window state, is discarded when an evolution is triggered. A new state
is built from scratch based on the reprocessing settings specified by
START_MODE.
For stateful queries, like GROUP BY aggregations, the results are
recalculated by reprocessing the source data, not by migrating the previous
state.
Important behavior
Not idempotent: Running the same
CREATE OR ALTERcommand always triggers a new evolution, even if nothing has changed. Use caution when running this command in automated scripts to avoid unintentional reprocessing.Concurrent evolutions are rejected: If an evolution is already in progress for the same materialized table, a second
CREATE OR ALTERcommand is rejected with an error.No automatic rollback: If the new query fails at runtime, for example, due to a permission error or a UDF failure, the materialized table enters a FAILED state. The previous version is not restored automatically.
Adopt an existing table as a materialized table
You can turn a table that already exists into a materialized table, instead of creating a new topic. This helps when the destination topic was created outside Flink, for example by a platform team through Terraform or a CI/CD pipeline. In that case you want Flink to keep the topic up to date without recreating it. In Confluent Cloud every Kafka topic already appears in Flink as a table.
Declare the schema explicitly when you adopt a table. Matching the columns, types, primary key, and watermark the topic already has keeps the schema registered in Schema Registry unchanged, so the contract that consumers rely on does not change. If you let the query infer the schema instead, it can write a different schema to Schema Registry. For more on this choice, see Explicit and inferred schemas.
The recommended flow is to print the existing definition first, then declare the
same schema in a CREATE OR ALTER MATERIALIZED TABLE statement with the query
that keeps the table up to date:
SHOW CREATE TABLE user_spending;
CREATE OR ALTER MATERIALIZED TABLE user_spending (
customer_id INT,
total_spend DOUBLE
) AS
SELECT customer_id, SUM(price) AS total_spend
FROM examples.marketplace.orders
GROUP BY customer_id;
When you adopt a table, some things carry over and some are defined fresh:
The topic and its data stay. The materialized table reuses the same Kafka topic and keeps the data already in it. Any consumer reading that topic keeps working.
The schema must stay compatible with the existing topic. Flink checks the schema your statement produces against the schema already registered for the topic. If the schemas are not compatible, for example if a column type changes or a nullable column becomes
NOT NULL, Flink rejects the conversion. Declare the columns, primary key, and watermark you want to keep, because anything the query leaves out is not carried over. The safe way is to runSHOW CREATE TABLEon the existing table first and paste its exact column list and constraints into theCREATE OR ALTER. Declaring the schema this way keeps the schema already registered for the topic in Schema Registry unchanged, because the statement reuses it instead of letting the query redefine it. Letting the query define the schema risks writing a different schema to Schema Registry and changing the contract that consumers rely on. This risk is highest for an inferred table whose schema lives only in Schema Registry. You can omit the columns only when the schema the query produces already matches the topic. For why declaring the schema explicitly matters, see Explicit and inferred schemas.Processing starts fresh. The materialized table runs a new query with empty state. It does not resume from the old table’s job, so the
RESUME_OR_*start modes fall back to their plain start point. A stateful query, such as aGROUP BY, rebuilds its results by reprocessing the source. Use START_MODE to control how much history is reprocessed.
How adoption treats the existing writer
Adopting a table acts on the Kafka topic behind it. It does not take over whatever is currently writing into that topic. A running statement is a separate object with its own processing state and its own read position, and today you cannot move a running statement into a materialized table.
For example, suppose a long-running statement keeps user_spending up to
date:
INSERT INTO user_spending
SELECT customer_id, SUM(price) AS total_spend
FROM examples.marketplace.orders
GROUP BY customer_id;
When you adopt user_spending as a materialized table, Flink starts a new
managed query for it. The original INSERT INTO statement is untouched:
It keeps running until you stop it, so two jobs write to the same topic.
Its aggregation state, the running
SUMper customer, is not handed to the materialized table. The new query starts with empty state.Its read position in the source is not carried over. The new query reads according to START_MODE, so
FROM_NOWcan miss rows written during the switch andFROM_BEGINNINGreprocesses everything.
Recommended approach: stop the statement that writes to the topic first,
then run CREATE OR ALTER MATERIALIZED TABLE to adopt it.
If you adopt without stopping the old writer, both jobs produce into the same topic at once, with these side effects:
Wrong results on an upsert table. The two jobs write rows for the same keys from different positions and overwrite each other, so the values are inconsistent.
Duplicate rows on an append-only table. Both jobs append the same results.
Double the cost. You pay for two jobs, each consuming at least one Confluent Flink Unit (CFU), until you stop the old one.
Adoption puts a materialized table on a topic that already exists. It is not a way to migrate a running pipeline.
A few more things to know when you adopt an existing table:
Nullability and types must match, not only column names. A common rejection is a column the topic stores as nullable that the query produces as
NOT NULL, for exampleSUMover aNOT NULLsource column. Re-declare the column with the same nullability the topic already has.SHOW CREATE TABLEprints exactly what to match.Offsets are not carried over from a previous statement. You can’t continue from the exact point where a stopped statement left off. A full reprocess repeats rows already in the topic and
FROM_NOWcan miss data written during the gap. Adoption is not a clean migration path for an existing statement. Use it to put a materialized table on a topic that already exists, not to move a running statement.Adoption is one-way. After a table becomes a materialized table, you can’t turn it back into a regular table. You can still use it like a regular table by suspending the managed job with
ALTER MATERIALIZED TABLE ... SUSPENDand then writing to it withINSERT INTO.A view can’t be adopted. A view has no backing topic, so there is nothing to adopt.
If the new query can’t start, you get a materialized table with no query attached. Fix the issue and run
CREATE OR ALTER MATERIALIZED TABLEagain or write to the table withINSERT INTO.
START_MODE
The START_MODE clause controls how much historical data is processed when
a materialized table is created or evolved. It answers the question: “What data
should the new query logic process?”
If START_MODE is omitted, the default is RESUME_OR_FROM_BEGINNING.
Value |
Description |
|---|---|
|
Reprocesses all available data from the source, starting from the earliest available offset. |
|
Processes only new data arriving after the command is executed. No historical data is reprocessed. |
|
Reprocesses data starting from a specific absolute timestamp. Specify
the timestamp as a |
|
Reprocesses data from a relative time in the past, evaluated at the
time the command is executed. Specify the value as an |
|
On ALTER (existing table): Attempts to resume from the previous
job’s last position. On CREATE (new table): Falls back to
|
|
On ALTER: Attempts to resume from the previous job’s last
position. On CREATE: Falls back to |
|
On ALTER: Attempts to resume from the previous job’s last
position. On CREATE: Falls back to |
|
On ALTER: Attempts to resume from the previous job’s last
position. On CREATE: Falls back to
|
Behavior on CREATE vs ALTER
The RESUME_OR_* options behave differently depending on whether the
materialized table is being created for the first time or being evolved:
On CREATE: No previous job exists, so the
RESUMEpart has no effect. The system uses the fallback behavior (FROM_BEGINNING,FROM_NOW,FROM_TIMESTAMP, orFROM_NOW(INTERVAL '<interval>')).On ALTER: The system first attempts to resume from the previous job’s savepoint, starting exactly where the old job stopped. If resume is not possible, for example, because the query change is incompatible, it falls back to the specified mode.
Interaction with source retention
For any option that reprocesses historical data, like FROM_BEGINNING or
FROM_TIMESTAMP, if the specified start point is older than the earliest
data available in the source, for example, due to topic retention policies,
the job starts processing from the earliest available data point. No error
is raised.
Examples
Add a column to an existing materialized table
CREATE OR ALTER MATERIALIZED TABLE enriched_orders (
`order_id` STRING,
`customer_id` INT,
`price` DOUBLE,
`product_id` STRING
) AS
SELECT order_id, customer_id, price, product_id
FROM examples.marketplace.orders;
The output topic schema is updated to include product_id. Data is
reprocessed based on the START_MODE (default:
RESUME_OR_FROM_BEGINNING).
Change query logic
Add a filter to an existing materialized table:
CREATE OR ALTER MATERIALIZED TABLE enriched_orders (
`order_id` STRING,
`customer_id` INT,
`price` DOUBLE,
`product_id` STRING
) AS
SELECT order_id, customer_id, price, product_id
FROM examples.marketplace.orders
WHERE price > 25.00;
Use START_MODE for full reprocessing
Force a full reprocessing of all available historical data:
CREATE OR ALTER MATERIALIZED TABLE enriched_orders
START_MODE = FROM_BEGINNING
AS
SELECT order_id, customer_id, price
FROM examples.marketplace.orders;
Use START_MODE with a relative interval
Reprocess only the last seven days of data:
CREATE OR ALTER MATERIALIZED TABLE enriched_orders
START_MODE = FROM_NOW(INTERVAL '7' DAY)
AS
SELECT order_id, customer_id, price
FROM examples.marketplace.orders;
Downstream consumer considerations
Because evolutions discard state and rebuild from scratch, downstream consumers might observe specific behaviors. For detailed guidance, see Downstream consumer impact.
Upsert mode: Keys that are filtered out by the new query logic have no delete message emitted. Old records remain downstream as stale “zombie” data.
Append-only mode: Reprocessed records appear alongside the original records, resulting in duplicates in the output topic.
Retract mode: The new job emits
INSERTmessages for the reprocessed data but does not emitDELETEmessages for the old result set.
Limitations
For the full list of current limitations, see Materialized Tables limitations.