Adding a currency conversion to a SQL query - sql

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

Related

Combining multiple queries

I want a table with all customers and their last charge transaction date and their last invoice date. I have the first two, but don't know how to add the last invoice date to the table. Here's what I have so far:
WITH
--Last customer transaction
cust_trans AS (
SELECT customer_id, created
FROM charges a
WHERE created = (
SELECT MAX(created) AS last_trans
FROM charges b
WHERE a.customer_id = b.customer_id)),
--All customers
all_cust AS (
SELECT customers.id AS customer, customers.email, CAST(customers.created AS DATE) AS join_date, ((1.0 * customers.account_balance)/100) AS balance
FROM customers),
--Last customer invoice
cust_inv AS (
SELECT customer_id, date
FROM invoices a
WHERE date = (
SELECT MAX(date) AS last_inv
FROM invoices b
WHERE a.customer_id = b.customer_id))
SELECT * FROM cust_trans
RIGHT JOIN all_cust ON all_cust.customer = cust_trans.customer_id
ORDER BY join_date;
This should get what you need. Notice each individual subquery is left-joined to the customer table, so you always START with the customer, and IF there is a corresponding record in each subquery for max charge date or max invoice date, it will be pulled in. Now, you may want to apply a COALESCE() for the max dates to prevent showing nulls, such as
COALESCE(maxCharges.LastChargeDate, '') AS LastChargeDate
but your call.
SELECT
c.id AS customer,
c.email,
CAST(c.created AS DATE) AS join_date,
((1.0 * c.account_balance) / 100) AS balance,
maxCharges.LastChargeDate,
maxInvoices.LastInvoiceDate
FROM
customers c
LEFT JOIN
(SELECT
customer_id,
MAX(created) LastChargeDate
FROM
charges
GROUP BY
customer_id) maxCharges ON c.id = maxCharges.customer_id
LEFT JOIN
(SELECT
customer_id,
MAX(date) LastInvoiceDate
FROM
invoices
GROUP BY
customer_id) maxInvoices ON c.id = maxInvoices.customer_id
ORDER BY
c.created

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;

Validating an SQL query

I am a beginner with SQL and looking to write a query to identify users whose first transaction was a successful card payment over an equivalent value of 10 USD (amounts are in different currencies).
This is a theoretical exercise whereby I have the databases in excel but currently cannot access any SQL servers to validate this query.
Firstly I have defined the result set as follows:
SELECT t.user_id, min(t.created_date), (t.amount / fx.rate / Power (10, cd.exponent) AS amount)
FROM transactions AS t
This should yield the user ID, earliest date of transaction and the transaction amount in USD (original transaction converted into USD and converted into a cash amount from an integer amount).
Fairly comfortable with the last formula, just want to make sure the referencing below brings the fx.rate and cd.exponent correctly so it can actually run:
JOIN fx_rates AS fx
ON ( fx.ccy = t.currency
AND fx.base_ccy = 'USD' )
JOIN currency_details AS cd
ON cd.currency = t.currency
The above should ensure the 'amount' column has all the references necessary to be calculated.
Finally I am looking to apply a set of restrictions so data includes only completed card payments over 10 USD:
WHERE t.type='card_payment'
AND t.state='completed'
AND amount>=10
This is the tricky bit as I read that you can't reference an alias ('amount') as it isn't really in the result set but not sure if that applies here.
I have two questions:
1) Would this query produce a list of first transactions which were over 10USD? I don't want it to find when/if the transaction reached that threshold. I am only interested in the first transaction for each user. If the answer is no, would I be better of creating a table with first transactions and filtering on that instead? I honestly thought that's what I'm doing here.
2) Is referencing the alias 'amount' allowed within the query? If not, is another SELECT required here?
Full query
SELECT t.user_id, min(t.created_date), (t.amount / fx.rate / Power (10, cd.exponent) AS amount)
FROM transactions AS t
JOIN fx_rates AS fx
ON ( fx.ccy = t.currency
AND fx.base_ccy = 'USD' )
JOIN currency_details AS cd
ON cd.currency = t.currency
WHERE t.type='card_payment'
AND t.state='completed'
AND amount>=10
-------UPDATE 1-------
Following numerous comments and answers the updated query is as follows:
SELECT t.user_id, t.created_date,
  (t.amount / fx.rate / Power(10, cd.exponent)) AS amount
