Enrich a Stream with Reference Data in Confluent Cloud for Apache Flink
Confluent Cloud for Apache Flink® can enrich each row of a table with the current version of a reference table, such as a table of customers, products, or rates that changes far less often than the table you’re enriching. For example, an order carries a customer ID, but the report you want shows the customer’s tier and country.
This guide shows how to enrich a table of orders with the current customer details by using a LATERAL SNAPSHOT join. You create a small reference table, join an orders table against it, and then update the reference data to see the new values flow through.
Prerequisites
Access to Confluent Cloud.
A Kafka cluster to hold the tables you create in this guide.
A Flink compute pool in the same region as the cluster.
Why not a regular join?
A regular join keeps every row from both sides in state forever and revises earlier results whenever either side changes. For enrichment, that means state that grows without limit and a stream of corrections instead of one clean row per order.
A LATERAL SNAPSHOT join keeps the reference table in state, plus any orders that arrive while it first loads that table. It joins each order a single time, against the customer details that are current at that moment, and emits one row per order that it never revises.
Create the tables
Create a customers reference table keyed by customer ID and an orders
table.
CREATE TABLE customers (
customer_id INT,
tier STRING,
country STRING,
PRIMARY KEY (customer_id) NOT ENFORCED
);
CREATE TABLE orders (
order_id INT,
customer_id INT,
amount DECIMAL(10, 2)
);
Because customers has a primary key, each new row for a customer replaces
the previous one. orders has no primary key, so it’s an append-only table,
which is what the join needs on the side it enriches.
Add two customers:
INSERT INTO customers VALUES
(1, 'gold', 'US'),
(2, 'silver', 'UK');
Enrich the orders
Start the join. It needs two arguments:
The
inputargument names the reference table to look values up in, herecustomers.The
on_timeargument names the column that records when Flink wrote each customer row, here$rowtime. The join uses it to apply changes to customers in the right order. Every table has this $rowtime system column, so you don’t need to add it yourself.
SELECT o.order_id, o.customer_id, o.amount, c.tier, c.country
FROM orders AS o
LEFT JOIN LATERAL SNAPSHOT(
input => TABLE customers,
on_time => DESCRIPTOR($rowtime),
load_completed_idle_timeout => INTERVAL '1' MINUTE
) AS c
ON o.customer_id = c.customer_id;
The join first reads the whole customers table, then starts joining. It
treats the table as fully read when the table’s watermark reaches the time you
started the statement. Both of your rows are older than that, so the third
argument, load_completed_idle_timeout, tells the join to
stop waiting after the table’s watermark has stopped moving for one minute. To
learn why you need it, see Why you need the idle timeout argument.
To skip the wait, insert another customer row instead. Its timestamp is later than the time you started the statement, so the watermark can move past that time and the join finishes loading. The watermark can trail the new row, so loading can take a little longer than the insert.
While the query runs, add three orders in a separate statement:
INSERT INTO orders VALUES
(101, 1, 20.00),
(102, 2, 35.50),
(103, 3, 12.00);
About a minute after you started the query, the join finishes loading. It enriches the orders that arrived meanwhile and emits them together:
order_id customer_id amount tier country
======== =========== ====== ====== =======
101 1 20.00 gold US
102 2 35.50 silver UK
103 3 12.00 NULL NULL
Order 103 is for a customer who isn’t in the reference table. The LEFT JOIN
keeps the order and fills the customer columns with NULL. A plain JOIN
would drop the order.
Update the reference data
Suppose customer 1 moves up to the platinum tier. Insert the new version of their row:
INSERT INTO customers VALUES (1, 'platinum', 'US');
The join doesn’t apply this change immediately. It first waits until it’s
sure no earlier changes to customers are still on their way, which takes
about 10 seconds here. Wait that long, then place another order for that
customer:
INSERT INTO orders VALUES (104, 1, 50.00);
The new order picks up the new tier:
order_id customer_id amount tier country
======== =========== ====== ======== =======
104 1 50.00 platinum US
Order 101 still shows gold, because the join never revises an order it
has already emitted.
Behind the wait, the join holds each change until the customers watermark
moves past it, so it applies changes in time order. When a table gets no new
data, its watermark moves only after Flink detects that the table is idle. That
takes about 10 seconds on a new statement and grows to as much as five minutes on
one that has run for a while. The join keeps joining orders the whole time,
against the version it already holds. This delay doesn’t depend on the
load_completed_idle_timeout argument, which only decides when loading ends.
For more on idleness detection, see
Validate or disable idleness handling.
Timing decides which version an order sees. An order that the join processes before an update reaches it gets the earlier version, so the same data can give slightly different results from one run to the next.
Why you need the idle timeout argument
Before the join enriches any orders, it reads all the rows the reference table held when you started the statement. That way, the join never matches an order against a partly loaded table. To tell when it has read them all, the join watches the table’s watermark, a timestamp that moves forward as new rows arrive. After the watermark reaches the time you submitted the statement, the join treats the table as fully loaded.
Reference tables are often static or slowly updating. They receive their rows up front, then updates now and then. When no new rows arrive, the watermark stops at the time of the last row and never reaches the time you submitted the statement. The join keeps waiting to finish loading, holds every order, and returns nothing, while the statement still shows as running.
The load_completed_idle_timeout argument solves this. When the reference
table’s watermark has stopped moving for the length of time you set, which
usually means the table has stopped receiving new data, the join treats the
table as fully loaded and starts joining. It isn’t set by default, so set it
whenever your reference table might be static or slowly updating.
Choose the timeout value
Pick a value longer than any pause you expect while the join first reads the reference table. About a minute suits most reference tables.
Too short: If the first read pauses for longer than the timeout, the join starts early and joins some orders against a partly loaded table.
Too long: You wait longer for the first results.
A large reference table doesn’t need a longer timeout on its own. While the join is still reading rows, the watermark usually keeps moving, so the wait only starts after new data stops arriving.
If you’d rather name the exact point that completes the load, set the
load_completed_time argument to a timestamp instead. The load finishes when
the reference table’s watermark reaches that time. This suits reference tables
that keep receiving updates, where the watermark reliably moves past the time
you choose.
Troubleshoot missing results
If the join returns no results, check these in order.
- Is the idle timeout argument set?
A static or slowly updating reference table without the
load_completed_idle_timeoutargument is the most common cause. Stop the statement and run it again with the argument set.- Is the join still loading?
Open the statement in the Query Profiler. If the join’s state keeps growing while nothing comes out and the reference table’s watermark has stopped moving, the join is still in its load phase.
- Do the join keys match?
An inner
JOINdrops orders that have no matching reference row. Switch toLEFT JOINto see those orders withNULLvalues.