Convert row to 4 columns in sql server - sql-server

I have a table that looks similar to below :
Maximum there can be only 4 different reason rows for an ID. I want to convert that Reason column into 4 columns and remove the other rows. If an ID doesn't have 4 reasons still split it into 4 columns and make them NULL. If an ID has same reasons repeating just show it in one column and make the other columns NULL.
The reason column need to be split into different columns based on number of distinct reasons
Expected result is as below.
Table :
ID Date Reason
100 10/27/2017 Insufficient
100 10/27/2017 Excessive
101 10/20/2017 Excessive
101 10/20/2017 Excessive
101 10/20/2017 Insufficient
101 10/20/2017 Derog
105 10/24/2017 Length
106 10/10/2017 Dismiss
107 10/10/2016 Rejected
108 10/10/2016 Dismiss
Expected Result :
ID Date Reason1 Reason2 Reason3
100 10/27/2017 Insufficient Excessive NULL
101 10/20/2017 Excessive Insufficient Derog
105 10/24/2017 Length NULL NULL
106 10/10/2017 Dismiss NULL NULL
107 10/10/2016 Rejected NULL NULL
108 10/10/2016 Dismiss NULL NULL

Here is how you would do this if you have a max of 4 columns. Note that if you have fifth column it will NOT appear in this. To handle the number of columns dynamically means we have to use dynamic sql and the complexity jumps pretty quickly.
declare #Something table
(
ID int
, MyDate date
, Reason varchar(20)
)
insert #Something values
(100, '10/27/2017', 'Insufficient')
, (100, '10/27/2017', 'Excessive')
, (101, '10/20/2017', 'Excessive')
, (101, '10/20/2017', 'Excessive')
, (101, '10/20/2017', 'Insufficient')
, (101, '10/20/2017', 'Derog')
, (105, '10/24/2017', 'Length')
, (106, '10/10/2017', 'Dismiss')
, (107, '10/10/2016', 'Rejected')
, (108, '10/10/2016', 'Dismiss')
;
select x.ID
, Result1 = MAX(case when RowNum = 1 then Reason end)
, Result2 = MAX(case when RowNum = 2 then Reason end)
, Result3 = MAX(case when RowNum = 3 then Reason end)
, Result4 = MAX(case when RowNum = 4 then Reason end)
from
(
select *
, RowNum = ROW_NUMBER() over (partition by ID order by MyDate)
from #Something
group by ID, Reason, MyDate
) x
group by x.ID

Related

T-SQL: get only rows which are after a row that meets some condition

I have a table of article's logs. I need to get all the articles which have only one log, or in case there are amount of logs more than 1: if an article has any log in status = 103, it's need to fetch only rows after this log, in other case all the logs. So from the following dataset I want to get only rows with Id 1383 and 284653.
Id
Article
Version
StatusId
AddedDate
1383
1481703
0
42
2011-11-25 09:23:42.000
284645
435545
1
41
2021-11-02 18:29:42.000
284650
435545
2
41
2021-11-02 18:34:58.000
284651
435545
2
103
2021-11-02 18:34:58.000
284653
435545
3
41
2021-11-02 18:38:33.000
Any ideas how to handle it properly ? Thanks in advance
You can use window functions here. A combination of a running COUNT and a windowed COUNT will do the trick
The benefit of using window functions rather than self-joins is that you only scan the base table once.
SELECT
Id,
Article,
Version,
StatusId,
AddedDate
FROM (
SELECT *,
HasPrev103 = COUNT(CASE WHEN StatusId = 103 THEN 1 END) OVER
(PARTITION BY Article ORDER BY AddedDate ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING),
Has103 = COUNT(CASE WHEN StatusId = 103 THEN 1 END) OVER (PARTITION BY Article),
Count = COUNT(*) OVER (PARTITION BY Article)
FROM YourTable t
) t
WHERE (Has103 > 0 AND HasPrev103 > 0) OR Count = 1;
db<>fiddle
CREATE TABLE #Article (
Id int NOT NULL PRIMARY KEY,
Article int NOT NULL,
Version int NOT NULL,
StatusId int NOT NULL,
DateAdded datetime NOT NULL
)
INSERT INTO #Article (Id, Article, Version, StatusId, DateAdded)
VALUES
(1383, 1481703, 0, 42, '2011-11-25 09:23:42.000'),
(284645, 435545, 1, 41 , '2021-11-02 18:29:42.000'),
(284650, 435545, 2, 41 , '2021-11-02 18:34:58.000'),
(284651, 435545, 2, 103, '2021-11-02 18:34:58.000'),
(284653, 435545, 3, 41 , '2021-11-02 18:38:33.000')
SELECT *
FROM #Article a
LEFT JOIN (
-- Get articles that appear only once.
SELECT Article
FROM #Article
GROUP BY Article
HAVING COUNT(*) = 1
) AS o
ON a.Article = o.Article
LEFT JOIN (
-- Get the 103s and their corresponding date.
SELECT Article, DateAdded
FROM #Article
WHERE StatusId = 103
) AS s
ON a.Article = s.Article AND s.DateAdded < a.DateAdded
WHERE o.Article IS NOT NULL OR (s.Article IS NOT NULL AND a.DateAdded > s.DateAdded)
DROP TABLE #Article

