正在加载,请稍候…

SQL Window Functions: RANK, LAG, LEAD, and Running Totals

Master SQL window functions for analytics queries. Learn RANK, ROW_NUMBER, LAG/LEAD, running totals with SUM OVER, moving averages, and practical business use cases.

SQL Window Functions: Analytics Made Easy

Window functions perform calculations across a set of rows related to the current row.

Ranking Functions

-- ROW_NUMBER: unique sequential number
SELECT
  employee_id,
  name,
  department,
  salary,
  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
FROM employees;

-- RANK: ties get same rank, gaps after ties
SELECT
  name,
  score,
  RANK() OVER (ORDER BY score DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM game_scores;

-- Score: 100, 100, 90 -> RANK: 1, 1, 3 | DENSE_RANK: 1, 1, 2

-- NTILE: divide rows into N equal groups
SELECT
  name,
  salary,
  NTILE(4) OVER (ORDER BY salary) AS salary_quartile
FROM employees;
-- 1=bottom 25%, 2=25-50%, 3=50-75%, 4=top 25%

LAG and LEAD

-- Compare with previous/next row
SELECT
  order_date,
  daily_revenue,
  LAG(daily_revenue) OVER (ORDER BY order_date) AS prev_day_revenue,
  LEAD(daily_revenue) OVER (ORDER BY order_date) AS next_day_revenue,
  daily_revenue - LAG(daily_revenue) OVER (ORDER BY order_date) AS day_over_day_change,
  ROUND(
    100.0 * (daily_revenue - LAG(daily_revenue) OVER (ORDER BY order_date))
    / NULLIF(LAG(daily_revenue) OVER (ORDER BY order_date), 0),
    2
  ) AS pct_change
FROM daily_sales
ORDER BY order_date;

-- Month over month comparison
SELECT
  DATE_TRUNC('month', order_date) AS month,
  SUM(amount) AS monthly_revenue,
  LAG(SUM(amount)) OVER (ORDER BY DATE_TRUNC('month', order_date)) AS prev_month_revenue
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;

Running Totals and Cumulative Statistics

-- Running total
SELECT
  order_date,
  amount,
  SUM(amount) OVER (ORDER BY order_date) AS running_total,
  AVG(amount) OVER (ORDER BY order_date) AS running_avg,
  COUNT(*) OVER (ORDER BY order_date) AS running_count
FROM orders;

-- Cumulative percentage
SELECT
  product_name,
  revenue,
  SUM(revenue) OVER () AS total_revenue,
  ROUND(100.0 * revenue / SUM(revenue) OVER (), 2) AS pct_of_total,
  ROUND(100.0 * SUM(revenue) OVER (ORDER BY revenue DESC) / SUM(revenue) OVER (), 2) AS cumulative_pct
FROM product_revenue
ORDER BY revenue DESC;
-- Pareto analysis: identify products making up 80% of revenue

Moving Averages

-- 7-day moving average
SELECT
  date,
  daily_users,
  AVG(daily_users) OVER (
    ORDER BY date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW  -- Current + 6 previous = 7 days
  ) AS ma_7day,
  AVG(daily_users) OVER (
    ORDER BY date
    ROWS BETWEEN 29 PRECEDING AND CURRENT ROW  -- 30-day moving average
  ) AS ma_30day
FROM daily_metrics;

-- Range-based window (not row count)
SELECT
  sale_date,
  amount,
  AVG(amount) OVER (
    ORDER BY sale_date
    RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW
  ) AS rolling_7day_avg
FROM sales;

First/Last Value in Group

-- First purchase date and most recent purchase per customer
SELECT DISTINCT
  customer_id,
  FIRST_VALUE(order_date) OVER (
    PARTITION BY customer_id
    ORDER BY order_date
  ) AS first_purchase,
  LAST_VALUE(order_date) OVER (
    PARTITION BY customer_id
    ORDER BY order_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS last_purchase,
  COUNT(*) OVER (PARTITION BY customer_id) AS total_orders
FROM orders;

Practical Business Examples

-- Top N per category
WITH ranked AS (
  SELECT
    category,
    product_name,
    revenue,
    ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC) AS rn
  FROM product_sales
)
SELECT category, product_name, revenue
FROM ranked
WHERE rn <= 3;  -- Top 3 products per category

-- Session analytics: gap and island problem
WITH sessions AS (
  SELECT
    user_id,
    event_time,
    LEAD(event_time) OVER (PARTITION BY user_id ORDER BY event_time) AS next_event,
    CASE
      WHEN LEAD(event_time) OVER (PARTITION BY user_id ORDER BY event_time)
           - event_time > INTERVAL '30 minutes'
      THEN 1 ELSE 0
    END AS session_end
  FROM events
)
SELECT user_id, COUNT(*) AS session_count
FROM sessions
WHERE session_end = 1
GROUP BY user_id;

Window functions eliminate the need for self-joins and subqueries in most analytical SQL.