Chapter 06 · Advanced SQL: Analytic Functions, MODEL, PIVOT, MATCH_RECOGNIZE, and SQL Macros

Analytic Functions, Partitions, Frames, Ranking, Running Metrics, and Gaps-and-Islands

Use Oracle analytic functions without collapsing detail rows: partition data, choose deterministic frames, rank within groups, compute running metrics, and solve gaps-and-islands.

Intermediate → Advanced115–135 minutesAnalytic frames + ranking/islands labOracle AI Database 26ai · RU 23.26.3 baselineFree · SQLcl/SQL*Plus/SQL DeveloperLast reviewed: August 2026

Learning outcomes

ServiceHub needs daily dashboards that show every reading, its running total, its rank within an asset, and streaks of consecutive service dates. A developer first reaches for GROUP BY, but aggregation collapses rows that the dashboard still needs. Analytic functions solve a different problem: compute across related rows while preserving each detail row.

01

Separate GROUP BY aggregation from analytic/window processing.

02

Partition analytic calculations by business entity and order them deterministically.

03

Choose ROWS, RANGE, or GROUPS frames deliberately instead of accepting an accidental default.

04

Use ROW_NUMBER, RANK, DENSE_RANK, LAG/LEAD and running aggregates correctly.

05

Solve top-N-per-asset and gaps-and-islands while making tie behavior explicit.

Prerequisite connection

Chapter 05 established deterministic ORDER BY and result-set semantics. Analytic ORDER BY defines calculation order inside a partition; it does not by itself guarantee final presentation order.

Lab/version/licensing baseline

Mandatory labs target a disposable ServiceHub schema in Oracle AI Database Free 26ai. This chapter was reviewed against July 2026 RU 23.26.3, SQL Developer 26.2, and SQLcl 26.2.1. No Diagnostics/Tuning Pack, RAC, Data Guard, Exadata, GoldenGate, or paid cloud service is required. Free remains capped at two processing cores, 2 GB RAM for SGA+PGA, and 12 GB user data and receives no Oracle patches or Support SRs, so it is a learning baseline rather than a production recommendation.

1. GROUP BY collapses; analytics preserve

An aggregate query returns one row per group. An analytic function computes over a window but returns a value for each input row. Oracle processes analytic functions after joins, WHERE, GROUP BY, and HAVING, but before the final query ORDER BY.

sql · compare group aggregation and analytic preservation
SELECT asset_id, SUM(reading_value) AS asset_totalFROM servicehub_sql_readingGROUP BY asset_id;SELECT asset_id, observed_at, reading_id, reading_value,       SUM(reading_value) OVER (PARTITION BY asset_id) AS asset_totalFROM servicehub_sql_readingORDER BY asset_id, observed_at, reading_id;

The second query repeats the partition total alongside each detail row. That is intentional; analytics augment a row set rather than replacing its grain.

2. PARTITION BY defines independent calculation groups

PARTITION BY asset_id restarts the analytic calculation for each asset. Omitting it creates one partition containing the entire result. Analytic partitions are query-processing groups; they are unrelated to table partitioning on disk.

sql · partitioned running total with a unique tie-breaker
SELECT asset_id, observed_at, reading_id, reading_value,       SUM(reading_value) OVER (         PARTITION BY asset_id         ORDER BY observed_at, reading_id         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW       ) AS running_totalFROM servicehub_sql_readingORDER BY asset_id, observed_at, reading_id;

3. Deliberately wrong: accept the default frame with tied sort values

When an analytic function has an ORDER BY and no explicit frame, current Oracle documentation defines the default as RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. RANGE is logical: rows with equal sort values are peers. If two readings share the same timestamp, both can receive the same cumulative value even though a developer expected row-by-row accumulation.

sql · wrong assumption: default RANGE is not row-by-row
SELECT asset_id, observed_at, reading_id, reading_value,       SUM(reading_value) OVER (         PARTITION BY asset_id         ORDER BY observed_at       ) AS default_running_totalFROM servicehub_sql_readingORDER BY asset_id, observed_at, reading_id;

The repair is not “always use ROWS.” Choose semantics. If you need physical row-by-row progression, specify ROWS and a unique ordering. If business logic treats equal timestamps as one peer group, RANGE or GROUPS can be the correct frame.