How to set rows value as column in SQL Server?

I have a table tblTags (Date, Tagindex, Value)
The values in the table are:
Date Tagindex Value
---------------------------------
2017-10-21 0 21
2017-10-21 1 212
2017-10-21 2 23
2017-10-21 0 34
2017-10-21 1 52
2017-10-21 2 65
I want the result as :
Date 0 1 2
-------------------------------
2017-10-21 21 212 23
2017-10-21 34 52 65
For this I wrote the followring query
select *
from
(SELECT a.Date, a.Tagindex,a.value
FROM tblTag a) as p
pivot
(max(value)
for Tagindex in ( [tblTag])
) as pvt
But I get these errors:
Msg 8114, Level 16, State 1, Line 10
Error converting data type nvarchar to int.
Msg 473, Level 16, State 1, Line 10
The incorrect value "tblTag" is supplied in the PIVOT operator.
How to solve this issue.
I think can use a query like this:
;with t as (
select *
, row_number() over (partition by [Date],[Tagindex] order by (select 0)) seq
from tblTag
)
select [Date],
max(case when [Tagindex] = 0 then [Value] end) '0',
max(case when [Tagindex] = 1 then [Value] end) '1',
max(case when [Tagindex] = 2 then [Value] end) '2'
from t
group by [Date], seq;
SQL Server Fiddle Demo
SQL Server Fiddle Demo - with pivot
Note: In above query I use row_number() function to create a sequence number for each Date and Tagindex, But the trick is in using (select 0) that is a temporary field to use in order by part, that will not trusted to return arbitrary order of inserted rows.So, if you need to achieve a trusted result set; you need to have an extra field like a datetime or an auto increment field.
Try this:
DECLARE #tblTag TABLE
(
[Date] DATE
,[TagIndex] TINYINT
,[Value] INT
);
INSERT INTO #tblTag ([Date], [TagIndex], [Value])
VALUES ('2017-10-21', 0, 21)
,('2017-10-21', 1, 212)
,('2017-10-21', 2, 23)
,('2017-10-22', 0, 34)
,('2017-10-22', 1, 52)
,('2017-10-22', 2, 65);
SELECT *
FROM #tblTag
PIVOT
(
MAX([value]) FOR [Tagindex] IN ([0], [1], [2])
) PVT;
You need to say exactly which are the PIVOT columns. If you are going to have different values for the TagIndex and you cannot hard-coded them, you need to use dynamic PIVOT.
Also, you need to be sure you have a way to group the tagIndex values in one row. For example, different date (as in my test data), ID column which is marking when a row is inserted or something else (group ID column or date added column).

T-SQL, Picking lowest date per row where X,X and X happen

