Validating an SQL query - sql

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

Related

calculate time difference of consecutive row dates in SQL

Hello I am trying to calculate the time difference of 2 consecutive rows for Date (either in hours or Days), as attached in the image
Highlighted in Yellow is the result I want which is basically the difference of the date in that row and 1 above.
How can we achieve it in the SQL? Attached is my complex code which has the rest of the fields in it
with cte
as
(
select m.voucher_no, CONVERT(VARCHAR(30),CONVERT(datetime, f.action_Date, 109),100) as action_date,f.col1_Value,f.col3_value,f.col4_value,f.comments,f.distr_user,f.wf_status,f.action_code,f.wf_user_id
from attdetailmap m
LEFT JOIN awftaskfin f ON f.oid = m.oid and f.client ='PC'
where f.action_Date !='' and action_date between '$?datef' and '$?datet'
),
.*select *, ROW_NUMBER() OVER(PARTITION BY action_Date,distr_user,wf_Status,wf_user_id order by action_Date,distr_user,wf_Status,wf_user_id ) as row_no_1 from cte
cte2 as
(
select *, ROW_NUMBER() OVER(PARTITION BY voucher_no,action_Date,distr_user,wf_Status,wf_user_id order by voucher_no ) as row_no_1 from cte
)
select distinct(v.dim_value) as resid,c.voucher_no,CONVERT(datetime, c.action_Date, 109) as action_Date,c.col4_value,c.comments,c.distr_user,v.description,c.wf_status,c.action_code, c.wf_user_id,v1.description as name,r.rel_value as pay_office,r1.rel_value as site
from cte2 c
LEFT OUTER JOIN aagviuserdetail v ON v.user_id = c.distr_user
LEFT OUTER JOIN aagviuserdetail v1 ON v1.user_id = c.wf_user_id
LEFT OUTER JOIN ahsrelvalue r ON r.resource_id = v.dim_Value and r.rel_Attr_id = 'P1' and r.period_to = '209912'
LEFT OUTER JOIN ahsrelvalue r1 ON r1.resource_id = v.dim_Value and r1.rel_Attr_id = 'Z1' and r1.period_to = '209912'
where c.row_no_1 = '1' and r.rel_value like '$?site1' and voucher_no like '$?trans'
order by voucher_no,action_Date
The key idea is lag(). However, date/time functions vary among databases. So, the idea is:
select t.*,
(date - lag(date) over (partition by transaction_no order by date)) as diff
from t;
I should note that this exact syntax might not work in your database -- because - may not even be defined on date/time values. However, lag() is a standard function and should be available.
For instance, in SQL Server, this would look like:
select t.*,
datediff(second, lag(date) over (partition by transaction_no order by date), date) / (24.0 * 60 * 60) as diff_days
from t;

How to sum from multiple columns and segregate into separate column if result is positive and negative

I am using postgresql and need to write a query to sum values from separate columns of two different tables and then segregate into separate columns if positive or negative.
For Example,
Below is the source table
Below is the resultant table which need to be created also used while populating it
I have written below query to aggregate sum and able to populate TOT_CREDIT and TOT_DEBIT column. Is there any optimized query to achieve that ?
select t.account_id,
t.transaction_date,
SUM(t.transaction_amt) filter (where t.transaction_amt >= 0) as tot_debit,
SUM(t.transaction_amt) filter (where t.transaction_amt < 0) as tot_credit,
case
when
(
SUM(t.transaction_amt) +
SUM(COALESCE(b.credit_balance,0)) +
SUM(COALESCE(b.debit_balance,0))
) < 0
then
(
SUM(t.transaction_amt) +
SUM(COALESCE(b.credit_balance,0)) +
SUM(COALESCE(b.debit_balance,0))
)
end as credit_balance,
case
when
(
SUM(t.transaction_amt) +
SUM(COALESCE(b.credit_balance,0)) +
SUM(COALESCE(b.debit_balance,0))
) > 0
then
(
SUM(t.transaction_amt) +
SUM(COALESCE(b.credit_balance,0)) +
SUM(COALESCE(b.debit_balance,0))
)
end as debit_balance,
from
transaction t
LEFT OUTER JOIN balance b ON (t.account_id = b.account_id
and t.transaction_date = b.transaction_date
and b.transaction_date=t.transaction_date- INTERVAL '1 DAYS')
group by
t.account_id,
t.transaction_date
Please provide some pointer.
EDIT 1: This query is not working in expected manner.
One way is to break your logic into smal queries and join them in the end!
select tw.account_id, tw.t_date,tw.t_c,th.T_D,fo.C_B,fi.d_B from
(select account_id, Transaction_date as t_date, sum(Transaction_AMT) as t_C from TransactionTABLE
where Transaction_AMT<0 group by account_id, Transaction_date ) as tw inner join
(select account_id, Transaction_date as t_date, sum(Transaction_AMT) as t_d from TransactionTABLE
where Transaction_AMT>0 group by account_id, Transaction_date ) as th on tw.account_id=th.account_id and tw.t_date=th.t_date inner join
(select account_id, Transaction_date as t_date, sum(Transaction_AMT) as C_B from TransactionTABLE
where sum(Transaction_AMT)<0 group by account_id, Transaction_date ) as fo on th.account_id=fo.account_id and th.t_date=fo.t_date inner join
(select account_id, Transaction_date as t_date, sum(Transaction_AMT) as d_B from TransactionTABLE
where sum(Transaction_AMT)>0 group by account_id, Transaction_date ) as fi on fi.account_id=fo.account_id and fi.t_date=fo.t_date;
Or else
You could try something as follows which calculates the running count of d_B over the Transaction_date and account_id
select account_id,
transaction_date,
SUM(transaction_amt) filter (where transaction_amt >= 0) as tot_debit,
SUM(transaction_amt) filter (where transaction_amt < 0) as tot_credit,
sum(transaction_amt) over (partition by account_id where sum(transaction_amt)<0) as credit_balance,
sum(transaction_amt) over (partition by account_id where sum(transaction_amt)>=0) as debit_balance
from TransactionTABLE group by account_id, Transaction_date order by 1,2;

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

