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
I have a calendar type check I'm trying to do on SQL Server. For each month of the year, I need to check if the employee was hired or not. There can be an original hire date, a rehire date, a termination date, and the last termination date; other than the original hire date, which will always have a value, all of these date fields can be null.
Given the following data:
EmpID OrigHireDate TermDate LastTermDate RehireDate
42 2017-09-25 NULL 2019-03-26 2019-10-30
What I am trying to achieve is the following result for each month for last year (i.e. 2019) and having no luck in coming up with the right statement. Assume I already have a table containing each month's number along with the start/end date of the month that I can use to compare the date ranges.
EmpID Month EmployeeDuring
42 1 True
42 2 True
42 3 True
42 4 False
42 5 False
42 6 False
42 7 False
42 8 False
42 9 False
42 10 True
42 11 True
42 12 True
The following works. May need some minor adjustments to handle all possible combinations of EmpID, OrigHireDate, TermDate, LastTermDate, RehireDate
I apologize for posting mostly code. Will add more explanation and or comments tomorrow.
DECLARE #EmpID int, #OrigHireDate date, #TermDate date, #LastTermDate date, #RehireDate date
DECLARE #year int
SET #year = 2019
SET #EmpID = 42
SET #OrigHireDate = '2017-09-25'
SET #TermDate = NULL
SET #LastTermDate = '2019-03-26'
SET #RehireDate = '2019-10-30'
SET #OrigHireDate = DATEADD(day,-DAY(#OrigHireDate)+1, #OrigHireDate)
SET #LastTermDate = DATEADD(day,-DAY(ISNULL(#LastTermDate,GETDATE()))+1, #LastTermDate)
SET #RehireDate = DATEADD(day,-DAY(#RehireDate)+1, #RehireDate)
SET #TermDate = DATEADD(day,-DAY(ISNULL(#TermDate,GETDATE()))+1, #TermDate)
;WITH CTE_DATES_ORIGINAL([Date],[Level])
AS
(
SELECT #OrigHireDate AS [DATE],
1 AS [Level]
UNION ALL
SELECT
DATEADD(MONTH,1, [DATE] ) , [Level] + 1
FROM CTE_DATES_ORIGINAL
WHERE [DATE] < ISNULL(#LastTermDate,GETDATE())
),
CTE_DATES_REHIRE([Date],[Level])
AS
(
SELECT #RehireDate AS [DATE],
1 AS [Level]
UNION ALL
SELECT
DATEADD(MONTH,1, [DATE] ) , [Level] + 1
FROM CTE_DATES_REHIRE
WHERE [DATE] < ISNULL(#TermDate,GETDATE())
),
CTE_DATES_YEAR(m) AS
(
SELECT 1
UNION ALL
SELECT m+1
FROM CTE_DATES_YEAR
WHERE m < 12
)
SELECT #EmpID AS EmpID, m AS [Month], ISNULL(EmployeeDuring.EmployeeDuring,0) AS EmployeeDuring
FROM CTE_DATES_YEAR y
LEFT OUTER JOIN
(
SELECT
[Date], 1 AS EmployeeDuring
FROM
CTE_DATES_ORIGINAL
UNION
SELECT
[Date] , 1 AS EmployeeDuring
FROM
CTE_DATES_REHIRE
) employeeDuring
ON DATEADD(month,m-1, CAST(CAST(#year AS CHAR(4)) + '-1-1' AS DATE)) = employeeDuring.[Date]
ORDER BY m
OPTION (MAXRECURSION 5000)
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
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.
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;