All posts · SQL & PL/SQL

Advanced Analytic Functions in Oracle SQL

Window functions like ROW_NUMBER, RANK, LAG, LEAD, and running totals transform what would require complex self-joins into readable, performant SQL.

Anurag Jangra · January 8, 2026 · 7 min read · ... views

What analytic functions are

Unlike GROUP BY which collapses rows, analytic functions compute values across related rows without reducing the result set:

graph LR
    subgraph "With GROUP BY — rows collapsed"
        GB[dept 10 → 1 row<br/>dept 20 → 1 row<br/>dept 30 → 1 row]
    end
    subgraph "With Analytic Function — all rows kept"
        AF[emp 101, dept 10, sal 5000, rank 1<br/>emp 102, dept 10, sal 4500, rank 2<br/>emp 103, dept 20, sal 8000, rank 1<br/>emp 104, dept 20, sal 7000, rank 2]
    end
    style GB fill:#2d0f0f,stroke:#EF4444,color:#e2e8f0
    style AF fill:#0f2d1f,stroke:#22C55E,color:#e2e8f0

The OVER clause anatomy

FUNCTION_NAME() OVER (
  PARTITION BY column    -- divide rows into groups
  ORDER BY column        -- define row order within group
  ROWS/RANGE BETWEEN ... -- define the window frame
)

ROW_NUMBER, RANK, DENSE_RANK

SELECT
  employee_id,
  department_id,
  salary,
  ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS row_num,
  RANK()       OVER (PARTITION BY department_id ORDER BY salary DESC) AS rnk,
  DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dense_rnk
FROM employees;
employee_iddeptsalaryROW_NUMBERRANKDENSE_RANK
101109000111
102108500222
103108500322
104107000443

RANK skips numbers after ties (1,2,2,4). DENSE_RANK does not (1,2,2,3).

Top-N per group — the most common use case

-- Top 3 earners per department, using ROW_NUMBER
SELECT dept_id, employee_name, salary
FROM (
  SELECT
    department_id AS dept_id,
    first_name || ' ' || last_name AS employee_name,
    salary,
    ROW_NUMBER() OVER (
      PARTITION BY department_id
      ORDER BY salary DESC
    ) AS rn
  FROM employees
)
WHERE rn <= 3
ORDER BY dept_id, rn;

LAG and LEAD — period comparisons without self-joins

SELECT
  month_year,
  revenue,
  LAG(revenue, 1, 0) OVER (ORDER BY month_year)         AS prev_month_revenue,
  revenue - LAG(revenue, 1, 0) OVER (ORDER BY month_year) AS month_change,
  ROUND(
    (revenue - LAG(revenue,1) OVER (ORDER BY month_year))
    / LAG(revenue,1) OVER (ORDER BY month_year) * 100, 1
  )                                                       AS pct_change
FROM monthly_sales
ORDER BY month_year;

Running totals and moving averages

SELECT
  sale_date,
  daily_amount,
  -- Running total since start of year
  SUM(daily_amount) OVER (
    PARTITION BY TO_CHAR(sale_date, 'YYYY')
    ORDER BY sale_date
  ) AS ytd_total,
  -- 7-day moving average
  ROUND(AVG(daily_amount) OVER (
    ORDER BY sale_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ), 2) AS moving_avg_7d
FROM daily_sales
ORDER BY sale_date;

NTILE — split into buckets

-- Divide employees into 4 salary quartiles
SELECT
  employee_id,
  first_name,
  salary,
  NTILE(4) OVER (ORDER BY salary) AS quartile,
  CASE NTILE(4) OVER (ORDER BY salary)
    WHEN 1 THEN 'Bottom 25%'
    WHEN 2 THEN '25-50%'
    WHEN 3 THEN '50-75%'
    WHEN 4 THEN 'Top 25%'
  END AS salary_band
FROM employees;

Think Beyond the Implementation

Questions worth sitting with after reading this

01

Why is this architecture appropriate for this specific context — and where would it be the wrong choice?

02

What assumptions did we make that aren't stated explicitly? What happens if those assumptions are wrong?

03

What would break first if the requirements changed — volume doubled, a third system was added, or the deadline halved?

04

What alternatives did we reject, and why? Was the decision made on evidence — or habit?

AJ
Anurag Jangra
Oracle Cloud PaaS Consultant · OIC & VBCS Specialist

4.5+ years delivering enterprise Oracle Cloud integrations and VBCS applications across manufacturing, IT services, and financial sectors. OCI Certified — writes about real-world OIC, VBCS, SQL, and BI Publisher patterns from production experience.

Chat on WhatsApp