🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
Search courses…
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Window Frames: ROWS, RANGE, GROUPS, BETWEEN, EXCLUDE

Advanced SQLWindow Frames⭐ Premium

Advertisement

Interview Question: "What's the difference between ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW versus RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW? When would you use GROUPS?" — Asked at Google, Facebook, Amazon for Data Scientist roles

ℹ️

Difficulty: Advanced | Companies: Google, Facebook, Amazon, Microsoft, Apple | Time: 45-60 minutes

Window Frame Syntax

The complete window frame syntax:

Frame=ROWSRANGEGROUPSBound=UNBOUNDED PRECEDINGn PRECEDINGCURRENT ROWn FOLLOWINGUNBOUNDED FOLLOWINGExclude=EXCLUDE CURRENT ROWEXCLUDE GROUPEXCLUDE TIESEXCLUDE NO OTHERS\text{Frame} = \text{ROWS} \mid \text{RANGE} \mid \text{GROUPS} \\ \text{Bound} = \text{UNBOUNDED PRECEDING} \mid n \text{ PRECEDING} \mid \text{CURRENT ROW} \\ \quad \mid n \text{ FOLLOWING} \mid \text{UNBOUNDED FOLLOWING} \\ \text{Exclude} = \text{EXCLUDE CURRENT ROW} \mid \text{EXCLUDE GROUP} \mid \text{EXCLUDE TIES} \mid \text{EXCLUDE NO OTHERS}
-- Create time-series data for frame analysis
CREATE TABLE stock_prices (
    trade_date DATE,
    symbol VARCHAR(10),
    price DECIMAL(10,2),
    volume INT
);

INSERT INTO stock_prices VALUES
('2024-01-01', 'AAPL', 150.00, 1000000),
('2024-01-02', 'AAPL', 152.50, 1200000),
('2024-01-03', 'AAPL', 148.75, 800000),
('2024-01-04', 'AAPL', 155.00, 1500000),
('2024-01-05', 'AAPL', 153.25, 900000),
('2024-01-08', 'AAPL', 157.50, 1100000),
('2024-01-09', 'AAPL', 159.00, 1300000),
('2024-01-10', 'AAPL', 156.75, 700000),
('2024-01-11', 'AAPL', 160.00, 1400000),
('2024-01-12', 'AAPL', 162.50, 1600000);

ROWS Frame Type

ROWS counts physical rows regardless of values:

