Aggregates (PostgreSQL)
The PostgreSQL extension provides aggregate functions that enforce currency consistency within each aggregation group.
Sum monetary values within a currency:
-- Basic sumSELECT SUM(amount)FROM transactionsWHERE monetary_currency(amount) = 'EUR';
-- Sum by currencySELECT monetary_currency(amount) as currency, SUM(amount) as totalFROM transactionsGROUP BY monetary_currency(amount);Currency Mismatch
Section titled “Currency Mismatch”Attempting to sum different currencies throws an error:
-- This will error if transactions contain mixed currenciesSELECT SUM(amount) FROM transactions;-- ERROR: cannot sum EUR and USD: currency mismatch in aggregateSolution: Group by currency or filter first:
-- Option 1: Group by currencySELECT monetary_currency(amount), SUM(amount)FROM transactionsGROUP BY monetary_currency(amount);
-- Option 2: Filter to single currencySELECT SUM(amount)FROM transactionsWHERE monetary_currency(amount) = 'EUR';Calculate average with proper rounding:
SELECT AVG(amount) as avg_transactionFROM transactionsWHERE monetary_currency(amount) = 'USD';The average uses round-half-away-from-zero for consistent results.
MIN / MAX
Section titled “MIN / MAX”Find minimum and maximum values:
SELECT MIN(amount) as smallest, MAX(amount) as largestFROM transactionsWHERE monetary_currency(amount) = 'EUR';Count works normally (currency-agnostic):
SELECT monetary_currency(amount) as currency, COUNT(*) as num_transactions, SUM(amount) as totalFROM transactionsGROUP BY monetary_currency(amount);Window Functions
Section titled “Window Functions”Use aggregates with OVER for running totals:
SELECT id, created_at, amount, SUM(amount) OVER ( ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) as running_totalFROM transactionsWHERE monetary_currency(amount) = 'EUR'ORDER BY created_at;Practical Examples
Section titled “Practical Examples”Daily Revenue Report
Section titled “Daily Revenue Report”SELECT DATE(created_at) as date, monetary_currency(amount) as currency, COUNT(*) as num_orders, SUM(amount) as revenue, AVG(amount) as avg_orderFROM ordersGROUP BY DATE(created_at), monetary_currency(amount)ORDER BY date DESC;Top Customers by Spend
Section titled “Top Customers by Spend”SELECT customer_id, monetary_currency(amount) as currency, SUM(amount) as total_spent, COUNT(*) as num_ordersFROM ordersGROUP BY customer_id, monetary_currency(amount)ORDER BY monetary_decimal(SUM(amount)) DESCLIMIT 10;Monthly Comparison
Section titled “Monthly Comparison”WITH monthly AS ( SELECT DATE_TRUNC('month', created_at) as month, SUM(amount) as revenue FROM orders WHERE monetary_currency(amount) = 'USD' GROUP BY DATE_TRUNC('month', created_at))SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) as prev_monthFROM monthlyORDER BY month;Performance Tips
Section titled “Performance Tips”- Filter early: Apply currency filters in WHERE, not HAVING
- Index currency: If frequently grouping by currency, consider a partial index
- Partition large tables: Partition by currency for separate processing
-- Partial index for EUR transactionsCREATE INDEX idx_orders_eur ON orders(created_at)WHERE monetary_currency(amount) = 'EUR';
-- Query uses indexSELECT SUM(amount)FROM ordersWHERE monetary_currency(amount) = 'EUR' AND created_at > '2025-01-01';Aggregate Summary
Section titled “Aggregate Summary”| Function | Description | Currency Check |
|---|---|---|
SUM(monetary) | Sum values | Yes - must match |
AVG(monetary) | Average with rounding | Yes - must match |
MIN(monetary) | Minimum value | Yes - must match |
MAX(monetary) | Maximum value | Yes - must match |
COUNT(*) | Count rows | No |