FROM (
    SELECT *, Row_Number () OVER
  (PARTITION BY t.user_id ORDER BY t.created_date) AS RowNum
    FROM transactions AS t)
  JOIN fx_rates fx 
    ON ( fx.ccy = t.currency
      AND fx.base_ccy = ‘USD') 
  JOIN currency_details cd 
    ON cd.currency = t.currency
WHERE RowNum = 1 
  AND t.type = ‘card_payment‘
  AND t.state = ‘completed‘
  AND (t.amount / fx.rate / Power(10, cd.exponent)) >= 10
GROUP  BY t.user_id;
The amount column name alias is not available in where condition.
You must repeat the code
SELECT t.user_id, min(t.created_date), (t.amount / fx.rate / Power (10, cd.exponent) ) AS amount
FROM transactions AS t
JOIN fx_rates AS fx
ON ( fx.ccy = t.currency
AND fx.base_ccy = 'USD' )
JOIN currency_details AS cd
ON cd.currency = t.currency
WHERE t.type='card_payment'
AND t.state='completed'
AND (t.amount / fx.rate / Power (10, cd.exponent) )>=10
this because the where clause is evaluated before the select clause so .. in where clause the alias column name is not available .. is used the original (row related) amount column content
"Would this query produce a list of first transactions which were over 10USD?"
No. Besides the fact that what you've written will produce multiple errors, the logic you are attempting here will not limit the results to the first ever transaction, rather it would return the first date of a completed card payment over $10.
"Is referencing the alias 'amount' allowed within the query?"
No. But you can repeat the same part of the select statement.
below is all untested
There are a variety of ways to accomplish what you want. One of the easiest to understand (I think) is to start with a query that orders the transactions by date per user:
select *, row_number() over (partition by user_id order by created_date) rown
from transactions
rown in the above query is the row number for each user (partition by user_id) with the earliest date (order by created_date) being row 1. The output of this query will have multiple rown columns being 1. Each of those rows represents the first ever transaction for that user. You can throw that query into a subquery and select only the rows where rown = 1
select *
from (
select *, row_number() over (partition by user_id order by created_date) rown
from transactions
) t
where t.rown=1
NOW you can add the rest of your stuff (note I don't know what to do with your power() statement)
select t.user_id, t.created_date, t.amount / fx.rate / 10.0 / cd.exponent AS amount
from (
select *, row_number() over (partition by user_id order by created_date) rown
from transactions
) t
inner JOIN fx_rates AS fx
ON fx.ccy = t.currency
AND fx.base_ccy = 'USD'
inner JOIN currency_details AS cd ON cd.currency = t.currency
where t.rown=1
and t.type='card_payment'
AND t.state='completed'
and t.amount / fx.rate / 10.0 / cd.exponent > 10.0
You might want >= 10.0 if you actually want at least $10.
You can also use CTE(Common Table Expressions)
WITH User_CTE (user_id , created_date , amount, RowNum)
AS
(
SELECT user_id , t.created_date , (t.amount / fx.rate / Power(10, cd.exponent)) AS amount
,ROW_NUMBER over (PARTITION BY t.user_id ORDER BY t.created_date asc ) AS RowNum
FROM transactions t
JOIN fx_rates fx
ON ( fx.ccy = t.currency
AND fx.base_ccy = 'USD')
JOIN currency_details cd
ON cd.currency = t.currency
WHERE
t.type = 'card_payment'
AND t.state = 'completed'
AND (t.amount / fx.rate / Power(10, cd.exponent)) >= 10
)
SELECT
user_id
, created_date
, amount
FROM
User_CTE
WHERE
RowNum =1

Using a column in sql join without adding it to group by clause

My actual table structures are much more complex but following are two simplified table definitions:
Table invoice
CREATE TABLE invoice (
id integer NOT NULL,
create_datetime timestamp with time zone NOT NULL,
total numeric(22,10) NOT NULL
);
id create_datetime total
----------------------------
100 2014-05-08 1000
Table payment_invoice
CREATE TABLE payment_invoice (
invoice_id integer,
amount numeric(22,10)
);
invoice_id amount
-------------------
100 100
100 200
100 150
I want to select the data by joining above 2 tables and selected data should look like:-
month total_invoice_count outstanding_balance
05/2014 1 550
The query I am using:
select
to_char(date_trunc('month', i.create_datetime), 'MM/YYYY') as month,
count(i.id) as total_invoice_count,
(sum(i.total) - sum(pi.amount)) as outstanding_balance
from invoice i
join payment_invoice pi on i.id=pi.invoice_id
group by date_trunc('month', i.create_datetime)
order by date_trunc('month', i.create_datetime);
Above query is giving me incorrect results as sum(i.total) - sum(pi.amount) returns (1000 + 1000 + 1000) - (100 + 200 + 150) = 2550.
I want it to return (1000) - (100 + 200 + 150) = 550
And I cannot change it to i.total - sum(pi.amount), because then I am forced to add i.total column to group by clause and that I don't want to do.
You need a single row per invoice, so aggregate payment_invoice first - best before you join.
When the whole table is selected, it's typically fastest to aggregate first and join later:
SELECT to_char(date_trunc('month', i.create_datetime), 'MM/YYYY') AS month
, count(*) AS total_invoice_count
, (sum(i.total) - COALESCE(sum(pi.paid), 0)) AS outstanding_balance
FROM invoice i
LEFT JOIN (
SELECT invoice_id AS id, sum(amount) AS paid
FROM payment_invoice pi
GROUP BY 1
) pi USING (id)
GROUP BY date_trunc('month', i.create_datetime)
ORDER BY date_trunc('month', i.create_datetime);
LEFT JOIN is essential here. You do not want to loose invoices that have no corresponding rows in payment_invoice (yet), which would happen with a plain JOIN.
Accordingly, use COALESCE() for the sum of payments, which might be NULL.
SQL Fiddle with improved test case.
Do the aggregation in two steps. First aggregate to a single line per invoice, then to a single line per month:
select
to_char(date_trunc('month', t.create_datetime), 'MM/YYYY') as month,
count(*) as total_invoice_count,
(sum(t.total) - sum(t.amount)) as outstanding_balance
from (
select i.create_datetime, i.total, sum(pi.amount) amount
from invoice i
join payment_invoice pi on i.id=pi.invoice_id
group by i.id, i.total
) t
group by date_trunc('month', t.create_datetime)
order by date_trunc('month', t.create_datetime);
See sqlFiddle
SELECT TO_CHAR(invoice.create_datetime, 'MM/YYYY') as month,
COUNT(invoice.create_datetime) as total_invoice_count,
invoice.total - payments.sum_amount as outstanding_balance
FROM invoice
JOIN
(
SELECT invoice_id, SUM(amount) AS sum_amount
FROM payment_invoice
GROUP BY invoice_id
) payments
ON invoice.id = payments.invoice_id
GROUP BY TO_CHAR(invoice.create_datetime, 'MM/YYYY'),
invoice.total - payments.sum_amount

Cannot group SQL results correctly

Hi i have a query which i need to show the number of transactions a user made,per day with the EUR equivalent of each transaction.
The query below does do that (find the eur equivalent by getting an average rate) but because the currencies are different i get the results by currency instead and not by total. what the query returns is:
Numb Transactions,Date, userid,transaction_type,total value (per currency),eur_equiv
1 12/12, 2, test 5 10
2 12/12,2, test 2 2
whereas i want it to return
Numb Transactions,Date, userid,transaction_type,total value (per currency),eur_equiv
1 12/12, 2, test 7 12
the query is shown below
SELECT COUNT(DISTINCT(ot.ID)) AS 'TRANSACTION COUNTER'
,CONVERT(VARCHAR(10) ,ot.CREATED_ON ,103) AS [DD/MM/YYYY]
,lad.ci
,ot.TRA_TYPE
,c.C_CODE
,CASE
WHEN op.CURRENCY_ID='CURRENCY-002' THEN SUM(CAST(op.IT_AMOUNT AS MONEY))
/(
SELECT AVG(CAST(cr.B_RATE AS MONEY)) AS AVG_RATE
FROM C_RATE cr
WHERE cr.CURRENCY_ID = 'CURRENCY-002'
)
WHEN op.CURRENCY_ID='-CURRENCY-005' THEN SUM(CAST(op.IT_AMOUNT AS MONEY))
/(
SELECT AVG(CAST(cr.B_RATE AS MONEY)) AS AVG_RATE
FROM C_RATE cr
WHERE cr.CURRENCY_ID = 'CURRENCY-005'
)
WHEN op.CURRENCY_ID='CURRENCY-006' THEN SUM(CAST(op.IT_AMOUNT AS MONEY))
/(
SELECT AVG(CAST(cr.B_RATE AS MONEY)) AS AVG_RATE
FROM C_RATE cr
WHERE cr.CURRENCY_ID = 'CURRENCY-006'
)
ELSE '0'
END AS EUR_EQUIVAL
FROM TRANSACTION ot
INNER JOIN PAYMENT op
ON op.ID = ot.ID
INNER JOIN CURRENCY c
ON op.CURRENCY_ID = c.ID
INNER JOIN ACCOUNT a
ON a.ID = ot.ACCOUNT_ID
INNER JOIN ACCOUNT_DETAIL lad
ON lad.A_NUMBER = a.A_NUMBER
INNER JOIN CUST cus
ON lad.CI = cus.CI
WHERE ot.TRA_TYPE_ID IN ('INBANK-TYPE'
,'IN-AC-TYPE'
,'DOM-TRANS-TYPE')
AND ot.STATUS_ID = 'COMPLETED'
AND cus.BRANCH IN ('123'
,'456'
,'789'
,'789')
GROUP BY
lad.CI
,CONVERT(VARCHAR(10) ,ot.CREATED_ON ,103)
,c.C_CODE
,op.CURRENCY_ID
,ot.TRAN_TYPE_ID
HAVING SUM(CAST(op.IT_AMOUNT AS MONEY))>'250000.00'
ORDER BY
CONVERT(VARCHAR(10) ,ot.CREATED_ON ,103) ASC
SELECT MIN([Numb Transactions]
, Date
, UserID
, Transaction_type
, SUM([Total Value]
, SUM([Eur Equiv]
FROM (
... -- Your current select (without order by)
) q
GROUP BY
Date
, UserId
, Transaction_type
The problem resides most likely in a double row in your joins. What I do, is Select * first, and see what columns generate double rows. You might need to adjust a JOIN relationship for the double rows to disappear.
Without any resultsets, it's very hard to reproduce the error you are getting.
Check for the following things:
Select * returns only double rows on the data i will merge with an aggregate function. If the answer here is "NO", you will need to alter a JOIN relationship with a Subselect. I am thinking of that Account and Account detail table.
certain joins can create duplicate rows if the join cannot be unique enough. Maybe you will have to join on multiple things here, for example JOIN table1 ON table1.ID = table2.EXT_ID and table1.Contact = table2.Contact
Couple of things:
1) Consider using a function for:
SELECT AVG(CAST(cr.B_RATE AS MONEY)) AS AVG_RATE
FROM C_RATE cr
WHERE cr.CURRENCY_ID = 'CURRENCY-002' with the currencyID as a parameter
2) Would grouping sets work here?
3) was the sum on the case or on the individual whens?
sum(CASE ..) vs sum(cast(op.IT_Amount as money)