How to run a subquery based on results of a query SQL - sql

I have 2 queries i'd like to run. The idea here is to run a query on the transaction table by the transaction "type". Based on these results, I want to run another query to see the customers last transaction based on a specific type to see if the service ID was the same. If it's not the same, I want to flag it as "upgraded"
Here is the initial query that Pulls the results from a transactions table based on a transaction type:
Select customerid, serviceid
from Transactions
where (dtcreated > #startdate and dtcreated < #enddate) and (transactiontype = 'Cust Save')
The output for this is:
Customerid ServiceID
1 11
2 21
3 21
4 11
5 12
6 11
What i'd like to do next is run this query, matching the customerID to see what the customers last charge was:
Select serviceID, MAx(dtcreated) as MostRecent
From Transactions
Where (transactiontype = 'Cust Purchase')
Group By serviceID
My Final output combining the two queries would be:
Customerid ServiceID Last Purchase Upgraded?
1 11 11 No
2 21 11 Yes
3 21 12 Yes
4 11 10 Yes
5 12 12 No
6 11 11 No
I thought this might work but it doesn't quite give me what I want. It returns too many results, so the query is obviously not correct.:
Select serviceID, MAx(dtcreated) as MostRecent
From Transactions
WHERE Where (transactiontype = 'Cust Purchase') AND EXISTS
(Select customerid, serviceid
from Transactions
where (dtcreated > #startdate and dtcreated < #enddate) and (transactiontype = 'Cust Save'))
GROUP BY serviceid

If I understand the requirements properly you can use ROW_NUMBER to determine which record is the latest per customerID. Then you can JOIN this back to the transactions table to determine if there is a match in ServiceID:
SELECT r.CustomerID,
t.ServiceID,
t.dtCreated,
Upgraded = CASE WHEN t.ServiceID = cp.ServiceID THEN 0 ELSE 1 END
FROM Transactions AS t
INNER JOIN
( SELECT CustomerID,
ServiceID,
dtCreated,
RowNumber = ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY dtCreated DESC)
FROM Transactions
WHERE transactiontype = 'Cust Purchase'
) AS cp
ON cp.CustomerID = t.CustomerID
AND cp.RowNumber = 1
WHERE t.dtcreated > #startdate
AND t.dtcreated < #enddate
AND t.transactiontype = 'Cust Save'

I think you need something like this.
Here t1 gives you the max dtcreated per customer;
t2 contains all transactions in the date range given;
t3 gives you the last purchase per customer.
select
t1.customerid,
t3.serviceid as Last_Purchase_ServiceID,
t1.dtcreated as Last_Purchase_DateCreated,
t2.ServiceID as Current_Purchase_ServiceID,
t2.dtcreated as Current_Purchase_DateCreated
from
(
select customerid, max(dtcreated) as dtcreated
from
Transactions
group by customerid
)
t1
join
(
select customerid, serviceid, dtcreated
from Transactions
where (dtcreated > #startdate and dtcreated < #enddate) and (transactiontype = 'Cust Save')
) t2 on t1.customerid = t2.customerid
join
Transactions t3 on t1.customerid = t3.customerid and t1.dtcreated = t3.dtcreated

Try OUTER APPLY:
Select t.customerid, t.serviceid, o.MostRecent
from Transactions t
OUTER APPLY (
Select MAx(dtcreated) as MostRecent
From Transactions
Where transactiontype = 'Cust Purchase' and Customerid = t.Customerid --And ServiceID = t.ServiceID
) o
where (t.dtcreated > #startdate and t.dtcreated < #enddate) and (t.transactiontype = 'Cust Save')

Related

Return value in Access query

I need help with a query in Access. I need to return a customer name who has three or more orders within the past 14 days as of today's date that are still active. It also should display the orderdates in the results. This will populate on a report and grouped by the "cusname" and show each "orderdate". I tried using the query wizard and entering in the below sql but it populates no results. Can someone please help?
Select customerid, count(*), cusname,orderdate,orderstatus
From tablename
Where orderstatus="active"
Group by customerid,cusname,orderdate,orderstatus
Having Count(*) >=3;
Table:
CusName:|orderdate:
Mary 4/4/2021
Mary 4/3/2021
Mary 4/8/2021
Mary 3/23/2021
Bob 4/9/2021
Bob 4/1/2021
What I expect the result to be :
Table:
Customerid:|CusName:|orderdate:
1 Mary 4/4/2021
1 Mary 4/3/2021
1 Mary 4/8/2021
you should put filter on date as well
you probably shouldn't group by order date, if you're trying to count the "unique orders per customer" but not "unique orders per customer per date"
SELECT customer_id, cusname, COUNT(*)
FROM <tablename>
WHERE ABS(DateDiff('d', order_date, NOW())) <= 14 -- check order date
AND order_status = 'active' -- check order status
GROUP BY customer_id, cusname -- group by customer, not by order
HAVING COUNT(*) >= 3 -- filter customers with 3+ orders
This is rather tricky in MS Access, but you can use:
select t.*
from tablename as t
where t.orderstatus = "active" and
t.orderdate in (select top 3 t2.orderdate
from tablename t2
where t2.customerid = t.customerid and
t2.orderstatus = "active" and
t2.orderdate > dateadd("day", -14, date())
order by t2.orderdate desc
) and
3 <= (select count(*)
from tablename as t3
where t3.customerid = t.customerid and
t3.orderstatus = "active" and
t3.orderdate > dateadd("day", -14, date())
);
The first subquery gets the most recent three rows for each customer. The second checks that there are at least three.
Try this
SELECT t.customerid,
t.cusname,
t.orderdate,
t.orderstatus
FROM tablename AS t
WHERE t.orderstatus = "active"
AND t.orderdate > Dateadd("d", -14, DATE())
AND (SELECT Count(t1.cusname)
FROM tablename AS t1
WHERE t.customerid = t1.customerid
AND t1.orderstatus = "active"
AND t1.orderdate > Dateadd("d", -14, DATE())) >= 3

Can my query be made more efficient, and how about adding a GROUP BY clause?

I am trying to write a query that returns:
The total amount of transactions that occurred before a date range, for a particular customer.
The total amount of transactions that occurred within a date range, for a particular customer.
The total amount of payments that occurred before a date range, for a particular customer.
The total amount of payments that occurred within a date range, for a particular customer.
To that end, I've come up with the following query.
declare #StartDate DATE = '2016-08-01'
declare #EndDate DATE = '2016-08-31'
declare #BillingCategory INT = 0
select c.Id, c.Name, c.StartingBalance,
(select coalesce(sum(Amount), 0) from Transactions t where t.CustomerId = c.Id and t.[Date] < #StartDate) xDebits,
(select coalesce(sum(Amount), 0) from Transactions t where t.CustomerId = c.Id and t.[Date] >= #StartDate and t.[Data] <= #EndDate) Debits,
(select coalesce(sum(Amount), 0) from Payments p where p.CustomerId = c.Id and p.[Date] < #StartDate) xCredits,
(select coalesce(sum(Amount), 0) from Payments p where p.CustomerId = c.Id and p.[Date] >= #StartDate and p.[Date] <= #EndDate) Credits
from customers c
where c.BillingCategory in (0,1,2,3,4,5)
This query seems to give the results I want. I used the subqueries because I couldn't seem to figure out how to accomplish the same thing using JOINs. But I have a few questions.
Does this query retrieve the transaction and payment data for every single customer before filtering it according to my WHERE condition? If so, that seems like a big waste. Can that be improved?
I'd also like to add a GROUP BY to total each payment and transaction column by BillingCategory. But how can you add a GROUP BY clause here when the SELECTed columns are limited to aggregate functions if they are not in the GROUP BY clause?
The Transactions and Payments tables both have foreign keys to the Customers table.
Sample data (not real)
Customers:
Id Name BillingCategory
----- ------- ---------------
1 'ABC' 0
2 'DEF' 1
3 'GHI' 0
Transactions:
Id CustomerId Date Amount
----- ---------- ------------ ------
1 2 '2016-08-01' 124.90
2 2 '2016-08-04' 37.23
3 1 '2016-08-27' 450.02
Payments:
Id CustomerId Date Amount
----- ---------- ------------ ------
1 1 '2016-09-01' 50.00
2 1 '2016-09-23' 75.00
3 2 '2016-09-01' 100.00
You could build your sums seperately for Transactions and Payments in a CTE and then join them together:
WITH
CustomerTransactions AS
(
SELECT CustomerId,
SUM(CASE WHEN [Date] < #StartDate THEN 1 ELSE 0 END * COALESCE(Amount, 0)) AS xDebits,
SUM(CASE WHEN [Date] >= #StartDate THEN 1 ELSE 0 END * COALESCE(Amount, 0)) AS Debits
FROM Transactions
GROUP BY CustomerId
),
CustomerPayments AS,
(
SELECT CustomerId,
SUM(CASE WHEN [Date] < #StartDate THEN 1 ELSE 0 END * COALESCE(Amount, 0)) AS xCredits,
SUM(CASE WHEN [Date] >= #StartDate THEN 1 ELSE 0 END * COALESCE(Amount, 0)) AS Credits
FROM Payments
GROUP BY CustomerId
)
SELECT C.Id, c.Name, c.StartingBalance,
COALESCE(T.xDebits, 0) AS xDebits,
COALESCE(T.Debits, 0) AS Debits,
COALESCE(P.xCredits, 0) AS xCredits,
COALESCE(P.Credits, 0) AS Credits
FROM Custormers C
LEFT OUTER JOIN CustomerTransactions T ON T.CustomerId = C.Id
LEFT OUTER JOIN CustomerPayments P ON P.CustomerId = C.Id
WHERE C.BillingCategory IN(0, 1, 2, 3, 4, 5);
You can do with sub-queries to be more efficient. Pre-query grouped by each customer for only those customers who qualify by the categories in question. These sub-queries will always result in an at-most, 1 record per customer so you don't get a Cartesian result. Get that for your debits and credits and re-join back to your master list of customers with a left-join in case one side or the other (debits/credits) may not exist.
declare #StartDate DATE = '2016-08-01'
declare #EndDate DATE = '2016-08-31'
declare #BillingCategory INT = 0
select
c.ID,
c.Name,
c.StartingBalance,
coalesce( AllDebits.xDebits, 0 ) DebitsPrior,
coalesce( AllDebits.Debits, 0 ) Debits
coalesce( AllCredits.xCredits, 0 ) CreditsPrior,
coalesce( AllCredits.Credits, 0 ) Credits
from
customers c
LEFT JOIN
( select t.CustomerID,
sum( case when t.[Date] < #StartDate then Amount else 0 end ) xDebits,
sum( case when t.[Date] >= #StartDate then Amount else 0 end ) Debits
from
customers c1
JOIN Transactions t
on c1.CustomerID = t.CustomerID
where
c1.BillingCategory in (0,1,2,3,4,5)
group by
t.CustomerID ) AllDebits
on c.CustomerID = AllDebits.CustomerID
LEFT JOIN
( select p.CustomerID,
sum( case when p.[Date] < #StartDate then Amount else 0 end ) xCredits,
sum( case when p.[Date] >= #StartDate then Amount else 0 end ) Credits
from
customers c1
JOIN Payments p
on c1.CustomerID = p.CustomerID
where
c1.BillingCategory in (0,1,2,3,4,5)
group by
p.CustomerID ) AllCredits
on c.CustomerID = AllCredits.CustomerID
where
c.BillingCategory in (0,1,2,3,4,5)
COMMENT ADDITION
With respect to Thomas's answer, yes they are close. My version also adds the join to the customer table for the specific billing category and here is why. I don't know the size of your database, how many customers, how many transactions. If you are dealing with a large amount that DOES have performance impact, Thomas's version is querying EVERY customer and EVERY transaction. My version is only querying the qualified customers by the billing category criteria you limited.
Again, not knowing data size, if you are dealing with 100k records may be no noticeable performance. If you are dealing with 100k CUSTOMERS, could be a totally different story.
#JonathanWood, correct, but my version has each internal subquery inclusive of the cus

sql query - find distinct user from table

I am trying to solve a problem using SQL query and need some expert's advice.
I have below transaction table.
-- UserID, ProductId, TransactionDate
-- 1 , 2 , 2014-01-01
-- 1 , 3 , 2014-01-05
-- 2 , 2 , 2014-01-02
-- 2 , 3 , 2014-05-07
.
.
.
What I am trying to achieve is to find all user who purchased more than one product WITHIN 30 DAYS .
My query so far is like
select UserID, COUNT(distinct ProductID)
from tableA
GROUP BY UserID HAVING COUNT(distinct ProductID) > 1
I am not sure where to apply "WITH IN 30 DAYS" logic in the query .
The outcome should be :
1, 2
2, 1
Thanks in advance for your help.
Edit: Within 30 Days
SQL Fiddle
SELECT
a.UserID,
COUNT(DISTINCT ProductID)
FROM TableA a
INNER JOIN (
SELECT UserID, TransactionDate = MAX(TransactionDate)
FROM TableA
GROUP BY UserID
) AS t
ON t.UserID = a.UserID
AND a.TransactionDate >= DATEADD(DAY, -30, t.TransactionDate)
AND a.TransactionDate <= t.TransactionDate
GROUP BY a.UserID
You can use GROUP BY YEAR(TransactionDate), MONTH(TransactionDate)
SELECT
UserID,
COUNT(DISTINCT ProductID)
FROM TableA
GROUP BY
UserID, YEAR(TransactionDate), MONTH(TransactionDate)
HAVING
COUNT(DISTINCT ProductID) > 1
Just add a where clause.
SELECT UserID, COUNT(DISTINCT ProductID) cnt
FROM tableA
WHERE TransactionDate >= CAST(DATEADD(DAY,-30,GETDATE()) AS DATE)
GROUP BY UserID
HAVING COUNT(DISTINCT ProductID) > 1
This works because the where clause is performed BEFORE the Group By and Having. So first it filters out all transactions over 30 days old and then returns only people who bought two distinct products.
Query Processing Order:
http://blog.sqlauthority.com/2009/04/06/sql-server-logical-query-processing-phases-order-of-statement-execution/

How to create stored procedure to calculate beginning stock, stock in, stock out, stock balance in SQL Server 2005

I have 3 tables:
Product (Product_Model, Product_Color, Product_Code)
StockIn (StockIn_Date, Product_Code, Product_SerialNo)
StockOut (StockOut_Date, Product_SerialNo)
I want to create a stored procedure to generate report with requirement fields below:
(beginning stock, stock in, stock out, stock balance)
Group by (Product_Model, Product_Color, Product_Code)
Filter by 2 parameters: #StartDate and #EndDate
How to create a stored procedure with this scenario?
Assuming that your tables StockIn and StockOut have a column Amount, instead the question is senseless.
So, the ugly and the simple query is:
SELECT
BeginningStockIn
IsNull(BeginningStockIn.Amount, 0)-IsNull(BeginningStockOut.Amount, 0) BeginningStock,
IsNull(PeriodStockIn.Amount, 0) StockIn,
IsNull(PeriodStockOut.Amount, 0) StockOut,
IsNull(BeginningStockIn.Amount, 0)-IsNull(BeginningStockOut.Amount, 0)+IsNull(PeriodStockIn.Amount, 0)-IsNull(PeriodStockOut.Amount, 0) StockBalance
FROM Product
LEFT JOIN
(
SELECT
SUM(Amount) Amount,
Product_Code
FROM StockIn
WHERE StockIn_Date < #StartDate
) BeginningStockIn ON BeginningStockIn.Product_Code = Product.Product_Code
LEFT JOIN
(
SELECT
SUM(Amount) Amount,
Product_Code
FROM StockOut
WHERE StockOut_Date < #StartDate
) BeginningStockOut ON BeginningStockOut.Product_Code = Product.Product_Code
LEFT JOIN
(
SELECT
SUM(Amount) Amount,
Product_Code
FROM StockIn
WHERE StockIn_Date >= #StartDate AND StockIn_Date < #EndDate
) PeriodStockIn ON PeriodStockIn .Product_Code = Product.Product_Code
LEFT JOIN
(
SELECT
SUM(Amount) Amount,
Product_Code
FROM StockOut
WHERE StockOut_Date >= #StartDate AND StockOut_Date < #EndDate
) PeriodStockOut ON PeriodStockOut.Product_Code = Product.Product_Code
And the answer - to create the stored procedure you have to use CREATE PROCEDURE statement as described here
CREATE PROC YourProcName
#StartDate datetime,
#EndDate datetime
AS
BEGIN
SET NOCOUNT ON;
the query
END
H,
All credit to Oleg Dok who posted the script above.
Here is the same Data I am working with:
Here is the Code:
declare #startdate date = '2012-01-02'
declare #enddate date = '2012-01-31'
SELECT
Product.Product_Code,
Product.Product_Color,
Product.Product_Model,
IsNull(BeginningStockIn.Amount, 0)-IsNull(BeginningStockOut.Amount, 0) BeginningStock,
IsNull(PeriodStockIn.Amount, 0) StockIn,
IsNull(PeriodStockOut.Amount, 0) StockOut,
IsNull(BeginningStockIn.Amount, 0)-IsNull(BeginningStockOut.Amount, 0)+IsNull(PeriodStockIn.Amount, 0)-IsNull(PeriodStockOut.Amount, 0) StockBalance
FROM Product
LEFT JOIN
(
SELECT
SUM(Amount) Amount,
Product_Code
FROM StockIn
WHERE StockIn_Date < #StartDate
group by Product_Code
) BeginningStockIn ON BeginningStockIn.Product_Code = Product.Product_Code
LEFT JOIN
(
SELECT
SUM(Amount) Amount,
Product_Code
FROM StockOut
WHERE StockOut_Date < #StartDate
group by Product_Code
) BeginningStockOut ON BeginningStockOut.Product_Code = Product.Product_Code
LEFT JOIN
(
SELECT
SUM(Amount) Amount,
Product_Code
FROM StockIn
WHERE StockIn_Date >= #StartDate AND StockIn_Date < #EndDate
group by Product_Code
) PeriodStockIn ON PeriodStockIn .Product_Code = Product.Product_Code
LEFT JOIN
(
SELECT
SUM(Amount) Amount,
Product_Code
FROM StockOut
WHERE StockOut_Date >= #StartDate AND StockOut_Date < #EndDate
group by Product_Code
) PeriodStockOut ON PeriodStockOut.Product_Code = Product.Product_Code
Here is the Output:
Product_Code Product_Color Product_Model BeginningStock StockIn StockOut StockBalance
1 red 123 5 0 3 2
2 red 456 10 0 3 7
Is this what you wanted?

How find Customers who Bought Product A and D > 6 months apart?

I need advice from more advanced SQL experts on this.
I am being asked to create a report showing customers who bought Product 105 and who then bought Product 312 more than 6 months later.
For example, I have the following Orders table:
RecID CustID ProdID InvoiceDate
1 20 105 01-01-2009
2 20 312 01-04-2009
3 20 300 04-20-2009
4 31 105 07-10-2005
5 45 105 10-03-2007
6 45 300 11-10-2007
7 45 312 08-25-2008
I need a report that looks at this table and comes back with:
CustID ElapsedDays
45 327
Do I need to use a cursor and iterate record by record, comparing dates as I go?
If so, what would the cursor procedure look like? I have not worked with cursors, although I have done years of procedural programming.
Thanks!
You've got some good answers above; a self-join is the way to go. I want to suggest to you how best to think about a problem like this. What if you had the purchases of Product A and D in different tables? Not that you should store the data that way, but you should think about the data that way. If you did, you could join, say, product_a_purchases to product_d_purchases on customer ID and compare the dates. So, for purposes of your query, that's what you need to produce. Not an actual on-disk table that is product_a_purchases, but a table of records from your purchases table that includes only Product A purchases, and the same for Product D. That's where the self-join comes about.
select A.CustID, ElapsedDays = datediff(d, A.InvoiceDate, B.InvoiceDate)
from Orders A
inner join Orders B on B.CustID = A.CustID
and B.ProdID = 312
-- more than 6 months ago
and B.InvoiceDate > dateadd(m,6,A.InvoiceDate)
where A.ProdID = 105
The above query is a simple interpretation of your requirement, where ANY purchase of A(105) and D(312) occurred 6 months apart. If the customer purchased
A in Jan,
A in March,
A in July, and then purchased
D in September
it would return 2 rows for the customer (Jan and March), since both of those are followed by a D purchase more than 6 months later.
The following query instead finds all cases where the LAST A purchase is 6 months or more before the FIRST D purchase.
select A.CustID, ElapsedDays = datediff(d, A.InvoiceDate, B.InvoiceDate)
from (
select CustID, Max(InvoiceDate) InvoiceDate
from Orders
where ProdID = 105
group by CustID) A
inner join (
select CustID, Min(InvoiceDate) InvoiceDate
from Orders
where ProdID = 312
group by CustID) B on B.CustID = A.CustID
-- more than 6 months ago
and B.InvoiceDate > dateadd(m,6,A.InvoiceDate)
And if for the same scenario above, you don't want to see this customer because the A (Jul) and D (Sep) purchases are not 6 months apart, you can exclude them from the first query using an EXISTS filter.
select A.CustID, ElapsedDays = datediff(d, A.InvoiceDate, B.InvoiceDate)
from Orders A
inner join Orders B on B.CustID = A.CustID
and B.ProdID = 312
-- more than 6 months ago
and B.InvoiceDate > dateadd(m,6,A.InvoiceDate)
where A.ProdID = 105
AND NOT EXISTS (
SELECT *
FROM Orders C
WHERE C.CustID=A.CustID
AND C.InvoiceDate > A.InvoiceDate
and C.InvoiceDate < B.InvoiceDate
and C.ProdID in (105,312))
You can do this with a self-join:
select a.custid, DATEDIFF(dd, a.invoicedate, b.invoicedate)
from #t a
inner join #t b
on a.custid = b.custid
and a.prodid = 105
and b.prodid = 312
where DATEDIFF(dd, a.invoicedate, b.invoicedate) > 180
The first use of #t (aliased a) is for the first product and the second use of #t (aliased b) is for the second product. Here's the script I used to test it:
create table #t (
recid int,
custid int,
prodid int,
invoicedate date)
insert into #t select 1, 20, 105, '1/1/2009'
insert into #t select 2, 20, 312,'1/4/2009'
insert into #t select 3, 20, 300,'4/20/2009'
insert into #t select 4, 31, 105,'7/10/2005'
insert into #t select 5, 45, 105,'10/3/2007'
insert into #t select 6, 45, 300,'11/10/2007'
insert into #t select 7, 45, 312,'8/25/2008'
select a.custid, DATEDIFF(dd, a.invoicedate, b.invoicedate)
from #t a
join #t b
on a.custid = b.custid
and a.prodid = 105
and b.prodid = 312
where DATEDIFF(dd, a.invoicedate, b.invoicedate) > 180
drop table #t
Probably something like this would work:
select CustID, datediff(day, O1.InvoiceDate, O2.InvoiceDate) as ElapsedDays
from Orders O1
inner join Orders O2
on O1.CustId = O2.CustId
and dateadd(month, 6, O1.InvoiceDate) <= O2.InvoiceDate
where
O1.ProdId = 105
and O2.ProdId = 312