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”-- Load extensionINSTALL monetary FROM community;LOAD monetary;
-- Load exchange ratesCREATE TABLE exchange_rates AS SELECT * FROM ecb_exchange_rates();
-- Configure the rates tableSET monetary_exchange_table = 'exchange_rates';
-- Convert!SELECT '100 EUR'::monetary ->> 'USD' AS in_usd;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:
-- EUR to USDSELECT '100 EUR'::monetary ->> 'USD';-- Result: 109.50 USD (rate dependent)
-- USD to EURSELECT '100 USD'::monetary ->> 'EUR';-- Result: 91.32 EUR
-- GBP to JPYSELECT '100 GBP'::monetary ->> 'JPY';-- Result: 19,234 JPYWorks with table columns:
CREATE TABLE prices (product VARCHAR, price monetary);INSERT INTO prices VALUES ('Widget', '29.99 EUR'), ('Gadget', '49.99 EUR'), ('Gizmo', '99.99 EUR');
SELECT product, price, price ->> 'USD' AS price_usdFROM prices;Currency Macros
Section titled “Currency Macros”Shorthand functions for common currencies:
-- Convert to specific currenciesSELECT USD('100 EUR'::monetary); -- To US DollarsSELECT EUR('100 USD'::monetary); -- To EurosSELECT GBP('100 EUR'::monetary); -- To British PoundsSELECT JPY('100 EUR'::monetary); -- To Japanese YenSELECT CHF('100 EUR'::monetary); -- To Swiss FrancsAll available macros:
| 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 |
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”-- Store rates with timestampsCREATE TABLE historical_rates ASSELECT * FROM ecb_exchange_rates();
-- Convert using rate from specific dateWITH rate_on_date AS ( SELECT rate FROM historical_rates WHERE currency_from = 'EUR' AND currency_to = 'USD' AND valid_from <= '2025-01-15' ORDER BY valid_from DESC LIMIT 1)SELECT monetary_convert('1000 EUR'::monetary, 'USD', rate)FROM rate_on_date;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');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