Implicitly Calculate Exchange Rates in SQL from Exchange Rate table - sql

I have one table of data with revenue in multiple currencies (let's call this one TRANSACTION_TABLE, with columns as such:
TRANSACTION_NAME
TRANSACTION_VALUE
CURRENCY
, and another table with exchange rates (EXCHANGE_RATE) with columns as such:
FROM_CURRENCY (e.g. JPY)
TO_CURRENCY (e.g. USD)
EXCHANGE_RATE (x)
The table has, at minimum, every currency converting to USD, but is not exhaustive with exchange rates for non-USD TO_CURRENCY values.
What I'm trying to achieve, is a query which converts the transactions to any currency, even if not explicitly stipulated in the EXCHANGE_RATE table, by converting the currencies to USD first, and then from USD into the destination currency.
E.g. 1000 JPY to GBP:
Find rate JPY to USD - calculation = 1000 * EXCHANGE_RATE = 9
Find rate GBP to USD - calculation = 9 \ EXCHANGE_RATE = 7
At the moment, I've done a left join for TRANSACTION_TABLE on EXCHANGE_RATE but I'm lost at where to go next.
Any assistance would be greatly appreciated.
The query (very basic) I've built so far is as follows, and I'm a novice SQL user. I built this query first to convert to USD, which works fine (as my Exchange Rate table contains values for all currencies to USD) - but it obviously fails when setting the destination currency as GBP, as it'll just return nulls.
SELECT TRANSACTION_NAME,
SUM (TRANSACTION_VALUE * EXCHANGE_RATE)
AS "REVENUE GBP"
FROM TRANSACTION_TABLE S
LEFT JOIN EXCHANGE_RATE C ON S.CURRENCY = C.FROM_CURRENCY AND C.TO_CURRENCY = 'GBP'
ORDER BY TRANSACTION_NAME

If your EXCHANGE_RATE table is exhaustive to USD, then you won't ever have more than two "hops" to do your conversion. At most, you'll convert to USD and then from USD to whatever. Given that, I would just code for all the possible cases rather than try something fancy like a CONNECT BY.
"All possible cases", I think, are:
The transaction is already in the target currency
The transaction is in a currency that is directly convertible to the target currency
The transaction must be converted to USD and then from USD to the target currency.
Here is a query that will do that. The WITH clauses are just to give it some data -- they won't be part of your solution, since you have the actual tables.
WITH rates ( from_currency, to_currency, exchange_rate ) AS
( SELECT 'JPY', 'USD', 0.009 FROM DUAL UNION ALL
SELECT 'GBP', 'USD', 1.31 FROM DUAL UNION ALL
SELECT 'CNY', 'USD', 0.15 FROM DUAL UNION ALL
SELECT 'JPY', 'CNY', 0.06 FROM DUAL),
txns ( transaction_name, transaction_value, currency ) AS
( SELECT 'txn 1 in JPY', 1000, 'JPY' FROM DUAL UNION ALL
SELECT 'txn 2 in GBP', 1000, 'GBP' FROM DUAL UNION ALL
SELECT 'txn 3 IN CNY', 1000, 'CNY' FROM DUAL UNION ALL
SELECT 'txn 4 IN unknown', 1000, 'XXX' FROM DUAL),
params ( target_currency ) AS
( SELECT 'CNY' FROM DUAL )
SELECT t.transaction_name,
t.transaction_value base_value,
t.currency base_currency,
t.transaction_value * CASE WHEN t.currency = params.target_currency THEN 1
WHEN r1.from_currency IS NOT NULL THEN r1.exchange_rate
ELSE r2usd.exchange_rate / r2tar.exchange_rate END converted_value,
params.target_currency converted_currency
FROM params CROSS JOIN
txns t
LEFT JOIN rates r1 ON r1.from_currency = t.currency AND r1.to_currency = params.target_currency
LEFT JOIN rates r2usd ON r2usd.from_currency = t.currency AND r2usd.to_currency = 'USD'
LEFT JOIN rates r2tar ON r2tar.from_currency = params.target_currency AND r2tar.to_currency = 'USD'

I'd propose to make an extra step to expand you exchange table with the exchange rates additionaly defined using UDS as transfer currency.
This query adds the new rates calulated via USD. It is a simple inner join constrained so that the calculation is via 'USD' and the from and to currencies are different. The WHERE clause limits the already know combinations.
select er1.FROM_CURRENCY, er2.TO_CURRENCY, er1.EXCHANGE_RATE * er2.EXCHANGE_RATE EXCHANGE_RATE
from exchange_rates er1
join exchange_rates er2
on er1.TO_CURRENCY = 'USD' and er2.FROM_CURRENCY = 'USD' and er1.FROM_CURRENCY != er2.TO_CURRENCY
where (er1.FROM_CURRENCY, er2.TO_CURRENCY)
not in (select FROM_CURRENCY, TO_CURRENCY from exchange_rates)
You may define a physical new table or view or even perform it only as a subquery as an UNION ALL of your original table and the result of this query.
Your final query uses this extended exchange rate table instead of the original one.
Here are sample data I tested with
create table exchange_rates
as
select 'GBP' FROM_CURRENCY, 'USD' TO_CURRENCY, 1.31 EXCHANGE_RATE from dual union all
select 'EUR' FROM_CURRENCY, 'USD' TO_CURRENCY, 1.16 EXCHANGE_RATE from dual union all
select 'AUD' FROM_CURRENCY, 'USD' TO_CURRENCY, .73 EXCHANGE_RATE from dual union all
select 'USD' FROM_CURRENCY, 'GBP' TO_CURRENCY, .76 EXCHANGE_RATE from dual union all
select 'USD' FROM_CURRENCY, 'EUR' TO_CURRENCY, .86 EXCHANGE_RATE from dual union all
select 'USD' FROM_CURRENCY, 'AUD' TO_CURRENCY, 1.36 EXCHANGE_RATE from dual union all
select 'GBP' FROM_CURRENCY, 'EUR' TO_CURRENCY, 1.12 EXCHANGE_RATE from dual;

Related

SQL query applying currency conversion rate

Please help to compose an SQL query having a table with currency conversion rates that should be applied to the other table with business data, for example:
currency_rates (conversion rate is given for every beginning of the month, starting from some moment)
currency_code
rate against USD
date
CAD
1.354
2022-11-01
CAD
1.3445
2022-12-01
CAD
1.3573
2023-01-01
business_data (fees are in USD, aggregated by name and date)
name
sum(fee) in USD
date
aaa
92.52
2021-10-10
bbb
76.18
2022-11-11
ccc
113.79
2022-12-12
ddd
133.42
2023-02-02
The expected result should be the following, assuming that aggregated fee dates lay in the range of conversion rate dates (the actual result of the multiplication isn't matter here, I show x*y just to describe an idea):
name
sum(fee) in USD * CAD conversion rate
date
aaa
92.52 * 1.354 (applied first existing rate)
2021-10-10
bbb
76.18 * 1.354
2022-11-11
ccc
113.79 * 1.3445
2022-12-12
ddd
133.42 * 1.3573 (applied last existing rate)
2023-02-02
We use Snowflake DB.
Don't hesitate to ask questions or add suggestions, thank you!
You can join the tables on year and month
select a.*, b.currency_rate --multiply as you wish
from business_data a
join currency_date b on
year(a.date)=year(b.date) and
month(a.date)=month(b.date)
Or more concisely,
select a.*, b.currency_rate --multiply as you wish
from business_data a
join currency_date b on trunc(a.date,'month')=b.date
As Adrian notes, my solution above doesn't account for the fact that your currency_data table does not have all the months that exist in your business_data table so let's fix that separately instead of complicating the code above.
Your sample data doesn't establish any relationship between currency_code and business data. For that reason I am going to use hardcoded 'CAD' for currency. If your business_data table has a column that ties to currency_code, you can easily modify this solution by incorporating that column in the full outer join and select statement. Also, you can switch up the order of columns inside coalesce to handle situations where you have both "last existing" and "first existing" rate to choose from.
with cte as
(select 'CAD' as currency_code, --ideally, this would a column that joins to currency data table even though your sample data doesn't indicate such a relationship
b.rate,
coalesce(trunc(a.business_date, 'month'),currency_date) as currency_date
from business_data a
full outer join currency_data b on b.currency_date=trunc(a.business_date, 'month') )
select currency_code ,
currency_date,
coalesce(rate,
lead(rate) ignore nulls over (partition by currency_code order by currency_date),
lag(rate) ignore nulls over (partition by currency_code order by currency_date)) as rate
from cte
order by currency_date
UNION ALL
LAG()
LEAD()
COALESCE()
JOIN
BETWEEN
WITH CURRENCY_RATES AS(
SELECT 'CAD' CURRENCY_CODE, 1.354 RATE, '2022-11-01'::DATE CURRENCY_DATE
UNION ALL SELECT 'CAD' CURRENCY_CODE, 1.3445 RATE, '2022-12-01'::DATE CURRENCY_DATE
UNION ALL SELECT 'CAD' CURRENCY_CODE, 1.3573 RATE, '2023-01-01'::DATE CURRENCY_DATE)
,CURRENCY_RATES_ENHANCED AS (
SELECT
CURRENCY_CODE
, RATE
, CURRENCY_DATE
, IFF(LAG(CURRENCY_DATE)OVER(PARTITION BY CURRENCY_CODE ORDER BY CURRENCY_DATE) IS NULL,'1900-01-01',CURRENCY_DATE) CURRENCY_START_DATES
, COALESCE(LEAD(CURRENCY_DATE)OVER(PARTITION BY CURRENCY_CODE ORDER BY CURRENCY_DATE ASC),CURRENT_DATE+999)-INTERVAL '1 SEC' CURRENCY_END_DATES
FROM CURRENCY_RATES
)
,BUSINSESS_DATA AS ( SELECT 'aaa' NAME, 92.52 FEE_IN_USD, '2021-10-10'::DATE BUSINESS_DATE
UNION ALL SELECT 'bbb' NAME, 76.18 FEE_IN_USD, '2022-11-11'::DATE BUSINESS_DATE
UNION ALL SELECT 'ccc' NAME, 113.79 FEE_IN_USD, '2022-12-12'::DATE BUSINESS_DATE
UNION ALL SELECT 'ddd' NAME, 133.42 FEE_IN_USD, '2023-02-02'::DATE BUSINESS_DATE)
SELECT
NAME,
FEE_IN_USD||' * '||RATE||
IFF(CURRENCY_START_DATES='1900-01-01'::DATE AND DATE_TRUNC('MONTH',B.BUSINESS_DATE)<>C.CURRENCY_DATE ,' (applied first existing rate)'
,IFF(CURRENCY_END_DATES=CURRENT_DATE+999-INTERVAL '1 SEC',' (applied last existing rate)',null)) VOLIA
,B.BUSINESS_DATE
FROM
BUSINSESS_DATA B
JOIN CURRENCY_RATES_ENHANCED C
ON BUSINESS_DATE BETWEEN CURRENCY_START_DATES AND CURRENCY_END_DATES
;

Using Cases for currency conversion in oracle

I have two tables
Payments_history
CREATE TABLE payments_history(payment_date , account_id , currency, amount) AS
SELECT TO_DATE ('05/01/2022','DD/MM/YYYY'), 2291969088, 'GBP', 10.00 FROM DUAL UNION ALL
SELECT TO_DATE ('05/01/2022','DD/MM/YYYY'), 7851880663, 'USD', 20 FROM DUAL UNION ALL
SELECT TO_DATE ('06/01/2022','DD/MM/YYYY'), 5326844767, 'USD', 3.000 FROM DUAL UNION ALL
SELECT TO_DATE ('05/01/2022','DD/MM/YYYY'), 3668657617, 'EUR', 40 FROM DUAL UNION ALL
SELECT TO_DATE ('06/01/2022','DD/MM/YYYY'), 9040142052, 'GBP', 30.000 FROM DUAL
Historics_rates
Create TABLE Historics_rates(t_date,from_ccy,to_ccy,rate) AS
SELECT TO_DATE ('06/01/2022','DD/MM/YYYY'),'GBP','EUR',1.1832 FROM DUAL UNION ALL
SELECT TO_DATE ('06/01/2022','DD/MM/YYYY'),'AUD','GBP',0.5263 FROM DUAL UNION ALL
SELECT TO_DATE ('06/01/2022','DD/MM/YYYY'),'EUR','GBP',0.8452 FROM DUAL UNION ALL
SELECT TO_DATE ('05/01/2022','DD/MM/YYYY'),'USD','GBP',0.7388 FROM DUAL UNION ALL
SELECT TO_DATE ('05/01/2022','DD/MM/YYYY'),'EUR','USD',1.1441 FROM DUAL
what I am trying to find is the 'daily amount GBP Equivalent per day for the last three months'. For example on date '05/01/2022 ' check the given amount in Payments_history table if the currency is GBP add it into total and move on to the next payment, next if the currency is in USD check for its equivalent in GBP rate and convert it into GBP and print the result. If the amount is in some currency whose from_ccy is not GBP skip that transaction
Date Amount in GBP
2022-01-05 24.78
This is what I have done so far
DECLARE
total NUMBER;
BEGIN
select PH.PAYMENT_DATE,
CASE
WHEN PH.CURRENCY = 'GBP' THEN total = total + PH.AMOUNT
WHEN PH.CURRENCY = 'USD' AND HR.from_ccy = 'USD' AND HR.to_ccy = 'GBP' THEN total = total + (PH.AMOUNT*HR."rate")
WHEN PH.CURRENCY = 'AUD' AND HR.from_ccy = 'AUD' AND HR.to_ccy = 'GBP' THEN total = total + (PH.AMOUNT*HR."rate")
WHEN PH.CURRENCY = 'EUR' AND HR.from_ccy = 'EUR' AND HR.to_ccy = 'GBP' THEN total = total + (PH.AMOUNT*HR."rate")
ELSE 'CURRENCY NOT FOUND'
END AS total
FROM "historic_rates" AS HR RIGHT JOIN PAYMENTS_HISTORY AS PH on HR."date" = PH.payment_date AND PH.Currency = HR."from_ccy"
WHERE Extract(Month from PH.payment_date) = Extract(month from add_months( sysdate, -3 )) GROUP BY PH.PAYMENT_DATE;
but it's giving me error
[Err] ORA-06550: line 8, column 40:
PL/SQL: ORA-00905: missing keyword
ORA-06550: line 6, column 1:
PL/SQL: SQL Statement ignored
ORA-06550: line 15, column 117:
PLS-00103: Encountered the symbol "end-of-file" when expecting one of the following:
( begin case declare end exception exit for goto if loop mod
null pragma raise return select update while with
<an identifier> <a double-quot
Your errors include:
Using = inside the THEN clause of a CASE expression is invalid syntax.
You cannot refer to a column alias inside the sub-query where it is defined (except in an ORDER BY clause) so you cannot use total inside the CASE expression.
A CASE expression needs to have the same data type in all of its outputs so you cannot mix numbers and strings.
You GROUP BY PH.PAYMENT_DATE but you do not have any aggregation function around the CASE expression.
If you do aggregate then 'CURRENCY NOT FOUND' is not something you can total.
In Oracle, AS before a table alias is invalid syntax.
You have a mix of quoted identifiers HR."from_ccy" and unquoted identifiers HR.from_ccy. While it is possible to have both in a table, one of them is almost certainly wrong (and using quoted identifiers is bad practice) and your DDL statements do not use quotes.
Unless you really do intend to get rows from December of any year, do not compare only months. Compare on a range from the start of the month 3-months ago until before the start of the month two-months ago.
Your PL/SQL block does not have an END statement.
You are using a SELECT statement in PL/SQL without using SELECT ... INTO.
You probably don't want to use PL/SQL.
None of your sample data is from 3 months ago (December 2021).
Something like this:
select PH.PAYMENT_DATE,
SUM(
CASE
WHEN PH.CURRENCY = 'GBP'
THEN PH.AMOUNT
WHEN PH.CURRENCY IN ('USD', 'AUD', 'EUR')
AND HR.from_ccy = PH.CURRENCY
AND HR.to_ccy = 'GBP'
THEN PH.AMOUNT*HR.rate
END
) As total
FROM historics_rates HR
RIGHT JOIN PAYMENTS_HISTORY PH
on HR.t_date = PH.payment_date
AND PH.Currency = HR.from_ccy
WHERE PH.payment_date >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -3)
AND PH.payment_date < ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -2)
GROUP BY PH.PAYMENT_DATE;
Which, for the sample data:
CREATE TABLE payments_history(payment_date , account_id , currency, amount) AS
SELECT TO_DATE ('05/12/2021','DD/MM/YYYY'), 2291969088, 'GBP', 10.00 FROM DUAL UNION ALL
SELECT TO_DATE ('05/12/2021','DD/MM/YYYY'), 7851880663, 'USD', 20 FROM DUAL UNION ALL
SELECT TO_DATE ('06/12/2021','DD/MM/YYYY'), 5326844767, 'USD', 3.000 FROM DUAL UNION ALL
SELECT TO_DATE ('05/12/2021','DD/MM/YYYY'), 3668657617, 'EUR', 40 FROM DUAL UNION ALL
SELECT TO_DATE ('06/12/2021','DD/MM/YYYY'), 9040142052, 'GBP', 30.000 FROM DUAL;
Create TABLE Historics_rates(t_date,from_ccy,to_ccy,rate) AS
SELECT TO_DATE ('06/12/2021','DD/MM/YYYY'),'GBP','EUR',1.1832 FROM DUAL UNION ALL
SELECT TO_DATE ('06/12/2021','DD/MM/YYYY'),'AUD','GBP',0.5263 FROM DUAL UNION ALL
SELECT TO_DATE ('06/12/2021','DD/MM/YYYY'),'EUR','GBP',0.8452 FROM DUAL UNION ALL
SELECT TO_DATE ('05/12/2021','DD/MM/YYYY'),'USD','GBP',0.7388 FROM DUAL UNION ALL
SELECT TO_DATE ('05/12/2021','DD/MM/YYYY'),'EUR','USD',1.1441 FROM DUAL;
Outputs:
PAYMENT_DATE
TOTAL
06-DEC-21
30
05-DEC-21
24.776
db<>fiddle here

SQL case statement, filter by date, group by id

I have two tables: transactions and currency. Transactions table contains transactions:
id date client_id currency amount
2 '2017-07-18' 29 'EURO' 340
3 '2018-08-09' 34 'RUB' 5000
Currency table contains currency exchange rates EURO or USD to RUB - 1st row for example means that 1 EURO = 70 RUB. For weekends the are no values as banks are closed and for calculations I need to use Friday exchange rates:
date currency value
'2017-08-07' 'EURO' 70
'2018-08-07' 'USD' 60
'2018-09-09' 'USD' NULL
So I need to calculate amount spent by every client in RUB. And if possible not use window functions.
I tried to use case when and group by client_id but then I need to consider currency rates every time they made a transaction and I don't know how to provide for that.
select t.*, amount * coalesce((select value
from currency c
where c.currency = t.currency
and c.date <= t.date order by c.date desc limit 1),
1)
from transactions t
Assumes if no currency is found it is RUB so it uses 1 as exchange rate.
You can express this with a lateral join:
select t.*,
t.amount * c.value as rub
from transactions t left join lateral
(select c.*
from currency c
where c.currency = t.currency and
c.value is not null and
c.date <= t.date
order by c.date desc
fetch first 1 row only
) c;

How to Select and multiply certain columns based off criteria?

I am trying to write a code that is dynamic for European countries to capture the appropriate price based on the certain country. I am trying to pull data where the List Price is less than 4000, but wanted to add a statement that if the Currency is in SEK, then I want to multiply that list price by 10 and then if it's less than 4000 I want to include it. For other Currencies just take the list price as is for currency codes EUR, GBP, and CHF.
For example this is what I have and it's not working correctly:
SELECT
[Country_IBS]
,[Item_Number]
,[Price_List]
,List_Price
,[Currency]
FROM #temp4
WHERE List_Price < 4000
OR (SELECT List_Price * 10 WHERE Currency = 'SEK') < 4000
AND Currency = 'EUR'
OR Currency = 'GBP'
OR Currency = 'CHF'
I would do:
where
list_price * (case when currency = 'SEK' then 10 else 1 end) < 4000
and currency in ('EUR', 'GBP', 'CHF', 'SEK')
Or you can use the more verbose form:
where
(currency in ('EUR', 'GBP', 'CHF') and list_price < 4000)
or (currency = 'SEK' and price < 400)
AND has higher precedence than OR. You need to club you conditions in parentheses.
Your query should be like this:
--version 1: Using OR
SELECT
[Country_IBS]
,[Item_Number]
,[Price_List]
,List_Price
,[Currency]
FROM #temp4
WHERE (List_Price < 4000 AND Currency IN ('EUR','GBP','CHF'))
OR (List_Price * 10 < 4000 AND Currency = 'SEK')
--Version 2: Using UNION ALL
SELECT
[Country_IBS]
,[Item_Number]
,[Price_List]
,List_Price
,[Currency]
FROM #temp4
WHERE List_Price < 4000 AND Currency IN ('EUR','GBP','CHF')
UNION ALL
SELECT
[Country_IBS]
,[Item_Number]
,[Price_List]
,List_Price
,[Currency]
FROM #temp4
WHERE List_Price * 10 < 4000 AND Currency = 'SEK'
These queries returning same resultset might have different query plan as per indices created on your table. Use whichever is more efficient than another.
Explore Operator Precedence and UNION for more details.

Adding a currency conversion to a SQL query

I have a database with a list of user purchases.
I'm trying to extract a list of users whose last successful purchase had a value of £100 or greater, which I have done:
SELECT
t.purchase_id
t.user_id,
t.purchase_date,
t.amount,
t.currency,
FROM
transactions t
INNER JOIN
(SELECT user_id, MAX(purchase_date) AS first_transaction
FROM transactions
GROUP BY user_id) frst ON t.user_id = frst.user_id
AND t.created_date = frst.first_transaction
WHERE
amount >= 100
ORDER BY
user_id;
The problem is that some of my purchases are in USD and some are in CAD. I would like to ensure that the value of the latest purchase is over £100 GBP despite the purchase currency.
Luckily I have another table with exchange rates:
base_currency currency exchange_rate
-----------------------------------------------
GBP USD 1.220185624
GBP CAD 1.602048721
So technically I just need to convert the amount using the exchange rate. I've hit a roadblock on how I can incorporate that into my current query. I'm thinking I need to create an extra column for amount_in_gbp but am not sure how to incorporate the case logic into my query?
You can avoid any JOIN statement:
SELECT t.purchase_id
,t.user_id
,t.purchase_date
,t.amount
,t.currency
FROM transactions t
INNER JOIN (
SELECT user_id
,MAX(purchase_date) AS first_transaction
FROM transactions
GROUP BY user_id
) frst ON t.user_id = frst.user_id
AND t.created_date = frst.first_transaction
WHERE (
SELECT t.amount / e.exchange_rate
FROM exchange AS e
WHERE t.currency = e.currency
) >= 100
ORDER BY user_id;
So that your column will be converted in GBP currency.
You join to the table:
SELECT t.*,
(t.amount / exchange_rate) as amoung_gbp
FROM transactions t LEFT JOIN
exchange e
ON t.currency = e.currency AND e.base_currency = 'GBP'
If you want to put this in a where clause, you need to repeat the expression:
where (t.amount / exchange_rate) > 100