Currency Conversions
The extension provides multiple ways to convert monetary values between currencies - from simple one-liners to full control over exchange rates.
Quick Start
Section titled “Quick Start”Setup: Loading Exchange Rates
Section titled “Setup: Loading Exchange Rates”Before converting, you need exchange rates. The simplest approach:
-- Create rates table from ECB (free, no API key)CREATE TABLE exchange_rates ASSELECT * FROM ecb_exchange_rates();
-- Tell the extension which table to useSET monetary_exchange_table = 'exchange_rates';For persistent configuration, add to your ~/.duckdbrc:
SET monetary_exchange_table = 'exchange_rates';Conversion Methods
Section titled “Conversion Methods”Arrow Operator (->>)
Section titled “Arrow Operator (->>)”The cleanest syntax for conversions:
Works with table columns:
Currency Macros
Section titled “Currency Macros”Shorthand functions for common currencies:
All available macros (each has an _AT variant for historical rates):
| Region | Currencies |
|---|---|
| Major | EUR, USD, GBP, JPY, CHF |
| Europe | NOK, SEK, DKK, PLN, CZK, HUF, RON |
| Asia-Pacific | CNY, KRW, SGD, HKD, TWD, INR, THB, IDR, MYR, PHP, AUD, NZD |
| Americas | CAD, BRL, MXN, CLP, COP, ARS |
| Middle East/Africa | AED, SAR, ILS, TRY, ZAR, EGP |
| Crypto | BTC, ETH |
| Precious Metals | XAU, XAG |
For historical conversions, use the _AT suffix: USD_AT(price, date), EUR_AT(price, date), etc.
convert_to Function
Section titled “convert_to Function”Programmatic conversion with variable target:
-- Same as arrow operatorSELECT convert_to('100 EUR'::monetary, 'USD');
-- Useful with variablesSELECT convert_to(price, target_currency)FROM prices, (SELECT 'GBP' as target_currency);Direct Conversion with Explicit Rate
Section titled “Direct Conversion with Explicit Rate”When you have the rate already:
-- monetary_convert(value, target_currency, rate)SELECT monetary_convert('100 USD'::monetary, 'EUR', 0.92);-- Result: 92.00 EUR
-- Useful for custom rates or specific point-in-time ratesSELECT monetary_convert( '1000 EUR'::monetary, 'USD', 1.0850 -- Your specific rate);Conversion with Markup/Spread
Section titled “Conversion with Markup/Spread”Apply a percentage spread (common in FX trading):
-- monetary_convert_markup(value, target, rate, spread_pct)-- Spread is subtracted: effective_rate = rate * (1 - spread/100)
SELECT monetary_convert_markup('1000 EUR'::monetary, 'USD', 1.10, 2.0);-- Rate becomes: 1.10 * 0.98 = 1.078-- Result: 1078.00 USDReal-World Examples
Section titled “Real-World Examples”E-commerce: Multi-Currency Product Catalog
Section titled “E-commerce: Multi-Currency Product Catalog”-- Products with base EUR pricesCREATE TABLE products ( id INTEGER, name VARCHAR, price monetary);
INSERT INTO products VALUES (1, 'Basic Plan', '9.99 EUR'), (2, 'Pro Plan', '29.99 EUR'), (3, 'Enterprise', '99.99 EUR');
-- Show prices in multiple currenciesSELECT name, price AS eur, price ->> 'USD' AS usd, price ->> 'GBP' AS gbp, price ->> 'JPY' AS jpyFROM products;Financial Report: Convert Revenue by Region
Section titled “Financial Report: Convert Revenue by Region”CREATE TABLE revenue ( region VARCHAR, amount monetary, quarter VARCHAR);
INSERT INTO revenue VALUES ('Europe', '1500000 EUR', 'Q1'), ('Americas', '2000000 USD', 'Q1'), ('Asia', '180000000 JPY', 'Q1'), ('UK', '800000 GBP', 'Q1');
-- Total revenue in USDSELECT SUM(monetary_decimal(amount ->> 'USD')) AS total_usdFROM revenue;
-- By region in USDSELECT region, amount, amount ->> 'USD' AS amount_usdFROM revenueORDER BY monetary_decimal(amount ->> 'USD') DESC;Travel App: Currency Converter
Section titled “Travel App: Currency Converter”-- User's budget in their home currencyWITH budget AS ( SELECT '5000 USD'::monetary AS amount)SELECT amount AS original, amount ->> 'EUR' AS europe, amount ->> 'GBP' AS uk, amount ->> 'JPY' AS japan, amount ->> 'THB' AS thailand, amount ->> 'MXN' AS mexicoFROM budget;Trading: Apply Different Spreads
Section titled “Trading: Apply Different Spreads”-- Different spreads for different customer tiersCREATE TABLE fx_orders ( customer_tier VARCHAR, amount monetary, target_currency VARCHAR);
INSERT INTO fx_orders VALUES ('retail', '10000 EUR', 'USD'), ('premium', '50000 EUR', 'USD'), ('institutional', '1000000 EUR', 'USD');
-- Get base rateWITH base_rate AS ( SELECT rate FROM exchange_rates WHERE currency_from = 'EUR' AND currency_to = 'USD' ORDER BY valid_from DESC LIMIT 1)SELECT customer_tier, amount, CASE customer_tier WHEN 'retail' THEN monetary_convert_markup(amount, 'USD', rate, 3.0) WHEN 'premium' THEN monetary_convert_markup(amount, 'USD', rate, 1.5) WHEN 'institutional' THEN monetary_convert_markup(amount, 'USD', rate, 0.5) END AS convertedFROM fx_orders, base_rate;Historical Conversion: Point-in-Time Rates
Section titled “Historical Conversion: Point-in-Time Rates”Use the _AT suffix macros to convert using rates from a specific date:
-- Convert using rate closest to a specific dateSELECT USD_AT('1000 EUR'::monetary, '2025-01-15');-- Uses the rate closest to January 15, 2025
SELECT GBP_AT('500 USD'::monetary, '2025-01-10');-- Uses the GBP rate closest to January 10, 2025
-- Generic function with variable currencySELECT convert_at_date('1000 EUR'::monetary, 'USD', '2025-01-15');
-- Or with variable targetSELECT convert_to_at(price, target_currency, report_date)FROM transactions;The _AT macros find the rate closest to the specified date. By default, an error is thrown if the closest rate is more than 24 hours away:
-- Configure max staleness (default: 86400 seconds = 24 hours)SET monetary_max_rate_staleness = 604800; -- 1 week
-- Now rates up to 7 days away are acceptableSELECT USD_AT('1000 EUR'::monetary, '2025-01-15');Available _AT macros for all currencies: EUR_AT, USD_AT, GBP_AT, JPY_AT, CHF_AT, CAD_AT, AUD_AT, and more.
Aggregate Conversions
Section titled “Aggregate Conversions”Sum with Conversion
Section titled “Sum with Conversion”Convert each row then sum:
-- Sum all revenue in USDCREATE TABLE sales (amount monetary);INSERT INTO sales VALUES ('100 EUR'), ('200 EUR'), ('150 GBP'), ('10000 JPY');
SELECT monetary_sum(amount ->> 'USD') AS total_usdFROM sales;monetary_sum_converted Aggregate
Section titled “monetary_sum_converted Aggregate”For performance with large datasets:
-- Requires rate for each rowWITH sales_with_rates AS ( SELECT s.amount, r.rate FROM sales s JOIN exchange_rates r ON monetary_currency(s.amount) = r.currency_from::VARCHAR AND r.currency_to = 'USD')SELECT monetary_sum_converted(amount, 'USD', rate) AS totalFROM sales_with_rates;Conversion Logic
Section titled “Conversion Logic”The extension uses these rules for EUR-based rate tables:
| Conversion | Logic |
|---|---|
| Same currency | Returns unchanged |
| EUR → X | Multiply by rate |
| X → EUR | Divide by rate |
| X → Y | Cross-rate: (Y_rate / X_rate) |
Example cross-rate calculation:
-- GBP to JPY via EUR-- If EUR/GBP = 0.85 and EUR/JPY = 160-- Then GBP/JPY = 160 / 0.85 = 188.24
SELECT '100 GBP'::monetary ->> 'JPY';-- Uses cross-rate automaticallyConfiguration
Section titled “Configuration”Set Exchange Table
Section titled “Set Exchange Table”-- Use a specific table for rate lookupsSET monetary_exchange_table = 'my_custom_rates';
-- Check current settingSELECT current_setting('monetary_exchange_table');Set Max Rate Staleness
Section titled “Set Max Rate Staleness”Controls how far from the requested date a rate can be when using _AT macros:
-- Default: 24 hours (86400 seconds)SET monetary_max_rate_staleness = 86400;
-- Allow rates up to 1 week oldSET monetary_max_rate_staleness = 604800;
-- Require exact date match (within 1 hour)SET monetary_max_rate_staleness = 3600;Required Table Schema
Section titled “Required Table Schema”Your exchange rate table needs these columns:
| Column | Type | Required |
|---|---|---|
| currency_from | VARCHAR/ENUM | Yes |
| currency_to | VARCHAR/ENUM | Yes |
| rate | DECIMAL/DOUBLE | Yes |
| valid_from | TIMESTAMP | Yes |
The ->> operator automatically selects the most recent rate by valid_from.
Error Handling
Section titled “Error Handling”-- Missing rate throws errorSELECT '100 XYZ'::monetary ->> 'USD';-- Error: No exchange rate found for XYZ to USD
-- Currency mismatch in arithmetic still errorsSELECT '100 EUR'::monetary + '50 USD'::monetary;-- Error: Cannot add monetary values with different currencies
-- Convert first, then addSELECT ('100 EUR'::monetary ->> 'USD') + '50 USD'::monetary;-- Works: 159.50 USD