SQL22 min readBy Priyanshu Pandey

Advanced SQL Analytics: Row Pattern Matching (MATCH_RECOGNIZE)

Master Oracle SQL's MATCH_RECOGNIZE clause. Learn how to perform complex row pattern matching, detect trends, sessionize data, find consecutive gaps, and analyze time-series data without procedural PL/SQL code.

Analytics · SQL Mastery Series

Advanced SQL: Row Pattern Matching (MATCH_RECOGNIZE)

Finding patterns across consecutive rows used to require complex self-joins or slow procedural loops. With MATCH_RECOGNIZE, you can search for trends, detect anomalies, and sessionize data using regular expressions directly in SQL.

22 min read📅June 27, 2026✍️Priyanshu Pandey📚SQL Mastery Series
THE PROBLEM

1. Why We Need Pattern Matching

Imagine you have a table of daily stock prices or retail sales. You are tasked with finding a "V-Shape" recovery: a pattern where sales drop for three consecutive days, and then immediately rise for three consecutive days.

Historically, solving this in SQL was a nightmare. You would need multiple layers of LAG() and LEAD() window functions, complex self-joins, or you would simply give up and write a procedural PL/SQL loop.

In Oracle 12c, the ISO SQL standard MATCH_RECOGNIZE clause was introduced. It allows you to search for sequences of rows matching a specific condition using Regular Expressions.

SYNTAX

2. The Anatomy of MATCH_RECOGNIZE

The MATCH_RECOGNIZE clause sits directly after the FROM or JOIN clause. It processes the data in a pipeline.

MATCH_RECOGNIZE Basic Structure
SQL
SELECT *
FROM daily_sales
MATCH_RECOGNIZE (
  PARTITION BY store_id          -- 1. How to group the data
  ORDER BY sales_date            -- 2. How to sort the data
  MEASURES                       -- 3. What columns to output
    STRT.sales_date AS start_date,
    LAST(UP.sales_date) AS end_date
  ONE ROW PER MATCH              -- 4. Output granularity (1 row per pattern vs all rows)
  AFTER MATCH SKIP TO LAST UP    -- 5. Where to start searching for the next pattern
  PATTERN (STRT DOWN+ UP+)       -- 6. The Regex Pattern!
  DEFINE                         -- 7. Define what the variables mean
    DOWN AS DOWN.sales_amt < PREV(DOWN.sales_amt),
    UP   AS UP.sales_amt > PREV(UP.sales_amt)
)

Understanding the Clauses

  1. PARTITION BY / ORDER BY: Just like window functions, you must define the boundaries (partition) and chronological sequence (order) of the data stream.
  2. PATTERN: The heart of the query. You assign arbitrary variable names (e.g., STRT, DOWN, UP) and use regex quantifiers:
    • * (0 or more times)
    • + (1 or more times)
    • ? (0 or 1 time)
    • {n,m} (between n and m times)
  3. DEFINE: Here you map the arbitrary variables to logical conditions. PREV() lets you look at the previous row.
  4. MEASURES: What to SELECT when a match is found.
USE CASES

3. Real-World Retail Use Cases

Use Case 1: Detecting Inventory Stockouts

Find instances where an item's inventory dropped below zero, stayed negative for at least two days, and then recovered to positive.

Detecting Stockouts
SQL
SELECT item_id, stockout_start, recovery_date, duration_days
FROM inventory_history
MATCH_RECOGNIZE (
  PARTITION BY item_id
  ORDER BY snapshot_date
  MEASURES
    STRT.snapshot_date AS stockout_start,
    RECOVER.snapshot_date AS recovery_date,
    MATCH_NUMBER() AS match_id,
    COUNT(NEGATIVE.*) AS duration_days
  ONE ROW PER MATCH
  PATTERN (STRT NEGATIVE+ RECOVER)
  DEFINE
    STRT     AS STRT.soh < 0,
    NEGATIVE AS NEGATIVE.soh < 0,
    RECOVER  AS RECOVER.soh >= 0
);

Use Case 2: Sessionization (Web Analytics)

In e-commerce, a "session" is a series of clicks by a user. If a user is inactive for more than 30 minutes, a new session begins.

Sessionizing Clickstream Data
SQL
SELECT user_id, session_id, start_time, end_time, page_views
FROM clickstream
MATCH_RECOGNIZE (
  PARTITION BY user_id
  ORDER BY click_time
  MEASURES
    MATCH_NUMBER() AS session_id,
    FIRST(click_time) AS start_time,
    LAST(click_time) AS end_time,
    COUNT(*) AS page_views
  ONE ROW PER MATCH
  PATTERN (STRT CLICKS*)
  DEFINE
    CLICKS AS CLICKS.click_time - PREV(CLICKS.click_time) <= INTERVAL '30' MINUTE
);
💡

MATCH_NUMBER()

The built-in MATCH_NUMBER() function sequentially assigns an ID to every pattern matched. It is incredibly useful for grouping rows into discrete sessions or events.

Use Case 3: Finding Missing Sequence Gaps

If you have a sequence of invoice numbers and want to find where numbers were skipped.

Detecting Gaps
SQL
SELECT start_gap, end_gap
FROM invoices
MATCH_RECOGNIZE (
  ORDER BY invoice_id
  MEASURES
    STRT.invoice_id + 1 AS start_gap,
    NEXT_VAL.invoice_id - 1 AS end_gap
  ONE ROW PER MATCH
  PATTERN (STRT NEXT_VAL)
  DEFINE
    NEXT_VAL AS NEXT_VAL.invoice_id > PREV(NEXT_VAL.invoice_id) + 1
);

Key Takeaways

    RetailCoder
    All systems operational
    v1.0 Live

    RC:OMS

    Multi-channel order management with double-entry inventory ledger. Amazon, Flipkart, Shopify, WooCommerce — one source of truth.

    Launch demo →
    v1.0 Live

    RC:Storefront

    Self-hosted headless e-commerce. Your server, your data, zero transaction fees. Native RC:OMS inventory sync.

    Visit Storefront →
    Pipeline

    RC:Pulse

    AI-powered retail analytics and demand forecasting — built natively on top of your RC:OMS and Storefront data.

    Request early access →
    Built in India 🇮🇳  ·  Architected by Priyanshu PandeyTalk to an engineer →