Complete guide to using window functions with Relica's SelectExpr() for advanced analytics
- 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
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.
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
<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)
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
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)
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!
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 functions (SUM, AVG, COUNT, MIN, MAX) can be used as window functions.
// 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_dateResult:
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
// 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 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 access values from other rows relative to current row.
// 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
// 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
// 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 divides rows into groups for window function calculation.
// 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", rankResult:
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
// 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 within window functions determines row order for calculations.
// 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, ...
}// 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 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
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)
// 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
// 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
}// 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
}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
✅ 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
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);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);Large frames are slower:
-- Fast: Small frame
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW
-- Slow: Large frame
ROWS BETWEEN 1000 PRECEDING AND 1000 FOLLOWINGReuse 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.
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 |
| 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
-
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 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")
// 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
}// 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
}// 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
}// 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
}// 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
}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)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")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 rowsProblem: 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);- 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+