Window Functions Guide
March 5, 2026 · View on GitHub
Complete guide to using window functions with Relica's SelectExpr() for advanced analytics
Table of Contents
- Introduction
- Basic Window Functions
- Ranking Functions
- Aggregate Window Functions
- Value Functions
- PARTITION BY
- ORDER BY in Window Functions
- Window Frames
- When to Use Window Functions
- Performance Considerations
- Database Compatibility
- Best Practices
- Common Patterns
- Troubleshooting
Introduction
Window functions perform calculations across a set of rows related to the current row, without collapsing rows like GROUP BY. Think of them as "aggregate functions that don't group."
Key Benefits:
- No grouping: Keep all detail rows while calculating aggregates
- Rankings: ROW_NUMBER(), RANK(), DENSE_RANK()
- Running totals: Cumulative sums, moving averages
- Comparative analysis: Compare row to previous/next rows
- Top N per group: Without complex subqueries
Note: Relica doesn't have dedicated window function API yet. Use SelectExpr() with raw SQL window function syntax.
Basic Window Functions
Using SelectExpr()
Window functions in Relica are added using SelectExpr() with raw SQL syntax.
package main
import (
"github.com/coregx/relica"
)
// Add row numbers to query results
func GetNumberedProducts(db *relica.DB) ([]Product, error) {
type Product struct {
RowNum int `db:"row_num"`
ID int `db:"id"`
Name string `db:"name"`
Price float64 `db:"price"`
}
var products []Product
err := db.Select("id", "name", "price").
SelectExpr("ROW_NUMBER() OVER (ORDER BY price DESC)", "row_num").
From("products").
All(&products)
return products, err
}
Generated SQL (PostgreSQL):
SELECT "id", "name", "price", ROW_NUMBER() OVER (ORDER BY price DESC) as row_num
FROM "products"
Result:
row_num | id | name | price
--------|----|-----------|---------
1 | 42 | Laptop | 1299.99
2 | 15 | Monitor | 499.99
3 | 8 | Keyboard | 89.99
Window Function Syntax
<function_name>(<expression>) OVER (
[PARTITION BY <columns>]
[ORDER BY <columns>]
[ROWS|RANGE <frame_clause>]
)
Components:
- Function: ROW_NUMBER(), SUM(), AVG(), etc.
- OVER: Defines the window
- PARTITION BY: Divide rows into groups (optional)
- ORDER BY: Order rows within partition (optional)
- Frame: Define row range for calculation (optional)
Ranking Functions
ROW_NUMBER()
Assigns unique sequential number to each row.
// Number products by price (descending)
func RankProductsByPrice(db *relica.DB) ([]ProductRank, error) {
type ProductRank struct {
Rank int `db:"rank"`
Name string `db:"name"`
Price float64 `db:"price"`
}
var products []ProductRank
err := db.Select("name", "price").
SelectExpr("ROW_NUMBER() OVER (ORDER BY price DESC)", "rank").
From("products").
All(&products)
return products, err
}
Generated SQL:
SELECT "name", "price", ROW_NUMBER() OVER (ORDER BY price DESC) as rank
FROM "products"
Result:
rank | name | price
-----|-----------|-------
1 | Laptop | 1299
2 | Monitor | 499
3 | Tablet | 399
4 | Keyboard | 89
RANK()
Assigns rank with gaps for ties.
// Rank products (same price = same rank, with gaps)
func RankProductsWithGaps(db *relica.DB) ([]ProductRank, error) {
type ProductRank struct {
Rank int `db:"rank"`
Name string `db:"name"`
Price float64 `db:"price"`
}
var products []ProductRank
err := db.Select("name", "price").
SelectExpr("RANK() OVER (ORDER BY price DESC)", "rank").
From("products").
All(&products)
return products, err
}
Result (note gaps after ties):
rank | name | price
-----|-----------|-------
1 | Laptop | 1299
2 | Monitor | 499
3 | Tablet | 399
3 | Phone | 399 ← Same rank
5 | Keyboard | 89 ← Gap (skipped 4)
DENSE_RANK()
Assigns rank without gaps for ties.
// Rank products (same price = same rank, no gaps)
func RankProductsDense(db *relica.DB) ([]ProductRank, error) {
type ProductRank struct {
Rank int `db:"rank"`
Name string `db:"name"`
Price float64 `db:"price"`
}
var products []ProductRank
err := db.Select("name", "price").
SelectExpr("DENSE_RANK() OVER (ORDER BY price DESC)", "rank").
From("products").
All(&products)
return products, err
}
Result (no gaps):
rank | name | price
-----|-----------|-------
1 | Laptop | 1299
2 | Monitor | 499
3 | Tablet | 399
3 | Phone | 399 ← Same rank
4 | Keyboard | 89 ← No gap!
NTILE()
Distributes rows into N buckets.
// Divide products into 4 price quartiles
func GetPriceQuartiles(db *relica.DB) ([]ProductQuartile, error) {
type ProductQuartile struct {
Quartile int `db:"quartile"`
Name string `db:"name"`
Price float64 `db:"price"`
}
var products []ProductQuartile
err := db.Select("name", "price").
SelectExpr("NTILE(4) OVER (ORDER BY price)", "quartile").
From("products").
All(&products)
return products, err
}
Result:
quartile | name | price
---------|-----------|-------
1 | Mouse | 19
1 | Keyboard | 89
2 | Tablet | 399
2 | Phone | 399
3 | Monitor | 499
3 | Headset | 199
4 | Laptop | 1299
4 | Desktop | 1499
Aggregate Window Functions
Aggregate functions (SUM, AVG, COUNT, MIN, MAX) can be used as window functions.
Running Total (SUM)
// Calculate running total of sales
func GetRunningTotalSales(db *relica.DB) ([]DailySales, error) {
type DailySales struct {
Date string `db:"sale_date"`
DailyTotal float64 `db:"daily_total"`
RunningTotal float64 `db:"running_total"`
}
var sales []DailySales
err := db.Select("DATE(created_at) as sale_date", "SUM(total) as daily_total").
SelectExpr("SUM(SUM(total)) OVER (ORDER BY DATE(created_at))", "running_total").
From("orders").
GroupBy("DATE(created_at)").
OrderBy("sale_date").
All(&sales)
return sales, err
}
Generated SQL:
SELECT DATE(created_at) as sale_date,
SUM(total) as daily_total,
SUM(SUM(total)) OVER (ORDER BY DATE(created_at)) as running_total
FROM "orders"
GROUP BY DATE(created_at)
ORDER BY sale_date
Result:
sale_date | daily_total | running_total
-----------|-------------|---------------
2025-01-01 | 1000 | 1000
2025-01-02 | 1500 | 2500
2025-01-03 | 800 | 3300
2025-01-04 | 1200 | 4500
Moving Average (AVG)
// Calculate 7-day moving average
func GetMovingAverage(db *relica.DB) ([]DailySales, error) {
type DailySales struct {
Date string `db:"sale_date"`
Total float64 `db:"daily_total"`
AvgLast7 float64 `db:"avg_last_7_days"`
}
var sales []DailySales
err := db.Select("DATE(created_at) as sale_date", "SUM(total) as daily_total").
SelectExpr("AVG(SUM(total)) OVER (ORDER BY DATE(created_at) ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)", "avg_last_7_days").
From("orders").
GroupBy("DATE(created_at)").
OrderBy("sale_date").
All(&sales)
return sales, err
}
Result:
sale_date | daily_total | avg_last_7_days
-----------|-------------|----------------
2025-01-01 | 1000 | 1000.00
2025-01-02 | 1500 | 1250.00
2025-01-03 | 800 | 1100.00
...
2025-01-07 | 1200 | 1150.00 ← Average of last 7 days
2025-01-08 | 900 | 1100.00
COUNT Window Function
// Count orders per customer with total count
func GetCustomerOrderCounts(db *relica.DB) ([]CustomerOrders, error) {
type CustomerOrders struct {
CustomerID int `db:"customer_id"`
OrderCount int `db:"order_count"`
TotalOrders int `db:"total_orders"`
}
var customers []CustomerOrders
err := db.Select("customer_id", "COUNT(*) as order_count").
SelectExpr("SUM(COUNT(*)) OVER ()", "total_orders").
From("orders").
GroupBy("customer_id").
All(&customers)
return customers, err
}
Value Functions
Value functions access values from other rows relative to current row.
LAG() - Previous Row Value
// Compare each month's sales to previous month
func GetMonthlySalesComparison(db *relica.DB) ([]MonthlySales, error) {
type MonthlySales struct {
Month string `db:"month"`
Sales float64 `db:"sales"`
PrevSales float64 `db:"prev_month_sales"`
Growth float64 `db:"growth_pct"`
}
var sales []MonthlySales
err := db.Select("DATE_TRUNC('month', created_at) as month", "SUM(total) as sales").
SelectExpr("LAG(SUM(total)) OVER (ORDER BY DATE_TRUNC('month', created_at))", "prev_month_sales").
SelectExpr("(SUM(total) - LAG(SUM(total)) OVER (ORDER BY DATE_TRUNC('month', created_at))) / LAG(SUM(total)) OVER (ORDER BY DATE_TRUNC('month', created_at)) * 100", "growth_pct").
From("orders").
GroupBy("DATE_TRUNC('month', created_at)").
OrderBy("month").
All(&sales)
return sales, err
}
Result:
month | sales | prev_month_sales | growth_pct
-----------|-------|------------------|------------
2025-01 | 10000 | NULL | NULL
2025-02 | 12000 | 10000 | 20.00
2025-03 | 11500 | 12000 | -4.17
2025-04 | 15000 | 11500 | 30.43
LEAD() - Next Row Value
// Compare current price to next product's price
func GetPriceComparison(db *relica.DB) ([]PriceComp, error) {
type PriceComp struct {
Name string `db:"name"`
Price float64 `db:"price"`
NextPrice float64 `db:"next_price"`
}
var products []PriceComp
err := db.Select("name", "price").
SelectExpr("LEAD(price) OVER (ORDER BY price)", "next_price").
From("products").
OrderBy("price").
All(&products)
return products, err
}
Result:
name | price | next_price
----------|-------|------------
Mouse | 19 | 89
Keyboard | 89 | 399
Tablet | 399 | 499
Monitor | 499 | 1299
Laptop | 1299 | NULL
FIRST_VALUE() and LAST_VALUE()
// Compare each product price to cheapest and most expensive in category
func GetCategoryPriceRange(db *relica.DB) ([]ProductPrice, error) {
type ProductPrice struct {
Category string `db:"category"`
Name string `db:"name"`
Price float64 `db:"price"`
MinPrice float64 `db:"min_price"`
MaxPrice float64 `db:"max_price"`
}
var products []ProductPrice
err := db.Select("category", "name", "price").
SelectExpr("FIRST_VALUE(price) OVER (PARTITION BY category ORDER BY price)", "min_price").
SelectExpr("LAST_VALUE(price) OVER (PARTITION BY category ORDER BY price ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)", "max_price").
From("products").
OrderBy("category", "price").
All(&products)
return products, err
}
PARTITION BY
PARTITION BY divides rows into groups for window function calculation.
Basic Partitioning
// Rank products within each category
func RankProductsByCategory(db *relica.DB) ([]ProductRank, error) {
type ProductRank struct {
Category string `db:"category"`
Name string `db:"name"`
Price float64 `db:"price"`
Rank int `db:"rank"`
}
var products []ProductRank
err := db.Select("category", "name", "price").
SelectExpr("ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC)", "rank").
From("products").
OrderBy("category", "rank").
All(&products)
return products, err
}
Generated SQL:
SELECT "category", "name", "price",
ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) as rank
FROM "products"
ORDER BY "category", rank
Result:
category | name | price | rank
------------|-----------|-------|------
Electronics | Laptop | 1299 | 1
Electronics | Monitor | 499 | 2
Electronics | Keyboard | 89 | 3
Furniture | Desk | 599 | 1
Furniture | Chair | 299 | 2
Furniture | Lamp | 49 | 3
Top N Per Group
// Get top 3 products per category
func GetTopProductsPerCategory(db *relica.DB, topN int) ([]Product, error) {
type Product struct {
Category string `db:"category"`
Name string `db:"name"`
Price float64 `db:"price"`
Rank int `db:"rank"`
}
// Use subquery to filter ranked results
ranked := db.Select("category", "name", "price").
SelectExpr("ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC)", "rank").
From("products")
var products []Product
err := db.Select("category", "name", "price", "rank").
FromSelect(ranked, "ranked").
Where("rank <= ?", topN).
OrderBy("category", "rank").
All(&products)
return products, err
}
ORDER BY in Window Functions
ORDER BY within window functions determines row order for calculations.
Impact on Results
// Different ORDER BY gives different results
func DemonstrateOrderBy(db *relica.DB) {
// Ascending order
db.Select("name", "price").
SelectExpr("ROW_NUMBER() OVER (ORDER BY price ASC)", "rank_asc").
From("products")
// rank_asc: 1=cheapest, 2=next cheapest, ...
// Descending order
db.Select("name", "price").
SelectExpr("ROW_NUMBER() OVER (ORDER BY price DESC)", "rank_desc").
From("products")
// rank_desc: 1=most expensive, 2=next expensive, ...
}
Multiple ORDER BY Columns
// Order by multiple columns
func RankByMultipleColumns(db *relica.DB) ([]Product, error) {
type Product struct {
Category string `db:"category"`
Name string `db:"name"`
Sales int `db:"sales"`
Rank int `db:"rank"`
}
var products []Product
err := db.Select("category", "name", "sales").
SelectExpr("ROW_NUMBER() OVER (ORDER BY category ASC, sales DESC)", "rank").
From("products").
All(&products)
return products, err
}
Window Frames
Window frames define which rows are included in window function calculation.
Syntax:
ROWS BETWEEN <start> AND <end>
RANGE BETWEEN <start> AND <end>
Frame bounds:
UNBOUNDED PRECEDING: First row of partitionN PRECEDING: N rows before currentCURRENT ROW: Current rowN FOLLOWING: N rows after currentUNBOUNDED FOLLOWING: Last row of partition
Default Frame
Without explicit frame:
- With ORDER BY:
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - Without ORDER BY:
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING(entire partition)
ROWS Frame
// Moving average of last 3 orders
func GetMovingAverage3(db *relica.DB) ([]OrderAvg, error) {
type OrderAvg struct {
OrderID int `db:"order_id"`
Total float64 `db:"total"`
Avg3 float64 `db:"avg_last_3"`
}
var orders []OrderAvg
err := db.Select("id as order_id", "total").
SelectExpr("AVG(total) OVER (ORDER BY id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)", "avg_last_3").
From("orders").
OrderBy("id").
All(&orders)
return orders, err
}
Result:
order_id | total | avg_last_3
---------|-------|------------
1 | 100 | 100.00 ← Only 1 row
2 | 150 | 125.00 ← (100+150)/2
3 | 200 | 150.00 ← (100+150+200)/3
4 | 120 | 156.67 ← (150+200+120)/3
5 | 180 | 166.67 ← (200+120+180)/3
RANGE Frame
// Sum of orders within same day
func GetDailyTotals(db *relica.DB) ([]DailyOrder, error) {
type DailyOrder struct {
OrderID int `db:"order_id"`
OrderDate string `db:"order_date"`
Total float64 `db:"total"`
DailyTotal float64 `db:"daily_total"`
}
var orders []DailyOrder
err := db.Select("id as order_id", "DATE(created_at) as order_date", "total").
SelectExpr("SUM(total) OVER (ORDER BY DATE(created_at) RANGE BETWEEN CURRENT ROW AND CURRENT ROW)", "daily_total").
From("orders").
OrderBy("created_at").
All(&orders)
return orders, err
}
Unbounded Windows
// Running total from start to current row
func GetCumulativeSum(db *relica.DB) ([]OrderTotal, error) {
type OrderTotal struct {
OrderID int `db:"order_id"`
Total float64 `db:"total"`
Cumsum float64 `db:"cumulative_sum"`
}
var orders []OrderTotal
err := db.Select("id as order_id", "total").
SelectExpr("SUM(total) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)", "cumulative_sum").
From("orders").
OrderBy("id").
All(&orders)
return orders, err
}
When to Use Window Functions
Decision Tree
Need aggregate calculation?
├─ Keep all detail rows? → Window function
│ ├─ Rank rows? → ROW_NUMBER/RANK/DENSE_RANK
│ ├─ Running total? → SUM() OVER (ORDER BY ...)
│ ├─ Compare to prev/next? → LAG/LEAD
│ └─ Top N per group? → ROW_NUMBER() OVER (PARTITION BY ...)
└─ Collapse rows? → GROUP BY
Use Cases
✅ Window functions excel at:
- Rankings (top N per category)
- Running totals / cumulative sums
- Moving averages
- Row-to-row comparisons (growth rates)
- Percentiles and quartiles
- Gap analysis
❌ Use GROUP BY instead when:
- Only need aggregated results (not detail rows)
- Simple totals without ranking
- Smaller result sets
Performance Considerations
1. Sorting Overhead
Window functions require sorting:
-- This sorts entire table by price
ROW_NUMBER() OVER (ORDER BY price)
Impact: O(n log n) complexity
Optimization: Index ORDER BY columns
CREATE INDEX idx_products_price ON products(price);
2. Partitioning Performance
PARTITION BY can be expensive:
-- Sorts once per partition
ROW_NUMBER() OVER (PARTITION BY category ORDER BY price)
Optimization: Index PARTITION BY + ORDER BY columns
CREATE INDEX idx_products_cat_price ON products(category, price);
3. Frame Size
Large frames are slower:
-- Fast: Small frame
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW
-- Slow: Large frame
ROWS BETWEEN 1000 PRECEDING AND 1000 FOLLOWING
4. Multiple Window Functions
Reuse windows when possible:
// ❌ BAD: Same window defined twice
db.Select("name").
SelectExpr("ROW_NUMBER() OVER (PARTITION BY category ORDER BY price)", "rank").
SelectExpr("DENSE_RANK() OVER (PARTITION BY category ORDER BY price)", "dense_rank")
// Computes same window twice
// ✅ GOOD: Define window once (PostgreSQL)
db.Select("name").
SelectExpr("ROW_NUMBER() OVER w", "rank").
SelectExpr("DENSE_RANK() OVER w", "dense_rank").
SelectExpr("WINDOW w AS (PARTITION BY category ORDER BY price)")
Note: Named windows (WINDOW clause) not yet supported.
Benchmark Results
Dataset: 1M products, 1000 categories
| Operation | Time | Index Impact |
|---|---|---|
| Simple ROW_NUMBER() | ~800ms | 50% faster with index |
| PARTITION BY | ~1200ms | 70% faster with index |
| LAG/LEAD | ~850ms | 60% faster with index |
| Moving average (7 rows) | ~900ms | 50% faster with index |
Database Compatibility
| Feature | PostgreSQL | MySQL | SQLite | Notes |
|---|---|---|---|---|
| ROW_NUMBER() | ✓ 8.4+ | ✓ 8.0+ | ✓ 3.25+ | |
| RANK() | ✓ 8.4+ | ✓ 8.0+ | ✓ 3.25+ | |
| DENSE_RANK() | ✓ 8.4+ | ✓ 8.0+ | ✓ 3.25+ | |
| NTILE() | ✓ 8.4+ | ✓ 8.0+ | ✓ 3.28+ | |
| LAG/LEAD | ✓ 8.4+ | ✓ 8.0+ | ✓ 3.25+ | |
| FIRST_VALUE/LAST_VALUE | ✓ 8.4+ | ✓ 8.0+ | ✓ 3.28+ | |
| SUM/AVG/COUNT | ✓ 8.4+ | ✓ 8.0+ | ✓ 3.25+ | |
| Frame clauses (ROWS/RANGE) | ✓ 8.4+ | ✓ 8.0+ | ✓ 3.28+ | |
| Named windows (WINDOW) | ✓ 8.4+ | ✓ 8.0+ | ✓ 3.25+ | Not in Relica yet |
MySQL Notes:
- MySQL 5.7: No window function support
- MySQL 8.0+: Full window function support
Best Practices
✅ DO
-
Index ORDER BY and PARTITION BY columns
CREATE INDEX idx_cat_price ON products(category, price); -
Use descriptive aliases
SelectExpr("ROW_NUMBER() OVER (ORDER BY price DESC)", "price_rank") -
Use ROW_NUMBER for Top N queries
SelectExpr("ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC)", "rank") -
Limit frame size when possible
SelectExpr("AVG(total) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)", "avg_7_days") -
Use CTEs for complex window queries
ranked := db.Select().SelectExpr("ROW_NUMBER() OVER (...)", "rn").From("products") db.Select().FromSelect(ranked, "r").Where("rn <= 10")
❌ DON'T
-
Don't use window functions when GROUP BY suffices
// ❌ Overkill SelectExpr("SUM(total) OVER ()") // ✅ Simple Select("SUM(total)") -
Don't forget ORDER BY in ranking functions
// ❌ Random order SelectExpr("ROW_NUMBER() OVER ()", "rank") // ✅ Meaningful order SelectExpr("ROW_NUMBER() OVER (ORDER BY price DESC)", "rank") -
Don't use large frames without testing
// ❌ May be slow ROWS BETWEEN 10000 PRECEDING AND 10000 FOLLOWING // ✅ Reasonable frame ROWS BETWEEN 30 PRECEDING AND CURRENT ROW -
Don't mix window functions with incompatible GROUP BY
// ❌ ERROR: Can't mix non-aggregated columns with GROUP BY Select("name"). SelectExpr("ROW_NUMBER() OVER (ORDER BY price)", "rank"). GroupBy("category")
Common Patterns
Pattern 1: Top N Per Group
// Top 3 selling products per category
func GetTopSellingProducts(db *relica.DB) ([]Product, error) {
type Product struct {
Category string `db:"category"`
Name string `db:"name"`
Sales int `db:"sales"`
Rank int `db:"rank"`
}
ranked := db.Select("category", "name", "sales").
SelectExpr("ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC)", "rank").
From("products")
var products []Product
err := db.Select().
FromSelect(ranked, "r").
Where("rank <= ?", 3).
OrderBy("category", "rank").
All(&products)
return products, err
}
Pattern 2: Percentage of Total
// Calculate each order's percentage of total sales
func GetOrderPercentages(db *relica.DB) ([]OrderPct, error) {
type OrderPct struct {
OrderID int `db:"order_id"`
Total float64 `db:"total"`
Percentage float64 `db:"pct_of_total"`
}
var orders []OrderPct
err := db.Select("id as order_id", "total").
SelectExpr("total / SUM(total) OVER () * 100", "pct_of_total").
From("orders").
All(&orders)
return orders, err
}
Pattern 3: Growth Rate Calculation
// Month-over-month growth rate
func GetMoMGrowth(db *relica.DB) ([]MonthlyGrowth, error) {
type MonthlyGrowth struct {
Month string `db:"month"`
Sales float64 `db:"sales"`
GrowthRate float64 `db:"growth_rate"`
}
var growth []MonthlyGrowth
err := db.Select("DATE_TRUNC('month', created_at) as month", "SUM(total) as sales").
SelectExpr("(SUM(total) - LAG(SUM(total)) OVER (ORDER BY DATE_TRUNC('month', created_at))) / NULLIF(LAG(SUM(total)) OVER (ORDER BY DATE_TRUNC('month', created_at)), 0) * 100", "growth_rate").
From("orders").
GroupBy("DATE_TRUNC('month', created_at)").
OrderBy("month").
All(&growth)
return growth, err
}
Pattern 4: Running Balance
// Calculate account running balance
func GetAccountBalance(db *relica.DB, accountID int) ([]Transaction, error) {
type Transaction struct {
Date string `db:"txn_date"`
Amount float64 `db:"amount"`
Balance float64 `db:"balance"`
}
var txns []Transaction
err := db.Select("DATE(created_at) as txn_date", "amount").
SelectExpr("SUM(amount) OVER (ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)", "balance").
From("transactions").
Where("account_id = ?", accountID).
OrderBy("created_at").
All(&txns)
return txns, err
}
Pattern 5: Median Calculation
// Calculate median product price per category (PostgreSQL)
func GetMedianPrices(db *relica.DB) ([]CategoryMedian, error) {
type CategoryMedian struct {
Category string `db:"category"`
MedianPrice float64 `db:"median_price"`
}
var medians []CategoryMedian
err := db.Select("category").
SelectExpr("PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price)", "median_price").
From("products").
GroupBy("category").
All(&medians)
return medians, err
}
Troubleshooting
Issue: Window Function in WHERE Clause
Problem: Can't use window functions in WHERE
// ❌ ERROR: Window functions not allowed in WHERE clause
db.Select().
SelectExpr("ROW_NUMBER() OVER (ORDER BY price)", "rank").
From("products").
Where("rank <= ?", 10)
Solution: Use subquery or CTE
// ✅ GOOD: Filter in outer query
ranked := db.Select().
SelectExpr("ROW_NUMBER() OVER (ORDER BY price)", "rank").
From("products")
db.Select().
FromSelect(ranked, "r").
Where("rank <= ?", 10)
Issue: ORDER BY Required for LAG/LEAD
Problem: LAG/LEAD without ORDER BY gives unpredictable results
// ❌ BAD: Random ordering
SelectExpr("LAG(price) OVER ()", "prev_price")
Solution: Always specify ORDER BY
// ✅ GOOD
SelectExpr("LAG(price) OVER (ORDER BY created_at)", "prev_price")
Issue: Frame Extends Beyond Partition
Problem: Frame calculation at partition boundaries
-- At first row: no "2 PRECEDING" rows exist
AVG(total) OVER (ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
Solution: Use COALESCE or accept NULL
// Averages whatever rows are available
SelectExpr("AVG(total) OVER (ORDER BY id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)", "avg_3")
// At row 1: avg of 1 row
// At row 2: avg of 2 rows
// At row 3+: avg of 3 rows
Issue: Performance Degradation
Problem: Window function query is slow
// Slow on large tables
SelectExpr("ROW_NUMBER() OVER (ORDER BY created_at DESC)", "rank")
Solution: Add index
CREATE INDEX idx_created_at ON orders(created_at DESC);
Further Reading
- Subquery Guide - Combine with window functions
- CTE Guide - Use CTEs for complex window queries
- PostgreSQL Window Functions
- MySQL Window Functions
Last Updated: 2025-11-24 Minimum Go Version: 1.25+