I've got an interesting problem, I created a view in SQL to aggregate some data on Orders so if an item is out of stock or we can't fill it, we can see why and see when the next time we'll get a shipment of that part in and how much. This would ive us a decent idea of when it'll be next available for an order. Unfortunately I'm getting Orders listed multiple times with ALL future part purchase dates so the data is roughly displayed as such:
Order# Part OrderQty AvailableQty PartPurchaseDate PurchaseQty
111 B 25 0 1/1/2016 15
111 B 25 0 2/1/2016 50
112 C 500 0 1/3/2016 400
113 B 11 0 1/1/2016 15
113 B 11 0 2/1/2016 50
Ok so first I'll apologize or the confusing format. Part of this project is figuring out how to format my questions to lead to the right answer. To clarify the above data is what I'm currently getting, which is just listing all possibilities with no real logic involved. Ignore that example, it seems that made it more confusing. Lets assume 3 tables because I think that will be easier. You have OrdDetail:
Order# Part OrderQty
111 B 25
112 C 500
113 B 11
PartQuantity (Which is really lots of quantity data summing to get what we deem as "Available") The reason they're all 0 was for simplicity. In this case there are none of X part in the warehouse right now that we can use, and we won't have any until a certain date. At which point other smaller orders may be filled first if we did have an available quantity, so for this we're just trying to see where the PO Quantity is estimated to fill the entire order. Its not perfect, and often we do fill orders in the order they are received but this is just for a rough ETA, so assumptions like this are acceptable. If we did have only a partial fill from Available we'll likely have either Reserved parts or shipped a partial order so that quantity would be Unavailable or the OrderQty would have been updated:
Part AvailableQTY
B 0
C 0
And then PurchaseOrder:
Part PartPurchaseDate PurchaseQty
B 1/1/2016 15
C 1/3/2016 400
B 2/1/2016 50
PartPurchaseDate being the date I expect those parts to be received, so our Available would go from 0 to 15 of Part B on 1/1/2016. Now since that doesn't fill the order #111 completely then it wouldn't receive an ETA, but #113 could be filled and the ETA for that would be 1/1/2016. Below is the rough output I'd like to receive after I join OrdDetail to PartQuantity and then grab the nearest date off the PO table that an entire order can be filled. Order #112 doesn't show up, which is fine, we just don't have an ETA for that Order (at which point the sales rep calls purchasing to complain and it stops being my problem).
Order# Part OrderQty AvailableQty PartPurchaseDate PurchaseQty
111 B 25 0 2/1/2016 50
113 B 11 0 1/1/2016 15
I understand this isn't complete with all data one might want to grab, and you guys may have questions as to why do it this way. This isn't something I'm copy/pasting. Just trying to figure out how something might work, so I'm giving you guys 10,000 ft information for a 10,000 ft answer. I apologize for the delay in getting back, holidays are my excuse.
This is what I got seems to work, found the minimum part purchase date for each order put that value with the order it belonged to into a # table then joined it to the maintable to get only the information you needed.
CREATE TABLE #tmp (ordernum varchar(5), part varchar(2), orderqty int, availableqty int, partpurchasedate date, purchaseqty int)
INSERT INTO #tmp SELECT '113', 'B', 11, 0, '2016/2/1', 50 -- edited this line to add the other values
--- This is the part the partains to your question
SELECT OrderNum, MIN(partpurchasedate) minppd -- find min partpurchasedate for each order
into #tmp2 -- put it into a #table
from #tmp
where purchaseqty > orderqty -- make sure the purchase qty is > or orderqty
group by ordernum
SELECT a.*
FROM #tmp a
JOIN #tmp2 b on a.ordernum = b.ordernum -- join the tables together on their order number
where a.partpurchasedate = minppd -- only show orders whose date match the ppd we found earlier
Results are what you specified you wanted I hope this helps.
This code piece seems to meet your requirments. It uses ROW_NUMBER() to create a ranking for based on the service date for each Order that a greater PurchaseQty then Order Qty and then displays the earliest row associated with that rank.
SELECT
c.[ORDER]
, c.Part
, c.OrderQty
, c.AvailableQty
, c.PartPurchaseDate
, c.PurchaseQty
FROM
(
SELECT
b.[ORDER]
, b.Part
, b.OrderQty
, b.AvailableQty
, b.PartPurchaseDate
, b.PurchaseQty
, Order_Rank = ROW_NUMBER()OVER(PARTITION BY b.[ORDER] ORDER BY b.PartPurchaseDate)
FROM
#ORDER b
WHERE
b.PurchaseQty > b.OrderQTY -- Only concerned when Purchase quantity greater than order quantity
) c
WHERE
c.Order_Rank = 1 -- Displays the earliest ranking
ORDER Part OrderQty AvailableQty PartPurchaseDate PurchaseQty
111 B 25 0 2016-02-01 50
113 B 11 0 2016-01-01 15
I'd solve this using an analytical function.
select
orders.OrderId
, orders.Part
, orders.OrderQty
, orders.AvailableQty
, case
when orders.PurchaseQty > orders.OrderQty then
min(orders.PartPurchaseDate) over (partition by orders.OrderId)
else orders.PartPurchaseDate
end as PartPurchaseDate
, orders.PurchaseQty
from #Orders orders
Results
OrderId Part OrderQty AvailableQty PartPurchaseDate PurchaseQty
111 B 25 0 2016-01-01 15
111 B 25 0 2016-01-01 50
112 C 500 0 2016-01-03 400
113 B 11 0 2016-01-01 15
113 B 11 0 2016-01-01 50
background code
create table #Orders
(
OrderId int
, Part varchar(1)
, OrderQty int
, AvailableQty int
, PartPurchaseDate date
, PurchaseQty int
)
insert into #Orders (
OrderId
, Part
, OrderQty
, AvailableQty
, PartPurchaseDate
, PurchaseQty
)
values
(111,'B' , 25, 0 , '1/1/2016',15 )
, (111,'B' , 25, 0 , '2/1/2016',50 )
, (112,'C' , 500 , 0 , '1/3/2016',400)
, (113,'B' , 11, 0 , '1/1/2016',15 )
, (113,'B' , 11, 0 , '2/1/2016',50 )
select
orders.OrderId
, orders.Part
, orders.OrderQty
, orders.AvailableQty
, orders.min(orders.PartPurchaseDate) over (partition by orders.OrderId) as PartPurchaseDate
, orders.PurchaseQty
from #Orders orders
P.S., Why is fulfillment data on the customer orders table?
I am going to assume what the real tables are
That view is not a good starting point
This just fills the order in order# but on a visual you can flop stuff around
with cte CumOrder as
( select O1.Part, O1.order#, O1.OrderQty, Sum(O2.OrderQty) as Cum
from Order O1
join Order O2
on O2.Part = O1.Part
and O2.Order# >= O2.Order#
group by O1.Part, O1.order#, O1.OrderQty
)
, CumSuplier as
( select S1.Part, S1.Odate, S1.OrderQty, Sum(S2.OrderQty) as Cum
from Supplier S1
join Supplier S2
on S2.Part = S1.Part
and S2.Odate >= S1.Odate
group by S1.Part, S1.Odate, S1.OrderQty
)
select * from
( select CumOrder.Part, CumOrder.order#, CumOrder.OrderQty as [CumOrder.OrderQty], CumOrder.Cum as [CumOrder.Cum]
, CumSuplier.Odate as [CumSuplier.Odate]
, CumSuplier.Cum as [CumSuplier.Cum]
, (CumSuplier.Cum - CumOrder.Cum) as Net
, rowNumber over (partition by CumOrder.Part, CumOrder.order# order by CumOrder.Odate) as rn
from CumOrder
join CumSuplier
on CumSuplier.Part = CumOrder.Part
and CumSuplier.Cum >= CumOrder.Cum
) ttt
where rn = 1
order by O1.Part, O1.order#

SQL How to create output with sub totals

I'm new to T-SQL and need help converting an excel report to a run on SQL. I have a SQL table that records all the daily inventory transactions (in/out) from each stockroom. I need to create a report that list the current inventory levels for each product in each location and the qty in each place as follows. In other words, the current inventory levels of each place.
I also need help on how to insert the Preferred Out Report (below) into SQL Server as a view so I can run this each month over and over again.
Thanks in Advance!
Inventory Log table:
PubID QTY LocationID Transaction
1 10 1 Add
1 20 2 Add
1 30 3 Add
1 5 1 Sold
1 10 2 Sold
1 5 3 Sold
2 10 1 Add
2 10 2 Add
2 5 2 Sold
2 8 2 Sold
1 20 1 Add
1 20 2 Add
2 2 2 Sold
Preferred Output Table:
PubID Local_1 Local_2 Local_3 Total
1 25 30 25 80
2 5 0 0 5
Total 30 30 25 85
I see a lot of close examples here but most just add the value while I need to subtract the Sold inventory from the Added stock to get my totals in each column.
The row totals and column totals on the right and bottom are pluses but not needed if it's easier without.
THANKS!
If this was about aggregation without pivoting, you could use a CASE expression, like this:
SELECT
...
Local_1 = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END),
...
FROM ...
GROUP BY ...
However, in the PIVOT clause, the argument of the aggregate function must be just a column reference, not an expression. You can work around that by transforming the original dataset so that QTY is either positive or negative, depending on Transaction:
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID
FROM dbo.InventoryLog
The above query will give you a result set like this:
PubID QTY LocationID
----- --- ----------
1 10 1
1 20 2
1 30 3
1 -5 1
1 -10 2
1 -5 3
2 10 1
2 10 2
2 -5 2
2 -8 2
1 20 1
1 20 2
2 -2 2
which is now easy to pivot:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID
FROM dbo.InventoryLog
)
SELECT
PubID,
Local_1 = [1],
Local_2 = [2],
Local_3 = [3]
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
;
Note that you could actually prepare the names Local_1, Local_2, Local_3 beforehand and avoid renaming them in the main SELECT. Assuming they are formed by appending the LocationID value to the string Local_, here's an example of what I mean:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
Name = 'Local_' + CAST(LocationID AS varchar(10))
FROM dbo.InventoryLog
)
SELECT
PubID,
Local_1,
Local_2,
Local_3
FROM prepared
PIVOT
(
SUM(QTY)
FOR Name IN (Local_1, Local_2, Local_3)
) AS p
;
You will see, however, that in this solution renaming will be needed at some point anyway, so I'll use the previous version in my further explanation.
Now, adding the totals to the pivot results as in your desired output may seem a little tricky. Obviously, the column could be calculated simply as the sum of all the Local_* columns, which might actually not be too bad with a small number of locations:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID
FROM dbo.InventoryLog
)
SELECT
PubID,
Local_1 = [1],
Local_2 = [2],
Local_3 = [3]
Total = COALESCE([1], 0)
+ COALESCE([2], 0)
+ COALESCE([3], 0)
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
;
(COALESCE is needed because some results may be NULL.)
But there's an alternative to that, where you don't have to list all the locations explicitly one extra time. You could return the totals per PubID alongside the details in the prepared dataset using SUM() OVER (...), like this:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID,
Total = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END)
OVER (PARTITION BY PubID)
FROM dbo.InventoryLog
)
…
or like this, if you wish to avoid repetition of the CASE expression:
WITH prepared AS (
SELECT
t.PubID,
QTY = x.AdjustedQTY,
t.LocationID,
Total = SUM(x.AdjustedQTY) OVER (PARTITION BY t.PubID)
FROM dbo.InventoryLog AS t
CROSS APPLY (
SELECT CASE t.[Transaction] WHEN 'Add' THEN t.QTY ELSE -t.QTY END
) AS x (AdjustedQTY)
)
…
Then you would just include the Total column into the main SELECT clause along with the pivoted results and PubID:
…
SELECT
PubID,
Local_1,
Local_2,
Local_3,
Total
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
;
That would be the total column for you. As for the row, it is actually easy to add it when you are acquainted with the ROLLUP() grouping function:
…
SELECT
PubID,
Local_1 = SUM([1]),
Local_2 = SUM([2]),
Local_3 = SUM([3]),
Total = SUM(Total)
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
GROUP BY ROLLUP(PubID)
;
The total row will have NULL in the PubID column, so you'll again need COALESCE to put the word Total instead (only if you want to return it in SQL; alternatively you could substitute it in the calling application):
…
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total'),
…
And that would be all. To sum it up, here is a complete query:
WITH prepared AS (
SELECT
PubID,
QTY = x.AdjustedQTY,
t.LocationID,
Total = SUM(x.AdjustedQTY) OVER (PARTITION BY t.PubID)
FROM dbo.InventoryLog AS t
CROSS APPLY (
SELECT CASE t.[Transaction] WHEN 'Add' THEN t.QTY ELSE -t.QTY END
) AS x (AdjustedQTY)
)
SELECT
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total'),
Local_1 = SUM([1]),
Local_2 = SUM([2]),
Local_3 = SUM([3]),
Total = SUM(Total)
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
GROUP BY ROLLUP(PubID)
;
As a final touch to it, you may want to apply COALESCE to the SUMs as well, to avoid returning NULLs in your data (if that is necessary).
The query below does what you need. I might have had one extra group by that could be combined into 1 but you get the idea.
DECLARE #InventoryLog TABLE
(
PubId INT,
Qty INT,
LocationId INT,
[Transaction] Varchar(4)
)
DECLARE #LocationTable TABLE
(
Id INT,
Name VarChar(10)
)
INSERT INTO #LocationTable
VALUES
(1, 'LOC_1'),
(2, 'LOC_2'),
(3, 'LOC_3')
INSERT INTO #InventoryLog
VALUES
(1 , 10, 1 , 'Add'),
(1 , 20, 2 , 'Add'),
(1 , 30, 3 , 'Add'),
(1 , 5 , 1 , 'Sold'),
(1 , 10, 2 , 'Sold'),
(1 , 5 , 3 , 'Sold'),
(2 , 10, 1 , 'Add'),
(2 , 10, 2 , 'Add'),
(2 , 5 , 2 , 'Sold'),
(2 , 8 , 2 , 'Sold'),
(1 , 20, 1 , 'Add'),
(1 , 20, 2 , 'Add'),
(2 , 2 , 2 , 'Sold')
SELECT PubId,
lT.Name LocationName,
CASE
WHEN [Transaction] ='Add' Then Qty
WHEN [Transaction] ='Sold' Then -Qty
END as Quantity
INTO #TempInventoryTable
FROM #InventoryLog iL
INNER JOIN #LocationTable lT on iL.LocationId = lT.Id
SELECT * INTO #AlmostThere
FROM
(
SELECT PubId,
ISNULL(LOC_1,0) LOC_1,
ISNULL(LOC_2,0) LOC_2,
ISNULL(LOC_3,0) LOC_3,
SUM(ISNULL(LOC_1,0) + ISNULL(LOC_2,0) + ISNULL(LOC_3,0)) AS TOTAL
FROM #TempInventoryTable s
PIVOT
(
SUM(Quantity)
FOR LocationName in (LOC_1,LOC_2,LOC_3)
) as b
GROUP BY PubId, LOC_1, LOC_2, LOC_3
) b
SELECT CAST(PubId as VARCHAR(10))PubId,
LOC_1,
LOC_2,
LOC_3,
TOTAL
FROM #AlmostThere
UNION
SELECT ISNULL(CAST(PubId AS VARCHAR(10)),'TOTAL') PubId,
[LOC_1]= SUM(LOC_1),
[LOC_2]= SUM(LOC_2),
[LOC_3]= SUM(LOC_3),
[TOTAL]= SUM(TOTAL)
FROM #AlmostThere
GROUP BY ROLLUP(PubId)
DROP TABLE #TempInventoryTable
DROP TABLE #AlmostThere
PubId LOC_1 LOC_2 LOC_3 TOTAL
1 25 30 25 80
2 10 -5 0 5
TOTAL 35 25 25 85
Sql Fiddle
Here is another approach: aggregate the data before pivoting, then pivot the aggregated results.
Compared to my other suggestion, this method is much simpler syntactically, which may also make it easier to understand and maintain.
All the aggregation is done with the help of the CUBE() grouping function. The basic query would be this:
SELECT
PubID,
LocationID,
QTY = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END)
FROM dbo.InventoryLog
GROUP BY CUBE(PubID, LocationID)
You can see the same CASE expression as in my other answer, only this time it can be directly used as the argument of SUM.
Using aggregation by CUBE gives us not only the totals by (PubID, LocationID), but also by PubID and LocationID separately, as well as the grand total. This is the result of the query for the example in your question:
PubID LocationID QTY
----- ---------- ---
1 1 35
2 1 10
NULL 1 45
1 2 50
2 2 25
NULL 2 75
1 3 35
NULL 3 35
NULL NULL 155
1 NULL 120
2 NULL 35
Rows with NULLs in LocationID are row totals in the final result set, and those with NULLs in PubID are column totals. The row with NULLs in both columns is the grand total.
Before we can proceed with the pivoting, we need to prepare column names for the pivoted results. If the names are supposed to be derived from the values of LocationID, the following declaration will replace LocationID in the original query's SELECT clause:
Location = COALESCE('Local_' + CAST(LocationID AS varchar(10)), 'Total')
We can also substitute 'Total' for the NULLs in PubID at this same stage, so this will replace PubID in the SELECT clause:
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total')
Now the results will look like this:
PubID LocationID QTY
----- ---------- ---
1 Local_1 35
2 Local_1 10
Total Local_1 45
1 Local_2 50
2 Local_2 25
Total Local_2 75
1 Local_3 35
Total Local_3 35
Total Total 155
1 Total 120
2 Total 35
and at this point everything is ready to apply PIVOT. This query transforms the above result set according to the desired format:
WITH aggregated AS (
SELECT
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total'),
Location = COALESCE('Local_' + CAST(LocationID AS varchar(10)), 'Total'),
QTY = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END)
FROM dbo.InventoryLog
GROUP BY CUBE(PubID, LocationID)
)
SELECT
PubID,
Local_1,
Local_2,
Local_3,
Total
FROM aggregated
PIVOT (
MAX(QTY)
FOR Location IN (Local_1, Local_2, Local_3, Total)
) AS p
;
This query will return NULLs for missing combinations of (PubID, LocationID). If you want to return 0 instead, apply COALESCE to the result of SUM in the definition of aggregated.