ROWS BETWEEN a AND b={i:icurrent+aiicurrent+b}\text{ROWS BETWEEN } a \text{ AND } b = \{i : i_{current} + a \leq i \leq i_{current} + b\}
-- ROWS: Fixed number of physical rows
SELECT 
    trade_date,
    price,
    volume,
    -- 3-day moving average (ROWS)
    AVG(price) OVER (
        ORDER BY trade_date
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS moving_avg_3day,
    
    -- 5-day moving sum
    SUM(volume) OVER (
        ORDER BY trade_date
        ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING
    ) AS moving_sum_5day,
    
    -- Running average from start
    AVG(price) OVER (
        ORDER BY trade_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_avg,
    
    -- Entire dataset average
    AVG(price) OVER (
        ORDER BY trade_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS overall_avg
FROM stock_prices
ORDER BY trade_date;

Output:

trade_datepricevolumemoving_avg_3daymoving_sum_5dayrunning_avgoverall_avg
2024-01-01150.001000000150.004500000150.00155.53
2024-01-02152.501200000151.254700000151.25155.53
2024-01-03148.75800000150.425400000150.42155.53
2024-01-04155.001500000152.085700000151.56155.53
2024-01-05153.25900000152.336800000151.90155.53
2024-01-08157.501100000155.256800000152.83155.53
2024-01-09159.001300000156.587000000153.71155.53
2024-01-10156.75700000157.757100000154.09155.53
2024-01-11160.001400000158.587200000154.72155.53
2024-01-12162.501600000159.757100000155.53155.53

ℹ️

ROWS Behavior: With ROWS, the frame always contains exactly the specified number of rows. If fewer rows exist, the frame is smaller.

RANGE Frame Type

RANGE includes all rows with values within the logical range:

RANGE BETWEEN a AND b={i:valuecurrent+avalueivaluecurrent+b}\text{RANGE BETWEEN } a \text{ AND } b = \{i : \text{value}_{current} + a \leq \text{value}_i \leq \text{value}_{current} + b\}
-- RANGE: Logical value range
SELECT 
    trade_date,
    price,
    volume,
    -- Sum of prices within ±5 of current price
    SUM(price) OVER (
        ORDER BY price
        RANGE BETWEEN 5 PRECEDING AND 5 FOLLOWING
    ) AS sum_within_range,
    
    -- Count of stocks within 10% of current price
    COUNT(*) OVER (
        ORDER BY price
        RANGE BETWEEN price * 0.1 PRECEDING AND price * 0.1 FOLLOWING
    ) AS count_nearby_prices,
    
    -- Average of all rows with price <= current
    AVG(volume) OVER (
        ORDER BY price
        RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS avg_volume_lower_prices
FROM stock_prices
ORDER BY trade_date;

Output:

trade_datepricevolumesum_within_rangecount_nearby_pricesavg_volume_lower_prices
2024-01-01150.001000000451.2531000000.00
2024-01-02152.501200000604.5041100000.00
2024-01-03148.75800000451.253900000.00
2024-01-04155.001500000609.5041166666.67
2024-01-05153.25900000609.5041100000.00
2024-01-08157.501100000467.5021100000.00
2024-01-09159.001300000479.0021200000.00
2024-01-10156.75700000474.2531033333.33
2024-01-11160.001400000320.0011160000.00
2024-01-12162.501600000162.5011200000.00

GROUPS Frame Type

GROUPS counts peer groups (rows with same ORDER BY value):

GROUPS BETWEEN a AND b={i:groupcurrent+agroupigroupcurrent+b}\text{GROUPS BETWEEN } a \text{ AND } b = \{i : \text{group}_{current} + a \leq \text{group}_i \leq \text{group}_{current} + b\}
-- GROUPS: Peer group counting
SELECT 
    trade_date,
    price,
    volume,
    -- Sum of 1 peer group before and after
    SUM(price) OVER (
        ORDER BY price
        GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING
    ) AS sum_peer_groups,
    
    -- Count with GROUPS
    COUNT(*) OVER (
        ORDER BY volume
        GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS count_peer_groups,
    
    -- Average with GROUPS
    AVG(price) OVER (
        ORDER BY volume
        GROUPS BETWEEN 2 PRECEDING AND 2 FOLLOWING
    ) AS avg_peer_groups
FROM stock_prices
ORDER BY trade_date;

Frame Boundary Comparison

-- Compare all three frame types
SELECT 
    trade_date,
    price,
    -- ROWS frame
    SUM(price) OVER (
        ORDER BY trade_date
        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
    ) AS sum_rows,
    
    -- RANGE frame (by value)
    SUM(price) OVER (
        ORDER BY price
        RANGE BETWEEN 2 PRECEDING AND 2 FOLLOWING
    ) AS sum_range,
    
    -- GROUPS frame
    SUM(price) OVER (
        ORDER BY price
        GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING
    ) AS sum_groups
FROM stock_prices
ORDER BY trade_date;

Output:

trade_datepricesum_rowssum_rangesum_groups
2024-01-01150.00302.50451.25604.50
2024-01-02152.50451.25604.50609.50
2024-01-03148.75456.25451.25604.50
2024-01-04155.00456.25609.50609.50
2024-01-05153.25466.25609.50609.50
2024-01-08157.50468.25467.50479.00
2024-01-09159.00473.25479.00479.00
2024-01-10156.75476.75474.25474.25
2024-01-11160.00479.25320.00160.00
2024-01-12162.50322.50162.50162.50

⚠️

Frame Confusion: RANGE with numeric ORDER BY uses ±n as value range, not row count. This can produce unexpected results.

EXCLUDE Clause Variations

-- EXCLUDE CURRENT ROW
SELECT 
    trade_date,
    price,
    AVG(price) OVER (
        ORDER BY trade_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
        EXCLUDE CURRENT ROW
    ) AS avg_excluding_current
FROM stock_prices
ORDER BY trade_date;

-- EXCLUDE GROUP (excludes all peers)
SELECT 
    trade_date,
    price,
    AVG(price) OVER (
        ORDER BY price
        GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
        EXCLUDE GROUP
    ) AS avg_excluding_peers
FROM stock_prices
ORDER BY trade_date;

-- EXCLUDE TIES (excludes peers but keeps current)
SELECT 
    trade_date,
    price,
    AVG(price) OVER (
        ORDER BY price
        GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
        EXCLUDE TIES
    ) AS avg_excluding_ties
FROM stock_prices
ORDER BY trade_date;

Advanced: Moving Percentiles

-- 7-day moving percentile
SELECT 
    trade_date,
    price,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price) OVER (
        ORDER BY trade_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS moving_median_7day,
    PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY price) OVER (
        ORDER BY trade_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS moving_q1_7day,
    PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY price) OVER (
        ORDER BY trade_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS moving_q3_7day
FROM stock_prices
ORDER BY trade_date;

Mathematical Formulas

For a window frame FF over ordered set SS:

ROWS(a,b)={si+a,...,si+b}where i=current position\text{ROWS}(a, b) = \{s_{i+a}, ..., s_{i+b}\} \quad \text{where } i = \text{current position}
RANGE(a,b)={sj:vi+avjvi+b}\text{RANGE}(a, b) = \{s_j : v_i + a \leq v_j \leq v_i + b\}
GROUPS(a,b)={sj:gi+agjgi+b}\text{GROUPS}(a, b) = \{s_j : g_i + a \leq g_j \leq g_i + b\}

The aggregate over frame FF:

agg(F)=aggregate function applied to {sF}\text{agg}(F) = \text{aggregate function applied to } \{s \in F\}

ℹ️

Frame Default: Default frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW if ORDER BY is specified, or ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING without ORDER BY.

Frame Performance Implications

Frame TypeSort RequiredMemory UsageSpeed
ROWSPhysicalLowFast
RANGELogical valueMediumMedium
GROUPSPeer groupsMediumMedium
-- Check frame execution cost
EXPLAIN (ANALYZE, BUFFERS)
SELECT 
    trade_date,
    AVG(price) OVER (
        ORDER BY trade_date
        ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING
    ) AS moving_avg
FROM stock_prices;

⚠️

Performance Warning: RANGE frames with non-integer ORDER BY values can be expensive because the database must evaluate the range for each row.

Advertisement