GROUP BY in subquery T-SQL - sql-server

I am trying to make a table like this:
ProductName | SalesByDate | TotalSalesUntilDate
A | 5 | 15
B | 10 | 30
C | 20 | 25
D | 18 | 43
SalesByDate means the number of product sold for each product on the input date and TotalSalesUntilDate indicates the number of product sold for each product from the first date of the month until the input date (example of input date: 17 March 2010)
I wrote this query using subquery:
select p.ProductName, A.SalesByDate,
(select(SUM(case when (pd.Date between '01' and #Date)
then s.SalesByDate else 0 end))
from Period pd
inner join Sales s on pd.TimeID = s.TimeID
full join Product p on s.ProductID = p.ProductID) as TotalSalesUntilDate
from Product p join
(select s.ProductID, pd.Date, s.SalesByDate
from Period pd join Sales s on pd.TimeID = s.TimeID) A on
p.ProductID = A.ProductID where #Date = A.Date
but I got the result:
ProductName | SalesByDate | TotalSalesUntilDate
A | 5 | 113
B | 10 | 113
C | 20 | 113
D | 18 | 113
which the TotalSalesUntilDate shows the number of product sold from the first date of the month until the input date but for all product without separation for each product.
So when I tried to change the query to like this (adding GROUP BY p.ProductID before "as TotalSalesUntilDate"):
select p.ProductName, A.SalesByDate,
(select(SUM(case when (pd.Date between '01' and #Date)
then s.SalesByDate else 0 end))
from Period pd
inner join Sales s on pd.TimeID = s.TimeID
full join Product p on s.ProductID = p.ProductID
group by p.ProductID) as TotalSalesUntilDate
from Product p join
(select s.ProductID, pd.Date, s.SalesByDate
from Period pd join Sales s on pd.TimeID = s.TimeID) A on
p.ProductID = A.ProductID where #Date = A.Date
and when I execute this query, I got this error message:
"Msg 512, Level 16, State 1, Procedure SalesMTDSubQuery, Line 7
Subquery returned more than 1 value. This is not permitted when the
subquery follows =, !=, <, <= , >, >= or when the subquery is used as
an expression."
Since I'm new in SQL and still learning, but I don't understand how to solve this. Any help will be appreciated. Thank you.

In the #Date variable we are storing the date:
SELECT DISTINCT PT.[ProductName]
,SUM(IIF(PD.[Date] = #Date, SL.[SalesByDate], 0))
,SUM(IIF(PD.[Date] BETWEEN '01' AND #Date, SL.[SalesByDate], 0))
FROM #Product PT
INNER JOIN #Sales SL
ON PT.[ProductID] = SL.[ProductID]
INNER JOIN #Period PD
ON SL.[TimeID] = PD.[TimeID]
GROUP BY PT.[ProductName]
Result:
Full code:
DECLARE #Period TABLE
(
[TimeID] TINYINT
,[Date] CHAR(2)
)
INSERT INTO #Period([TimeID], [Date])
VALUES (1,'01')
,(2,'02')
,(3,'03')
,(4,'04')
,(5,'05')
,(6,'06')
,(7,'07')
,(8,'08')
,(9,'09')
,(10,'10')
,(11,'11')
,(12,'12')
,(13,'13')
,(14,'14')
,(15,'15')
DECLARE #Product TABLE
(
[ProductID] TINYINT
,[ProductName] CHAR(1)
)
INSERT INTO #Product( [ProductID], [ProductName])
VALUES (1,'A')
,(2,'B')
,(3,'C')
,(4,'D')
DECLARE #Sales TABLE
(
[TimeID] TINYINT
,[ProductID] TINYINT
,[SalesByDate] TINYINT
)
INSERT INTO #Sales ([TimeID], [ProductID], [SalesByDate])
VALUES (1, 1, 10)
,(1, 4, 20)
,(7, 2, 10)
,(7, 3, 5)
,(15, 1, 5)
,(15, 2, 10)
,(15, 3, 15)
,(15, 4, 18)
,(19, 2, 15)
,(20, 3, 2)
,(22, NULL, 2)
,(1, 4, 5)
,(7, 2, 10)
,(15, 3, 5)
DECLARE #Date CHAR(2) = '15'
SELECT DISTINCT PT.[ProductName]
,SUM(IIF(PD.[Date] = #Date, SL.[SalesByDate], 0))
,SUM(IIF(PD.[Date] BETWEEN '01' AND #Date, SL.[SalesByDate], 0))
FROM #Product PT
INNER JOIN #Sales SL
ON PT.[ProductID] = SL.[ProductID]
INNER JOIN #Period PD
ON SL.[TimeID] = PD.[TimeID]
GROUP BY PT.[ProductName]
EDIT:
If you need to use sub-query, this is how your example can works:
SELECT PT.[ProductName]
,SUM(SL.[SalesByDate])
,DataSource.[TotalSalesByDate]
FROM #Product PT
INNER JOIN #Sales SL
ON PT.[ProductID] = SL.[ProductID]
INNER JOIN #Period PD
ON SL.[TimeID] = PD.[TimeID]
INNER JOIN
(
SELECT S.[ProductID]
,SUM(S.[SalesByDate]) AS [TotalSalesByDate]
FROM #Sales S
INNER JOIN #Period P
ON S.[TimeID] = P.[TimeID]
WHERE P.[Date] BETWEEN '01' AND #Date
GROUP BY S.[ProductID]
) AS DataSource
ON PT.[ProductID] = DataSource.[ProductID]
WHERE PD.[Date] = #Date
GROUP BY PT.[ProductName]
,DataSource.[TotalSalesByDate]

First, in the Table Period you must have dates, not '01','02' so you can use BETWEEN. Or you can use 1,2,3 ... but they have to be numbers.
So, we suppose that in table Table Period you have numbers for dates (I make this remark, because you use 01, instead of 1 which assumes string value. The query itself is relatively easy:
SELECT
p.ProductName,
SUM(CASE WHEN s.TimeID = 10 THEN s.SalesByDate ELSE 0 END) as SalesByDate,
SUM(CASE WHEN s.TimeID = 10 THEN 0 ELSE s.SalesByDate END) as TotalSalesUntilDate
FROM
Product p
INNER JOIN Salse s ON p.ProductID = s.ProductID
WHERE
s.TimeID BETWEEN 1 AND 10
GROUP BY p.ProductName;
You take Sales for each date. If this is a selected date then add sales to column SalesByDate, else add then to column TotalSalesUntilDate. You group by ProductName to calculate SUM. And select only dates which are in the desired period in WHERE clause. We assume that this query is started only for a specific month (because we use only date element - i.e. 1,2,... not month).
This will show only Products with sales. If you want to see all Products list use LEFT JOIN instead of INNER JOIN.

Related

Referencing the current row outer apply column within separate outer join

Recently I've been tasked with creating a report that outputs sales information by Date of Business and Hour of the Day.
Here is the query I have currently written.
WITH CTE AS
(
SELECT 0 AS Count
UNION ALL
SELECT Count + 1
FROM CTE
WHERE Count + 1 <= 23
),
ALLDATES AS
(
SELECT CONVERT(datetime, #startDate) AS [DOB]
UNION ALL
SELECT DATEADD(DAY, 1, [DOB])
FROM AllDates
WHERE [DOB] < #endDate
)
SELECT D.DOB, A.Count AS [Hour], CONCAT(A.Count, ':00') AS [DisplayHour]
, B.OrderModeName, COALESCE(B.Sales_Total, 0) AS [Sales]
, COALESCE(B.Comps, 0) AS Comps, COALESCE(B.Promos, 0) AS Promos
FROM CTE AS A
OUTER APPLY (SELECT DOB FROM ALLDATES) D
LEFT OUTER JOIN (
SELECT DATEPART(HH, ItemDetail.TransactionTime) AS [Hour]
, OrderMode.OrderModeName, SUM(ItemDetail.GrossPrice) Sales_Total
, SUM(CompAmount) AS Comps, SUM(PromoAmount) AS Promos
FROM ItemDetail
INNER JOIN OrderMode ON OrderMode.OrderModeID = ItemDetail.OrderModeID
WHERE ItemDetail.DOB = D.DOB /*NEED HELP HERE*/ AND LocationID IN (
SELECT LocationID
FROM LocationGroupMember
WHERE LocationGroupID = '#locationGroupID'
)
GROUP BY ItemDetail.DOB, DATEPART(HH, ItemDetail.TransactionTime), OrderMode.OrderModeName
) AS B
ON A.Count = B.Hour
ORDER BY D.DOB, A.Count
Where I am struggling is being able to reference the current row's DOB column that is coming from the OUTER APPLY.
I have tried WHERE ItemDetail.DOB = D.DOB, however I receive an error that the identifier can't be bound. Am I correct that in understanding that the outer applied data is not visible to the subquery within the join?
Here is an example of the output I'm expecting:
DOB | Hour | Display Hour | OrderModeName | Sales | Comps | Promos
1/8/2020 | 17 | 17:00 | Order | 163.17 | 0 | 0 <-- Sales for Hour and Order Mode present
1/8/2020 | 23 | 23:00 | | 0 | 0 | 0 <-- No sales at all for a given hour
Thanks in advance for any direction and advice!
The basic pattern here is to CROSS JOIN to define the result "grain" and then LEFT JOIN the fact table to populate the rows for which data exists. EG
WITH CTE AS
(
SELECT 0 AS Count
UNION ALL
SELECT Count + 1
FROM CTE
WHERE Count + 1 <= 23
),
ALLDATES AS
(
SELECT CONVERT(datetime, #startDate) AS [DOB]
UNION ALL
SELECT DATEADD(DAY, 1, [DOB])
FROM AllDates
WHERE [DOB] < #endDate
),
ALLHOURS as
(
SELECT D.DOB, A.Count AS [Hour], CONCAT(A.Count, ':00') AS [DisplayHour]
FROM CTE AS A
CROSS JOIN ALLDATES D
),
ITEM_SUMMARY as
(
SELECT DOB, DATEPART(HH, ItemDetail.TransactionTime) AS [Hour], OrderMode.OrderModeName, SUM(ItemDetail.GrossPrice) Sales_Total, SUM(CompAmount) AS Comps, SUM(PromoAmount) AS Promos
FROM ItemDetail
INNER JOIN OrderMode ON OrderMode.OrderModeID = ItemDetail.OrderModeID
AND LocationID IN (SELECT LocationID FROM LocationGroupMember WHERE LocationGroupID = #locationGroupID)
where DOB >= #startDate
and DOB < #endDate
GROUP BY ItemDetail.DOB, DATEPART(HH, ItemDetail.TransactionTime), OrderMode.OrderModeName
)
select ALLHOURS.DOB,
ALLHOURS.Count AS [Hour],
CONCAT(ALLHOURS.Count, ':00') AS [DisplayHour],
ITEM_SUMMARY.OrderModeName,
COALESCE(ITEM_SUMMARY.Sales_Total, 0) AS [Sales],
COALESCE(ITEM_SUMMARY.Comps, 0) AS Comps,
COALESCE(ITEM_SUMMARY.Promos, 0) AS Promos
from ALLHOURS
LEFT OUTER JOIN ITEM_SUMMARY
on ITEM_SUMMARY.DOB = ALLHOURS.DOB
and ITEM_SUMMARY.Hour = ALLHOURS.Hour

How to group by week even when the count is 0

My below example works fine, the only challenge i am facing is that weeks with 0 results do not show.
Here is a sample of my code:
SELECT
DATENAME (WK, DATE) AS WEEK,
COUNT (DISTINCT COMPANY_ID) AS AMOUNT
FROM
(
SELECT COMPANY, DATE = MIN(DATE)
FROM TABLE1 A INNER JOIN TABLE2 B
ON A.ID = B.ID
WHERE YEAR(A.DATE) = '2019' AND COMPANY_ID NOT IN(SELECT COMPANY_ID FROM
TABLE1 A INNER JOIN TABLE2 B ON A.ID = B.ID AND DATE < '2019-01-01') GROUP
BY COMPANY_ID) d
GROUP BY dateadd(wk, datediff(wk, 0, DATE), 0), DATENAME(WK, DATE)
ORDER BY dateadd(wk, datediff(wk, 0, DATE), 0)
My current output looks like this:
week | amount
4 | 354
6 | 222
7 | 144
8 | 354
9 | 45
10 | 55
11 | 76
12 | 98
13 | 45
14 | 344
The result above is missing many weeks (1,2,3 and 15,16,17 etc.)
How do i get to show those with 0 count?
My desired output:
week | amount
1 | 0
2 | 0
3 | 0
4 | 354
6 | 222
7 | 144
8 | 354
9 | 45
10 | 55
11 | 76
12 | 98
13 | 45
14 | 344
15 | 0
16 | 0
17 | 0
Couple of things to note -
1) Your current query is not correct (Possibly, you have removed some portion of it to hide confidential stuff).
Ex. In the subquery named "d", the GROUP BY is on "company_id" column but "company" has been SELECT-ed.
SELECT DATENAME (WK, DATE) AS WEEK,
COUNT (DISTINCT COMPANY_ID) AS AMOUNT
FROM
(
SELECT COMPANY /*Different from group_by clause*/, DATE = MIN(DATE)
FROM TABLE1 A INNER JOIN TABLE2 B ON (A.ID = B.ID)
WHERE YEAR(A.DATE) = '2019'
AND COMPANY_ID NOT IN
(
SELECT COMPANY_ID
FROM TABLE1 A INNER JOIN TABLE2 B ON A.ID = B.ID AND DATE < '2019-01-01'
)
GROUP BY COMPANY_ID
) d
GROUP BY dateadd(wk, datediff(wk, 0, DATE), 0), DATENAME(WK, DATE)
ORDER BY dateadd(wk, datediff(wk, 0, DATE), 0)
2) I hope while editing you have not remove any clauses mistakenly.
3) Could you please post some input data to understand the output better.
(Apologies for posting here, as I don't have privilege to comment.)
First Create a temp table that has all weeks numbers
then join it with your query
DECLARE #Weeks AS Table(ID int)
DECLARE #i int = 1
WHILE #i < 53
BEGIN
INSERT INTO #Weeks (ID)
VALUES(#i)
SET #i = #i + 1
END
SELECT * FROM #Weeks
SELECT
DATENAME (WK, DATE) AS WEEK,
COUNT (DISTINCT COMPANY_ID) AS AMOUNT
FROM
(
SELECT COMPANY, DATE = MIN(DATE)
FROM TABLE1 A INNER JOIN TABLE2 B
ON A.ID = B.ID
RIGHT OUTER JOIN #Weeks W ON W.ID = DATENAME (WK, DATE)
WHERE YEAR(A.DATE) = '2019' AND COMPANY_ID NOT IN(SELECT COMPANY_ID FROM
TABLE1 A INNER JOIN TABLE2 B ON A.ID = B.ID AND DATE < '2019-01-01') GROUP
BY COMPANY_ID) d
GROUP BY dateadd(wk, datediff(wk, 0, DATE), 0), DATENAME(WK, DATE)
ORDER BY dateadd(wk, datediff(wk, 0, DATE), 0)

SQL Server : sum two columns

I have the following SQL statement which shows me transactions from a Point Of Sale system. I would like to calculate the total sum of (MoneyIn - MoneyOut) columns but is a bit beyond me and show this as a single value. I am sure its a simple mod to this.
DECLARE #StartDate DateTime;
DECLARE #EndDate DateTime;
DECLARE #SearchTerm NVARCHAR(200);
SET #SearchTerm = '%widget1%'
SET #StartDate = '2018-05-01 00:00:00'
SET #EndDate = DATEADD(month, 1, #StartDate)
SELECT TOP (500)
t.TransactionDate,
t.MoneyIn,
t.MoneyOut,
t.Description,
p.PaymentMethodName,
t.TransactionRef,
c.SalesItems,
COUNT(*) AS Occurrences
FROM
Transactions t
LEFT JOIN
Tills tl ON t.TillId = tl.TillId
INNER JOIN
PaymentMethods p ON t.PaymentMethodId = p.PaymentMethodsID
INNER JOIN
Membership m ON t.UserId = m.UserId
CROSS APPLY
(SELECT
STUFF((SELECT ',' + CAST(tp.Description AS VARCHAR(100))
FROM TransactionsPosLines tp
WHERE t.TransactionId = tp.TransactionId
FOR XML PATH('')), 1, 1, '') AS SalesItems) c
WHERE
t.TransactionDate >= #StartDate
AND t.TransactionDate <= #EndDate
AND (t.PaymentTypeId = 1)
AND SalesItems LIKE #SearchTerm
GROUP BY
t.TransactionDate,
t.MoneyIn,
t.MoneyOut,
t.Description,
p.PaymentMethodName,
t.TransactionRef,
m.Username,
c.SalesItems
ORDER BY
COUNT(*) DESC
Output:
2018-05-01 17:23:23.243 9.99 0.00 Sale - Card Card Shop Sale Grab n Go item 1
2018-05-08 13:15:04.577 10.00 -0.01 Sale - Cash Total: 9.99 Cash Shop Sale Grab n Go item 1
2018-05-10 14:08:47.120 7.99 0.00 Sale - Card Card Shop Sale Grab n Go item,Discount 1
and I want to show:
Total Sales: 27.97
(Sum of MoneyIn - Sum of MoneyOut)
So that this question has an answer
(edit: added the actual SQL I used to resolve this which gave the single result I was looking for), thanks for help:
Select top (10000)
sum(t.MoneyIn) - sum(t.MoneyOut) as 'moneytotal'
From
Transactions t
LEFT JOIN Tills tl
ON t.TillId = tl.TillId
INNER JOIN PaymentMethods p
ON t.PaymentMethodId = p.PaymentMethodsID
INNER JOIN Membership m
On t.UserId = m.UserId
CROSS APPLY
(SELECT STUFF(
(SELECT ',' + CAST(tp.Description AS VARCHAR(100))
FROM
TransactionsPosLines tp
WHERE t.TransactionId = tp.TransactionId
FOR XML PATH(''))
,1,1,'') as SalesItems) c
Where
t.TransactionDate >= #StartDate
AND t.TransactionDate <= #EndDate
AND (t.PaymentTypeId = 1)
and SalesItems LIKE #SearchTerm
Order By
count(*) Desc
GO

Recursive cte with values from 2 tables

I need some help with this please.
I would like to create a recursive query with values from a table for the anchor, multiplied by a coefficient from another table.
Let me be more explicit :
Tables structure and filling :
create table T
(
Site varchar(10) primary key,
Price money,
Year int
);
create table B
( Site varchar(10),
Coeff float,
Year int
);
insert into T values /* Each Site appears only once here */
('A', 125.10, 2003),
('B', 78.10, 2002),
('C', 23.34, 2001)
insert into B values /* Each (Site,Year) appears only once here */
('A', 12, 2003),
('A', 0.111, 2004),
('B', 0.322, 2002),
('B', 0.333, 2003),
('C', 0.555, 2001),
('C', 0.666, 2002)
My recursive formula is :
Price (n) = Price (n-1)* Coeff(n-1)
(where n is the year)
Here is my last attempt :
;WITH cte
AS (SELECT T.Site, T.Year, T.Price as RootPrice FROM T
UNION ALL
SELECT T.Site, T.Year, CAST(cte.RootPrice * B.Coeff AS MONEY) AS PriceYear
FROM T INNER JOIN cte ON T.Site = cte.Site AND T.Year = cte.Year INNER JOIN B ON cte.Year = B.Year AND cte.Site = B.Site)
SELECT * FROM cte
This cte is running endlessly. What am I missing ?
Edit :
Output needed :
Site | Price | Year
---------------------------
A | 1501.2 | 2003
A | 166.78 | 2004
B | 25.15 | 2002
B | 8.37 | 2003
C | 12.95 | 2001
C | 8.63 | 2002
This produces the output you want:
;WITH CTE AS
(
SELECT
Site = T.Site,
Year = T.Year,
Price = CONVERT(MONEY, T.Price * B.Coeff)
FROM
T AS T
INNER JOIN B AS B ON
T.Site = B.Site AND
T.Year = B.Year
UNION ALL
SELECT
Site = C.Site,
Year = C.Year + 1,
Price = CONVERT(MONEY, C.Price * B.Coeff)
FROM
CTE AS C
INNER JOIN B AS B ON
C.Site = B.Site AND
C.Year + 1 = B.Year
)
SELECT
*
FROM
CTE AS C
ORDER BY
C.Site,
C.Year
The problem with your solution is that the anchor doesn't start with the correct price, you should multiply the price with coeff on the anchor. Keep in mind that the anchor is the first set of the resulting CTE and it is included in it.
So your anchor:
SELECT
T.Site,
T.Year,
T.Price as RootPrice
FROM
T
Should start with the correct price for that year:
SELECT
Site = T.Site,
Year = T.Year,
Price = CONVERT(MONEY, T.Price * B.Coeff)
FROM
T AS T
INNER JOIN B AS B ON
T.Site = B.Site AND
T.Year = B.Year
And remove the T reference on the recursive set, since you don't need it anymore.
If you also want to see the root prices, you can tamper a little with the recursive join expression:
;WITH CTE AS
(
SELECT
Site = T.Site,
Year = T.Year,
Price = T.Price,
IsRoot = 1
FROM
#T AS T
UNION ALL
SELECT
Site = C.Site,
Year = CASE WHEN C.IsRoot = 1 THEN C.Year ELSE C.Year + 1 END,
Price = CONVERT(MONEY, C.Price * B.Coeff),
IsRoot = 0
FROM
CTE AS C
INNER JOIN #B AS B ON C.Site = B.Site
WHERE
(C.IsRoot = 1 AND C.Year = B.Year) OR
(C.IsRoot = 0 AND C.Year + 1 = B.Year)
)
SELECT
*
FROM
CTE AS C
ORDER BY
C.Site,
C.Year,
C.IsRoot DESC
/*
Results:
Site Year Price IsRoot
---------- ----------- --------------------- -----------
A 2003 125,10 1
A 2003 1501,20 0
A 2004 166,6332 0
B 2002 78,10 1
B 2002 25,1482 0
B 2003 8,3744 0
C 2001 23,34 1
C 2001 12,9537 0
C 2002 8,6272 0
*/

TSQL group by generate duplicate row

I'm trying to extract all prices and taxes by dates range (not necessary the same date) in 2 column and group by ID.
Because I need to group by 2 others columns because T-SQL need that:
Column '...' is invalid in the select list because
it is not contained in either an aggregate function or the GROUP BY clause.
I have a duplicate user/ID sometimes. ( don't know why by the way..)
I have this SQL:
WITH myQuery AS
(
Select
c.name, c.id,
CASE
WHEN g.dateCreated BETWEEN CAST ('2016-06-01 00:00:00.000' AS DATETIME)
AND CAST ('2017-05-31 23:59:59.000' AS DATETIME)
THEN SUM(CAST(g.price AS decimal(20,2) ))
ELSE 0
END AS TOTAL_PRICE,
CASE
WHEN g.dateCreated BETWEEN CAST ('2016-01-01 00:00:00.000' AS DATETIME)
AND CAST ('2016-12-31 23:59:59.000' AS DATETIME)
THEN SUM(CAST(g.tax AS decimal(20,2) ))
ELSE 0
END AS TOTAL_TAX
FROM customers c
inner join goodies g
ON c.id = g.customer_id
GROUP BY c.name, c.id, g.dateCreated
)
SELECT count(*) FROM myQuery
I got 5203 rows. I have only 5031 users.
When I Analyse my data, I have some duplicate data.
Example:
Alex, 12, 0.00, 0.00
Alex, 12, 100.00, 14.55
Nancy, 4, 0.00, 0.00
Arthur, 97, 48.14, 09.17
I tried to group by only by id but it seem that I can't do that.
Why I have a duplicate data and How to prevent that and ensure that I have 1 row by USER even if they don't buy goodies?
Correcting your conditional aggregation and removing dateCreated from the group by:
with myQuery as (
select
c.name
, c.id
, total_price = sum(case
when g.dateCreated >= '20160601' and g.dateCreated < '20170601'
then cast(g.price as decimal(20,2))
else 0
end)
, total_tax = sum(case
when g.dateCreated >= '20160101' and g.dateCreated < '20170101'
then cast(g.tax as decimal(20,2))
else 0
end)
from customers c
left join goodies g
on c.id = g.customer_id
group by
c.name
, c.id
--, g.dateCreated
)
select count(*) from myQuery;
Changing the inner join to a left join will return customers even if they have no corresponding row in goodies.
I also changed your date range code to be more explicit about what is included.
Reference:
Bad habits to kick : mis-handling date / range queries - Aaron Bertrand
What do between and the devil have in common? - Aaron Bertrand

Resources