sql · repair: explicit ROWS frame plus deterministic ordering
SUM(reading_value) OVER (  PARTITION BY asset_id  ORDER BY observed_at, reading_id  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

4. ROWS, RANGE, and GROUPS answer different window questions

Frame unit Boundary model Tie behavior
ROWS Physical row offsets Can split peers; deterministic only with unique ordering for physical offsets.
RANGE Logical value offsets Peers at the boundary move together; offset forms normally permit one sort key.
GROUPS Peer-group offsets Moves by groups of equal ORDER BY values without splitting peers.
sql · current and previous timestamp peer-group
SELECT asset_id, observed_at, reading_id, reading_value,       SUM(reading_value) OVER (         PARTITION BY asset_id         ORDER BY observed_at         GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW       ) AS current_and_previous_time_groupFROM servicehub_sql_readingORDER BY asset_id, observed_at, reading_id;

5. Ranking and navigation: ties are part of the contract

sql · rank rows three ways and compare prior value
SELECT asset_id, reading_id, reading_value,       ROW_NUMBER() OVER (PARTITION BY asset_id ORDER BY reading_value DESC, reading_id) AS rn,       RANK()       OVER (PARTITION BY asset_id ORDER BY reading_value DESC) AS rnk,       DENSE_RANK() OVER (PARTITION BY asset_id ORDER BY reading_value DESC) AS dense_rnk,       LAG(reading_value) OVER (PARTITION BY asset_id ORDER BY observed_at, reading_id) AS prior_valueFROM servicehub_sql_readingORDER BY asset_id, reading_value DESC, reading_id;

ROW_NUMBER always assigns distinct numbers, so add a tie-breaker if the chosen row matters. RANK leaves gaps after ties; DENSE_RANK does not. The right function depends on the business meaning of “top 3,” not on which output looks neatest.

sql · top two physical rows per asset using an inline query
SELECT asset_id, reading_id, reading_valueFROM (  SELECT r.*,         ROW_NUMBER() OVER (           PARTITION BY asset_id           ORDER BY reading_value DESC, reading_id         ) AS rn  FROM servicehub_sql_reading r)WHERE rn <= 2ORDER BY asset_id, rn;

6. Gaps-and-islands: turn consecutive dates into a stable group key

For one service row per asset/date, subtracting a consecutive row number from the date produces the same anchor date across a run of consecutive days. Grouping by that derived anchor turns detail rows into islands.

sql · consecutive-service-date islands
WITH numbered AS (  SELECT asset_id, service_date,         ROW_NUMBER() OVER (           PARTITION BY asset_id           ORDER BY service_date         ) AS rn  FROM servicehub_sql_service_day), tagged AS (  SELECT asset_id, service_date,         service_date - rn AS island_key  FROM numbered)SELECT asset_id,       MIN(service_date) AS island_start,       MAX(service_date) AS island_end,       COUNT(*) AS day_countFROM taggedGROUP BY asset_id, island_keyORDER BY asset_id, island_start;

This pattern assumes one row per asset/day and calendar-day consecutiveness. If the domain means business days or timestamp gaps, define a different adjacency rule.

7. Hands-on lab

sql · setup
DROP TABLE servicehub_sql_service_day IF EXISTS PURGE;DROP TABLE servicehub_sql_reading IF EXISTS PURGE;CREATE TABLE servicehub_sql_reading (  reading_id NUMBER PRIMARY KEY,  asset_id NUMBER NOT NULL,  observed_at TIMESTAMP NOT NULL,  reading_value NUMBER(10,2) NOT NULL);INSERT INTO servicehub_sql_reading VALUES (1,10,TIMESTAMP '2026-08-25 08:00:00',10);INSERT INTO servicehub_sql_reading VALUES (2,10,TIMESTAMP '2026-08-25 08:00:00',15);INSERT INTO servicehub_sql_reading VALUES (3,10,TIMESTAMP '2026-08-25 09:00:00',20);INSERT INTO servicehub_sql_reading VALUES (4,20,TIMESTAMP '2026-08-25 08:30:00',7);INSERT INTO servicehub_sql_reading VALUES (5,20,TIMESTAMP '2026-08-25 09:30:00',9);CREATE TABLE servicehub_sql_service_day (  asset_id NUMBER NOT NULL,  service_date DATE NOT NULL,  CONSTRAINT sh06_service_day_pk PRIMARY KEY (asset_id, service_date));INSERT INTO servicehub_sql_service_day VALUES (10,DATE '2026-08-20');INSERT INTO servicehub_sql_service_day VALUES (10,DATE '2026-08-21');INSERT INTO servicehub_sql_service_day VALUES (10,DATE '2026-08-23');INSERT INTO servicehub_sql_service_day VALUES (10,DATE '2026-08-24');INSERT INTO servicehub_sql_service_day VALUES (10,DATE '2026-08-25');COMMIT;
sql · cleanup
DROP TABLE servicehub_sql_service_day PURGE;DROP TABLE servicehub_sql_reading PURGE;

8. Production judgment

Window functions often sort or buffer data. Measure on representative volumes and watch TEMP/PGA pressure in later performance chapters rather than inventing a universal “windows are fast” rule. Keep partition/order keys aligned with the business question, make physical frames deterministic, and separate analytic calculation order from final presentation order. No paid pack, restart, parameter, or COMPATIBLE change is required for these mandatory examples on the declared 26ai baseline.

Check your understanding

  1. Why does GROUP BY not replace an analytic function when detail rows must remain?
  2. What is Oracle’s documented default frame when an analytic ORDER BY is present but no frame is written?
  3. Why can ROWS produce nondeterministic physical-offset results?
  4. How do RANK and DENSE_RANK differ after ties?
  5. What assumption makes the date-minus-row-number gaps-and-islands pattern valid?
Review the answers

GROUP BY collapses rows to group grain; analytics preserve each input row and add a computed value.

RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.

If ORDER BY does not uniquely order peers, Oracle has no unique physical row sequence for the frame.

RANK leaves numeric gaps after ties; DENSE_RANK does not.

The example assumes one row per asset/date and defines consecutive calendar dates as belonging to the same island.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.