How SQL Server iterates over rows when computing several aggregate functions in one select query

I have a SQL Server 2012
And query
SELECT ManagerId,
SUM(CASE WHEN SoldInDay < 30 THEN 1 ELSE 0 END) as badSoldDays,
SUM(CASE WHEN Category = 'PC' THEN 1 ELSE 0 END) as DaysWithSoldPc
FROM SomeTable
GROUP BY ProductId
Some Table Definition
ManagerId | SoldInDay | Category
1 50 PC
1 20 Laptop
2 30 PC
3 40 Laptop
So, question is:
Does it mean that Sql will iterate over all rows twice? so, each aggregate function executes in separate cycle over all rows in table? or it's much smarter?
Doesn't matter what I want to get by this query, it's my dream.
(It appears that your question was addressed by a comment, but for completeness, I've provided an official answer here.)
First, your example SQL will not run. You are including ManagerId in the field list but not the GROUP BY. You will get an error akin to this:
Msg 8120, Level 16, State 1, Line 9 Column '#SomeTable.ManagerID' is
invalid in the select list because it is not contained in either an
aggregate function or the GROUP BY clause.
Assuming you meant "ManagerId" instead of "ProductId" in the field list, I reproduced your situation and reviewed the execution plans. It showed only one "Stream Aggregate" operator. You can force it to run over the table twice by separating the aggregations into two different common table expressions (CTEs) and JOINing them back together. In that case, you see two Stream Aggregate operators (one for each run through the table).
Here is the code to generate the execution plans:
DECLARE #SomeTable TABLE
(
ManagerId int,
SoldInDay int,
Category varchar(50)
);
INSERT INTO #SomeTable (ManagerId, SoldInDay, Category) VALUES (1, 50, 'PC');
INSERT INTO #SomeTable (ManagerId, SoldInDay, Category) VALUES (1, 20, 'Laptop');
INSERT INTO #SomeTable (ManagerId, SoldInDay, Category) VALUES (2, 30, 'PC');
INSERT INTO #SomeTable (ManagerId, SoldInDay, Category) VALUES (3, 40, 'Laptop');
/*
This produces an error:
Msg 8120, Level 16, State 1, Line 9
Column '#SomeTable.ManagerID' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
SELECT
ManagerId,
SUM(CASE WHEN SoldInDay < 30 THEN 1 ELSE 0 END) as badSoldDays,
SUM(CASE WHEN Category = 'PC' THEN 1 ELSE 0 END) as DaysWithSoldPc
FROM #SomeTable
GROUP BY ProductId;
*/
SELECT
ManagerId,
SUM(CASE WHEN SoldInDay < 30 THEN 1 ELSE 0 END) as BadSoldDays,
SUM(CASE WHEN Category = 'PC' THEN 1 ELSE 0 END) as DaysWithSoldPc
FROM #SomeTable
GROUP BY ManagerId;
WITH DaysWithSoldPcTable AS
(
SELECT
ManagerId,
SUM(CASE WHEN Category = 'PC' THEN 1 ELSE 0 END) as DaysWithSoldPc
FROM #SomeTable
GROUP BY ManagerId
), BadSoldDaysTable AS
(
SELECT
ManagerId,
SUM(CASE WHEN SoldInDay < 30 THEN 1 ELSE 0 END) as BadSoldDays
FROM #SomeTable
GROUP BY ManagerId
)
SELECT
DaysWithSoldPcTable.ManagerId,
DaysWithSoldPcTable.DaysWithSoldPc,
BadSoldDaysTable.BadSoldDays
FROM DaysWithSoldPcTable
JOIN BadSoldDaysTable
ON DaysWithSoldPcTable.ManagerId = BadSoldDaysTable.ManagerId;

Resources