Getting the value of a previous record using ROW_NUMBER() in SQL Server

Hopefully this is easy enough for those more experienced in SQL Server.
I have a table to customer loan activity data which is updated whenever an action happens on their account. For example if their limit is increased, a new record will be created with their new limit. I want to be able to create a listing of their activity where the activity amount is their new limit subtracting whatever their previous limit was.
At the moment I have the following but I'm struggling to work out how to access that previous record.
SELECT
CUSTOMER
,LEDGER
,ACCOUNT
,H.AMOUNT - COALESCE(X.AMOUNT, 0)
FROM
dbo.ACTIVITY H WITH (NOLOCK)
LEFT OUTER JOIN
(SELECT
CUSTOMER
,LEDGER
,ACCOUNT
,ACTIVITY_DATE
,AMOUNT
,ROW_NUMBER() OVER (PARTITION BY CUSTOMER, LEDGER, ACCOUNT ORDER BY ACTIVITY_DATE ASC) AS ROW_NUMBER
FROM
dbo.ACTIVITY WITH (NOLOCK)) X ON H.CUSTOMER = X.CUSTOMER
AND H.LEDGER = X.LEDGER
AND H.ACCOUNT = X.ACCOUNT
So basically I only want to subtract x.amount if it's the previous record but I'm not sure how to do this when I don't know what day it happened.
I thought Row_Number() would help me but I'm still a bit stumped.
Hope you hear from you all soon :)
Cheers
Here's a query that will only pass through dbo.Activity ONCE
SELECT H.CUSTOMER
,H.LEDGER
,H.ACCOUNT
,MAX(H.ACTIVITY_DATE) ACTIVITY_DATE
,SUM(CASE X.I WHEN 1 THEN AMOUNT ELSE -AMOUNT END) AMOUNT
FROM (SELECT CUSTOMER
,LEDGER
,ACCOUNT
,ACTIVITY_DATE
,AMOUNT
,ROW_NUMBER() OVER (PARTITION BY CUSTOMER, LEDGER, ACCOUNT ORDER BY ACTIVITY_DATE DESC) AS ROW_NUMBER
FROM dbo.ACTIVITY WITH (NOLOCK)
) H
CROSS JOIN (select 1 union all select 2) X(I)
WHERE ROW_NUMBER - X.I >= 0
GROUP BY H.CUSTOMER
,H.LEDGER
,H.ACCOUNT
,ROW_NUMBER - X.I;
And here's the DDL/DML for some data I used to test
CREATE TABLE dbo.ACTIVITY(CUSTOMER int, LEDGER int, ACCOUNT int, ACTIVITY_DATE datetime, AMOUNT int)
INSERT dbo.ACTIVITY select
1,2,3,GETDATE(),123 union all select
1,2,3,GETDATE()-1,16 union all select
1,2,3,GETDATE()-2,12 union all select
1,2,3,GETDATE()-3,1 union all select
4,5,6,GETDATE(),1000 union all select
4,5,6,GETDATE()-6,123 union all select
7,7,7,GETDATE(),99;
Alternatives
A more traditional approach using a subquery to get the previous row:
SELECT CUSTOMER, LEDGER, ACCOUNT, ACTIVITY_DATE,
AMOUNT - ISNULL((SELECT TOP(1) I.AMOUNT
FROM dbo.ACTIVITY I
WHERE I.CUSTOMER = O.CUSTOMER
AND I.LEDGER = O.LEDGER
AND I.ACCOUNT = O.ACCOUNT
AND I.ACTIVITY_DATE < O.ACTIVITY_DATE
ORDER BY I.ACTIVITY_DATE DESC), 0) AMOUNT
FROM dbo.ACTIVITY O
ORDER BY CUSTOMER, LEDGER, ACCOUNT, ACTIVITY_DATE;
Or ROW_NUMBER() the data twice and join between them
SELECT A.CUSTOMER, A.LEDGER, A.ACCOUNT, A.ACTIVITY_DATE,
A.AMOUNT - ISNULL(B.AMOUNT,0) AMOUNT
FROM (SELECT *, RN=ROW_NUMBER() OVER (partition by CUSTOMER, LEDGER, ACCOUNT
order by ACTIVITY_DATE ASC)
FROM dbo.ACTIVITY) A
LEFT JOIN (SELECT *, RN=ROW_NUMBER() OVER (partition by CUSTOMER, LEDGER, ACCOUNT
order by ACTIVITY_DATE ASC)
FROM dbo.ACTIVITY) B ON A.CUSTOMER = B.CUSTOMER
AND A.LEDGER = B.LEDGER
AND A.ACCOUNT = B.ACCOUNT
AND B.RN = A.RN-1 -- prior record
ORDER BY A.CUSTOMER, A.LEDGER, A.ACCOUNT, A.ACTIVITY_DATE;

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)