MATCH_RECOGNIZE
MATCH_RECOGNIZE finds sequences of rows that match a regular-expression-like pattern. Use it for funnels, customer journeys, state transitions, sessions, anomaly shapes, and other ordered event-stream problems that are difficult to express with self-joins and window functions.
Run these statements through Altertable's SQL engine.
First example
Suppose an event table contains one row for each user action:
WITH events(user_id, ts, event) AS (VALUES(1, TIMESTAMP '2024-01-01 10:00:00', 'view'),(1, TIMESTAMP '2024-01-01 10:01:00', 'click'),(1, TIMESTAMP '2024-01-01 10:02:00', 'view'),(2, TIMESTAMP '2024-01-01 10:00:00', 'view'))SELECT *FROM eventsMATCH_RECOGNIZE (PARTITION BY user_idORDER BY tsMEASURESA.ts AS view_ts,B.ts AS click_tsPATTERN (A B)DEFINEA AS event = 'view',B AS event = 'click')ORDER BY user_id, view_ts;
This query finds a view immediately followed by a click for each user:
PARTITION BY user_idcreates an independent sequence for each user.ORDER BY tsdefines the order in which rows are consumed.AandBare pattern variables that describe row roles.DEFINEspecifies which rows qualify for each role.PATTERN (A B)requires two consecutive rows: anArow followed by aBrow.MEASURESdefines the columns in each result row.
Matches are non-overlapping. After a match is emitted, matching resumes after the last consumed row. Rows that cannot start a complete match are skipped.
Operator syntax
The supported FROM-clause shape is:
FROM input_relation [input_alias]MATCH_RECOGNIZE ([PARTITION BY partition_column [, ...]][ORDER BY input_column [ASC | DESC] [NULLS FIRST | NULLS LAST] [, ...]][MEASURES measure_expression AS output_name [, ...]][ONE ROW PER MATCH | ALL ROWS PER MATCH[SHOW EMPTY MATCHES | OMIT EMPTY MATCHES | WITH UNMATCHED ROWS]][AFTER MATCH SKIPPAST LAST ROW | TO NEXT ROW | TO FIRST variable | TO LAST variable]PATTERN (row_pattern) [WITHIN interval_expression][SUBSET subset_name = (variable [, ...]) [, ...]][DEFINE pattern_variable AS boolean_expression [, ...]])[match_alias]
PATTERN is required. ORDER BY, MEASURES, PARTITION BY, SUBSET, and DEFINE are optional, but the query must produce at least one output column.
The source relation must appear immediately before MATCH_RECOGNIZE. The result can be aliased after the clause:
SELECT m.user_id, m.start_tsFROM events AS eMATCH_RECOGNIZE (PARTITION BY user_idORDER BY tsMEASURES A.ts AS start_tsPATTERN (A)DEFINE A AS event = 'view') AS m;
Common SQL constructs such as CTEs, subqueries, joins, outer filters, grouping, and aliases can surround the operator.
Define the row sequence
PARTITION BY
PARTITION BY splits the input into independent streams. A pattern never crosses a partition boundary:
PARTITION BY user_id, channel
The partition columns appear in the output before the measure columns. Without PARTITION BY, the entire input is treated as one partition.
ORDER BY
ORDER BY defines the sequence that the pattern consumes:
ORDER BY ts, sequence_number
It supports input columns and ASC, DESC, NULLS FIRST, and NULLS LAST. If the primary ordering column can tie, add a stable secondary column so matching is deterministic.
When ORDER BY is omitted, rows keep their input order within each partition. Add an outer ORDER BY when result order matters, especially when the query uses multiple partitions or multiple threads.
DEFINE
DEFINE maps pattern variables to Boolean predicates:
DEFINEA AS event = 'view',B AS event = 'click',C AS event NOT IN ('view', 'click')
The predicate is evaluated against the current input row. A NULL predicate does not match; only TRUE qualifies. An undefined pattern variable matches every row, but explicitly defining every variable makes the query intent clearer.
Write patterns
Pattern variables represent row roles. The pattern language supports:
A— one row matching variableAA B— concatenationA | B— alternation(A B)— groupingA*,A+,A?— greedy repetitionA{n},A{n,},A{n,m}— bounded repetitionA*?,A+?,A??, and similar forms — reluctant repetitionPERMUTE(A, B, …)— any ordering of the listed variables^and$— partition-start and partition-end anchors{- A -}— exclusion fromALL ROWS PER MATCHoutput
Patterns can be composed:
PATTERN ((A B) | (A C* B) | A)
Alternatives are evaluated from left to right. In the example above, a complete A B match is preferred, then A C* B, then the single-row A fallback. Repetition is greedy by default.
Return match results
MEASURES
MEASURES defines the output columns. Every expression needs an alias:
MEASURESA.ts AS start_ts,B.ts AS end_ts,B.ts - A.ts AS elapsed
In a measure, A.column returns the value from the last row assigned to A. An unqualified column such as ts returns the value from the last row of the whole match.
Pattern expressions also support navigation and pattern functions, including:
PREV,NEXT,FIRST, andLASTCLASSIFIER()MATCH_NUMBER()- Pattern aggregates such as
COUNT,SUM,AVG,MIN, andMAX - Top-level
RUNNINGandFINALprefixes, such asRUNNING LAST(value)orFINAL COUNT(*)
If a variable did not participate in the selected alternative, referencing it produces NULL.
Rows per match
ONE ROW PER MATCH is the default and emits one summary row for each successful match:
ONE ROW PER MATCH
ALL ROWS PER MATCH emits one row for each matched input row. It supports modifiers for empty and unmatched rows:
ALL ROWS PER MATCH SHOW EMPTY MATCHESALL ROWS PER MATCH OMIT EMPTY MATCHESALL ROWS PER MATCH WITH UNMATCHED ROWS
With ONE ROW PER MATCH, the output contains partition columns followed by measure columns. With ALL ROWS PER MATCH, the output also includes order columns and remaining input columns.
AFTER MATCH SKIP
Choose where matching resumes after a match:
AFTER MATCH SKIP PAST LAST ROWAFTER MATCH SKIP TO NEXT ROWAFTER MATCH SKIP TO FIRST variableAFTER MATCH SKIP TO LAST variable
PAST LAST ROW is the default. A skip target that does not advance the search, or that refers to a missing variable, raises an error.
SUBSET
Create a union variable that refers to multiple pattern variables:
SUBSET milestone = (A, B)
Union variables can be used in MEASURES, DEFINE, and AFTER MATCH SKIP TO FIRST or TO LAST.
Bound a match by time
Add WITHIN after PATTERN to require that a match completes within an interval:
PATTERN (A B C) WITHIN INTERVAL '30' SECOND
The interval is inclusive: the last timestamp can equal the first timestamp plus the interval. WITHIN requires an ascending temporal first ORDER BY key, such as DATE, TIMESTAMP, or TIMESTAMPTZ. The interval must be a non-null, strictly positive, foldable DuckDB INTERVAL.
Practical patterns
Funnel with filler events
Use a repeated variable when unrelated events may occur between two milestones:
SELECT *FROM eventsMATCH_RECOGNIZE (PARTITION BY user_idORDER BY tsMEASURESA.ts AS view_ts,B.ts AS click_tsPATTERN (A C* B)DEFINEA AS event = 'view',B AS event = 'click',C AS event NOT IN ('view', 'click'))ORDER BY user_id, view_ts;
C* consumes zero or more filler rows. It cannot consume rows that do not match C, and the trailing B still needs to complete the pattern.
Preferred path with a fallback
Use an alternation when a longer match should win, but a shorter match should still produce a result:
SELECT *FROM eventsMATCH_RECOGNIZE (PARTITION BY user_idORDER BY tsMEASURESA.ts AS start_ts,B.ts AS click_tsPATTERN ((A B) | A)DEFINEA AS event = 'view',B AS event = 'click')ORDER BY user_id, start_ts;
When a view is followed by a click, (A B) wins and consumes both rows. When no click follows, the query falls back to A, producing a row with click_ts = NULL.
Pattern recognition in windows
Pattern recognition is also supported in named or inline window structures. A pattern window produces one result row per input row:
SELECTuser_id,value OVER w,label OVER wFROM eventsWINDOW w AS (PARTITION BY user_idORDER BY tsMEASURESRUNNING LAST(value) AS value,CLASSIFIER() AS labelROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWINGAFTER MATCH SKIP PAST LAST ROWSEEKPATTERN (A B+)DEFINE B AS value < PREV(B.value));
Window pattern frames must start at CURRENT ROW. The frame can end at CURRENT ROW, a constant n FOLLOWING, or UNBOUNDED FOLLOWING. INITIAL and SEEK, AFTER MATCH SKIP, and SUBSET are supported in pattern windows.
Anchors and MATCH_NUMBER() are not supported in windows. Ordinary window functions over the matched frame currently cover SUM, COUNT, AVG, MIN, and MAX.
Learn more
- SQL Explorer: run and refine pattern queries interactively.
- SQL engine: learn about Altertable's DuckDB SQL dialect.