Window Functions¶
ASQL provides intuitive syntax for window functions, making common patterns like deduplication, ranking, and running totals much cleaner than SQL.
The per Command¶
The per command creates a window context for partitioned operations:
per <partition_cols> <operation> by <order_cols> [as <alias>]
Or without partition (whole table):
<operation> by <order_cols> [as <alias>]
Available Operations¶
| Operation | What it does | Default alias | Row count |
|---|---|---|---|
first |
Keep first row per partition | (filters) | ↓ Reduces |
last |
Keep last row per partition | (filters) | ↓ Reduces |
number |
Add row number column | row_num |
Same |
rank |
Add rank column | rank |
Same |
dense rank |
Add dense rank column | dense_rank |
Same |
Deduplication with first / last¶
Keep the first or last row per group:
-- Most recent order per customer
from orders
per customer_id first by -order_date
-- Oldest order per customer
from orders
per customer_id first by order_date
-- Keep last row per user/event
from events
per user_id, event_type last by timestamp
The - prefix means descending order.
Row Numbering¶
Add row numbers within partitions:
-- Number orders per customer (most recent = 1)
from orders
per customer_id number by -order_date
-- Adds column: row_num
With custom alias:
from orders
per customer_id number by -order_date as order_num
Ranking¶
Add rank columns:
-- Rank employees by salary within department
from employees
per department rank by -salary
-- Dense rank (no gaps)
from employees
per department dense rank by -salary
Whole-Table Operations¶
Without partition (operates on entire table):
-- Number all rows
from events
number by -timestamp
-- Rank all scores
from scores
rank by -score as global_rank
-- Dense rank all
from scores
dense rank by -score
prior() and next() (LAG/LEAD)¶
Access previous or next row values:
from monthly_sales
order by month
select
month,
revenue,
prior(revenue) as prev_month, -- LAG(revenue, 1)
prior(revenue, 3) as three_ago, -- LAG(revenue, 3)
next(revenue) as next_month -- LEAD(revenue, 1)
The order by clause provides the window ordering.
Month-over-Month Comparison¶
from monthly_sales
order by month
select
month,
revenue,
prior(revenue) as prev_revenue,
revenue - prior(revenue) as mom_change,
(revenue - prior(revenue)) / prior(revenue) * 100 as mom_pct_change
Running Aggregates (Cumulative)¶
Calculate cumulative totals from the start:
from transactions
order by date
select
date,
amount,
running_sum(amount) as cumulative_total,
running_avg(amount) as avg_to_date,
running_count(*) as transaction_number
Available running functions:
- running_sum(col) — Cumulative sum
- running_avg(col) — Cumulative average
- running_count(*) — Row number (cumulative count)
Rolling Aggregates (Moving Window)¶
Calculate moving averages with a specified window size:
from daily_sales
order by date
select
date,
revenue,
rolling_avg(revenue, 7) as seven_day_avg,
rolling_sum(revenue, 30) as thirty_day_total
The second parameter is the window size (number of rows).
first() and last() in GROUP BY¶
Get the first or last value when aggregating:
from orders
group by customer_id (
first(order_id order by -order_date) as latest_order,
last(order_id order by order_date) as first_order,
# as total_orders
)
per ... first vs first() in GROUP BY¶
| Use Case | Syntax | Purpose |
|---|---|---|
| Keep whole row | per customer_id first by -date |
Deduplication |
| Extract specific value | first(order_id order by -date) |
Aggregation |
arg_max() / arg_min()¶
Get the value of one column where another column is max/min (ClickHouse-inspired):
from orders
group by customer_id (
arg_max(order_id, order_date) as latest_order_id,
arg_min(order_id, order_date) as earliest_order_id
)
arg_max(return_col, sort_col)— Value ofreturn_colwheresort_colis maximumarg_min(return_col, sort_col)— Value ofreturn_colwheresort_colis minimum
QUALIFY Clause¶
Filter on window function results:
from orders
select *, row_number() over (partition by customer_id order by -order_date) as rn
qualify rn = 1
This is equivalent to wrapping in a subquery and filtering.
DISTINCT ON¶
PostgreSQL-style deduplication:
from orders
distinct on (customer_id)
order by customer_id, -order_date
Note: per ... first by is often clearer and more portable.
Standard Window Functions¶
You can use standard SQL window functions directly:
from orders
select
*,
row_number() over (partition by customer_id order by -order_date) as rn,
rank() over (partition by category order by -amount) as category_rank,
sum(amount) over (partition by region) as region_total
Window Frame Specifications¶
For advanced use cases:
from sales
select
date,
revenue,
sum(revenue) over (
order by date
rows between 6 preceding and current row
) as seven_day_sum
Quick Reference¶
| Intent | ASQL Syntax |
|---|---|
| Most recent row per group | per group_col first by -date |
| Oldest row per group | per group_col first by date |
| Add row numbers per group | per group_col number by -date |
| Add rank per group | per group_col rank by -value |
| Add dense rank per group | per group_col dense rank by -value |
| Get column value at max | arg_max(col, sort_col) |
| Previous row value | prior(col) |
| Next row value | next(col) |
| Cumulative sum | running_sum(col) |
| Cumulative average | running_avg(col) |
| 7-day moving average | rolling_avg(col, 7) |
| First value in group | first(col order by ...) |
Real-World Examples¶
Latest Order Per Customer¶
from orders
per customer_id first by -order_date
select customer_id, order_id, amount, order_date
Rank Products by Revenue¶
from order_items
& products on order_items.product_id = products.id
group by products.category, products.name (
sum(order_items.amount) as revenue
)
per category rank by -revenue as category_rank
Year-over-Year Comparison¶
from monthly_metrics
order by month
select
month,
revenue,
prior(revenue, 12) as same_month_last_year,
revenue - prior(revenue, 12) as yoy_change
Running Total with Daily Revenue¶
from orders
group by day(created_at) as date (
sum(amount) as daily_revenue
)
order by date
select
date,
daily_revenue,
running_sum(daily_revenue) as cumulative_revenue
Next Steps¶
- Aggregations — GROUP BY with window functions
- CTEs & Variables — Composing complex queries
- Examples — More real-world patterns