Definition
- MATCH_RECOGNIZE – The ANSI SQL construct for complex event processing, detecting patterns across ordered rows.
- PATTERN – A regex-like expression over named row variables with quantifiers like
A B+ C. - DEFINE – Gives each variable a boolean condition that a row must satisfy to be bound to that variable.
- MEASURES – Computes output columns from the matched rows using navigation functions like FIRST and LAST.
- AFTER MATCH SKIP – Controls where matching resumes after a match, affecting overlapping detection and state growth.
The National Bank of India's trading desk wants to spot a shape in a stock's price: the price drops, stays low for a while, then recovers. MATCH_RECOGNIZE is like giving a watchful analyst a pattern written in shorthand: "first a down tick, then one or more low ticks, then an up tick." The analyst scans the ordered stream and raises a flag every time that exact shape appears.
Understanding the Concepts
What is MATCH_RECOGNIZE?
MATCH_RECOGNIZE is the ANSI SQL construct for complex event processing in Flink SQL. It sits in the FROM clause over a table and detects patterns across a sequence of rows.
-- Detect a drop-low-recovery pattern in stock prices
SELECT *
FROM stock_prices
MATCH_RECOGNIZE (
PARTITION BY symbol
ORDER BY event_time
MEASURES
A.price AS start_price,
LAST(B.price) AS lowest_price,
C.price AS recovery_price,
A.event_time AS pattern_start,
C.event_time AS pattern_end
ONE ROW PER MATCH
AFTER MATCH SKIP PAST LAST ROW
PATTERN (A B+ C)
DEFINE
A AS A.price > 100,
B AS B.price < A.price,
C AS C.price >= A.price
);
What are the clause components?
Each clause in MATCH_RECOGNIZE has a specific role.
-- Breaking down each clause:
SELECT * FROM loan_defaults
MATCH_RECOGNIZE (
-- Split into independent sequences per loan
PARTITION BY loan_id
-- Order by time (required for pattern detection)
ORDER BY payment_date
-- Output columns from matched rows
MEASURES
A.payment_date AS first_missed,
LAST(B.payment_date) AS last_missed,
C.payment_date AS partial_payment_date,
COUNT(B.payment_date) AS consecutive_misses
-- One summary row per match
ONE ROW PER MATCH
-- Resume matching after the last matched row
AFTER MATCH SKIP PAST LAST ROW
-- Pattern: one missed payment, then more missed, then a partial payment
PATTERN (A B+ C)
-- Conditions for each variable
DEFINE
A AS A.amount_paid = 0,
B AS B.amount_paid = 0,
C AS C.amount_paid > 0 AND C.amount_paid < C.emi_due
);
How do quantifiers work?
Quantifiers control how many rows each variable can match.
-- Quantifier examples:
-- A exactly one row
-- B+ one or more rows (greedy)
-- B* zero or more rows (greedy)
-- B? zero or one row
-- B{3} exactly 3 rows
-- B{2,5} between 2 and 5 rows
-- Detect 3 or more consecutive failed payments on a loan
SELECT * FROM payment_events
MATCH_RECOGNIZE (
PARTITION BY loan_id
ORDER BY payment_date
MEASURES
FIRST(A.payment_date) AS first_failure,
LAST(A.payment_date) AS last_failure,
COUNT(A.payment_date) AS failure_count
ONE ROW PER MATCH
AFTER MATCH SKIP PAST LAST ROW
PATTERN (A{3,})
DEFINE
A AS A.status = 'FAILED'
);
How does DEFINE reference earlier matched rows?
Conditions in DEFINE can reference the current row and previously matched rows, enabling relative logic.
package com.bank.flink;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;
public class MatchRecognizeDemo {
public static void main(String[] args) {
TableEnvironment tEnv = TableEnvironment.create(
EnvironmentSettings.inStreamingMode());
// Register stock_prices table with event-time watermark
// ...
// Detect drop-then-recovery pattern
tEnv.executeSql(
"SELECT * FROM stock_prices " +
"MATCH_RECOGNIZE (" +
" PARTITION BY symbol " +
" ORDER BY event_time " +
" MEASURES " +
" A.price AS start_price, " +
" LAST(B.price) AS low_price, " +
" C.price AS recovery_price " +
" ONE ROW PER MATCH " +
" AFTER MATCH SKIP PAST LAST ROW " +
" PATTERN (A B+ C) " +
" DEFINE " +
" B AS B.price < A.price, " + // B references A's price
" C AS C.price >= A.price" + // C references A's price
")"
);
}
}
What does AFTER MATCH SKIP do?
AFTER MATCH SKIP controls where matching resumes after a match completes. It affects overlapping matches and state growth.
-- AFTER MATCH SKIP PAST LAST ROW:
-- Resume matching after the last row of the current match.
-- No overlapping matches. Lower state cost.
-- AFTER MATCH SKIP TO NEXT ROW:
-- Resume from the row after the first row of the match.
-- Allows overlapping matches. Higher state cost.
-- AFTER MATCH SKIP TO LAST B:
-- Resume from the last row bound to variable B.
-- Useful for chained patterns.
What is the banking use case for loan default detection?
At the National Bank of India, MATCH_RECOGNIZE detects a sequence of missed payments followed by a partial payment, signaling a loan at risk of default.
-- Loan default early warning pattern
CREATE TABLE emi_payments (
loan_id STRING,
emi_amount DECIMAL(15, 2),
paid_amount DECIMAL(15, 2),
due_date TIMESTAMP(3),
WATERMARK FOR due_date AS due_date - INTERVAL '1' DAY
) WITH ('connector' = 'kafka', 'topic' = 'nbi-emi-payments',
'format' = 'json',
'properties.bootstrap.servers' = 'kafka.bank.internal:9092');
SELECT * FROM emi_payments
MATCH_RECOGNIZE (
PARTITION BY loan_id
ORDER BY due_date
MEASURES
FIRST(A.due_date) AS first_missed_date,
LAST(A.due_date) AS last_missed_date,
COUNT(A.due_date) AS missed_count,
B.paid_amount AS partial_amount
ONE ROW PER MATCH
AFTER MATCH SKIP PAST LAST ROW
PATTERN (A{2,} B)
DEFINE
A AS A.paid_amount = 0,
B AS B.paid_amount > 0 AND B.paid_amount < B.emi_amount
);
Example match:
Loan L-001:
Jan: paid 0 → A (missed)
Feb: paid 0 → A (missed)
Mar: paid 0 → A (missed)
Apr: paid 5000 → B (partial, less than EMI of 15000)
Match: first_missed=Jan, last_missed=Mar, missed_count=3, partial=5000
What about state management?
Partial-match state can grow if patterns are open-ended. Bounding the pattern and using AFTER MATCH SKIP PAST LAST ROW keeps state finite.
-- WRONG: unbounded pattern with no skip control
-- PATTERN (A B*) with SKIP TO NEXT ROW
-- → keeps many partial matches alive → state explosion
-- CORRECT: bounded pattern with skip past last row
-- PATTERN (A B{1,10} C) with SKIP PAST LAST ROW
-- → at most 10 rows per partial match → bounded state
What should a senior engineer know?
- MATCH_RECOGNIZE runs on event time with watermarks, so the ORDER BY column must have a watermark.
- Greedy quantifiers (default) match as many rows as possible. Reluctant quantifiers match as few.
- MATCH_RECOGNIZE is the declarative alternative to Flink's CEP library and hand-written ProcessFunctions.
- Very dynamic patterns that change at runtime are a limitation: the pattern is compiled statically.
- ONE ROW PER MATCH produces a summary; ALL ROWS PER MATCH produces one row per matched input.
// Senior tip: EXPLAIN the MATCH_RECOGNIZE query to see the physical plan
tEnv.executeSql("EXPLAIN SELECT * FROM stock_prices MATCH_RECOGNIZE (...)");
// Shows the NFA operator and state requirements
Visual Overview
MATCH_RECOGNIZE Clause Structure
SELECT * FROM table_name
MATCH_RECOGNIZE (
┌─────────────────────────────────────┐
│ PARTITION BY symbol │ ← split per key
│ ORDER BY event_time │ ← time ordering
│ │
│ MEASURES │ ← output columns
│ A.price AS start_price │
│ LAST(B.price) AS low_price │
│ C.price AS recovery_price │
│ │
│ ONE ROW PER MATCH │ ← output shape
│ AFTER MATCH SKIP PAST LAST ROW │ ← resume point
│ │
│ PATTERN (A B+ C) │ ← regex pattern
│ │
│ DEFINE │ ← conditions
│ B AS B.price < A.price │
│ C AS C.price >= A.price │
└─────────────────────────────────────┘
)
Pattern Matching Timeline (Drop-Low-Recovery)
Price ▲
150 │ ●(A)
140 │ ●
120 │ ●(B)
110 │ ●(B)
100 │ ●(B)
130 │ ●
155 │ ●(C)
└──────────────────────────────────▶ Time
A B B B C
start ───sustained low─── recovery
Key Takeaway
MATCH_RECOGNIZE is the ANSI SQL construct for complex event processing in Flink SQL. It detects patterns across ordered rows using a regex-like PATTERN clause with named variables and quantifiers, a DEFINE clause giving each variable a boolean condition, and a MEASURES clause computing output columns from matched rows. It runs on event time with watermarks and tracks partial matches per partition in keyed state.
The canonical use case is detecting sequences like a price drop followed by a sustained low followed by a recovery. You write PATTERN (A B+ C), define B as a price below A's starting price, and define C as a recovery back above A. AFTER MATCH SKIP PAST LAST ROW prevents overlapping re-detection and keeps partial-match state bounded.
At the National Bank of India, we used MATCH_RECOGNIZE for loan default early warning: detecting two or more consecutive missed EMI payments followed by a partial payment. The pattern fired the instant the shape completed, and because it ran on event time with watermarks, matches were deterministic and auditable. Bounding the pattern with a maximum count kept state manageable.
Remember one sentence: MATCH_RECOGNIZE turns complex event processing into declarative SQL by detecting regex-like sequence patterns across an ordered, partitioned stream with PATTERN, DEFINE, and MEASURES clauses.
---