SQL Server - Pivot without aggregation - sql-server

I have the following two tables
dimDate dd
+----------------------------------------------------------------------+
| dimDate dimYear dimMonth dimQuarter dimWeekDayNumber |
+----------------------------------------------------------------------+
| 2019-12-01 2019 12 4 5 |
| . . . . . |
| . . . . . |
| . . . . . |
| 2021-12-10 2021 12 4 5 |
| 2021-12-11 2021 12 4 6 |
| 2021-12-12 2021 12 4 7 |
| 2021-12-13 2021 12 4 1 |
+----------------------------------------------------------------------+
Goal Table pg
+-------------------------------------------------+
| location goal startdate enddate ptype |
+-------------------------------------------------+
| A 600000 2019-01-01 2019-12-31 CONN |
| B 400000 2019-01-01 2020-10-01 CONN |
| C 600000 2019-01-01 NULL CONN |
| D 450000 2019-01-01 NULL CONN |
| A 500000 2020-01-01 NULL CONN |
| B 500000 2020-10-02 2021-03-15 CONN |
| B 600000 2021-03-16 NULL CONN |
+-------------------------------------------------+
Desired output:
+-----------------------------------------------------------------------------------------------------------+
| dimDate dimYear dimMonth dimQuarter dimWeekDayNumber A B C D |
+-----------------------------------------------------------------------------------------------------------+
| 2019-12-01 2019 12 4 7 600000 400000 600000 450000 |
| . . . . . . . . . |
| . . . . . . . . . |
| . . . . . . . . . |
| 2021-12-10 2021 12 4 5 500000 600000 600000 450000 |
| 2021-12-11 2021 12 4 6 500000 600000 600000 450000 |
| 2021-12-12 2021 12 4 7 500000 600000 600000 450000 |
| 2021-12-13 2021 12 4 1 500000 600000 600000 450000 |
+-----------------------------------------------------------------------------------------------------------+
I've been trying the SQL pivot operator, but it wants to aggregate, and I've tried using cases but it pulls back four of the same date since I have four locations, with only one location with the goal per row. Any help would be appreciated.

This is the best I've come up with so far. I don't like it because it isn't dynamic. I must type in each location explicitly. Suggestions always welcome.
SELECT dd.*,
(SELECT goal from mdp.ProductionGoals where dd.dimDate between startdate and isnull(enddate, getdate()) and [location] = 'A') AS 'A' ,
(SELECT goal from mdp.ProductionGoals where dd.dimDate between startdate and isnull(enddate, getdate()) and [location] = 'B') AS 'B' ,
(SELECT goal from mdp.ProductionGoals where dd.dimDate between startdate and isnull(enddate, getdate()) and [location] = 'C') AS 'C' ,
(SELECT goal from mdp.ProductionGoals where dd.dimDate between startdate and isnull(enddate, getdate()) and [location] = 'D') AS 'D'
FROM mdp.dimDate dd
left outer join mdp.ProductionGoals pg
on dd.dimdate between startdate and isnull(enddate, getdate())
where dd.dimDate between '2019-01-01' and getdate()
+------------------------------------------------------------------------------------------------+
|dimDate dimYear dimMonth dimQuarter dimWeekDayNumber A B C D |
+------------------------------------------------------------------------------------------------+
|2019-12-01 2019 12 4 7 600000 400000 600000 450000 |
|. . . . . . . . . |
|. . . . . . . . . |
|. . . . . . . . . |
|2021-12-10 2021 12 4 5 500000 600000 600000 450000 |
|2021-12-11 2021 12 4 6 500000 600000 600000 450000 |
|2021-12-12 2021 12 4 7 500000 600000 600000 450000 |
|2021-12-13 2021 12 4 1 500000 600000 600000 450000 |
+------------------------------------------------------------------------------------------------+

You don't need subqueries, you can do this with conditional aggregation. PIVOT is a type of conditional aggregation, but you can also use SUM or MAX with a CASE expression, which is far more flexible
SELECT
dd.dimDate,
dd.dimYear,
dd.dimMonth,
dd.dimQuarter,
dd.dimWeekDayNumber,
SUM(CASE WHEN pg.location = 'A' THEN pg.goal END) AS [A],
SUM(CASE WHEN pg.location = 'B' THEN pg.goal END) AS [B],
SUM(CASE WHEN pg.location = 'C' THEN pg.goal END) AS [C],
SUM(CASE WHEN pg.location = 'D' THEN pg.goal END) AS [D]
FROM mdp.dimDate dd
left outer join mdp.ProductionGoals pg
on dd.dimdate between pg.startdate and isnull(pg.enddate, getdate())
where dd.dimDate between '20190101' and getdate()
GROUP BY
dd.dimDate,
dd.dimYear,
dd.dimMonth,
dd.dimQuarter,
dd.dimWeekDayNumber;
If you want a dynamic solution, I would recommend not going down the full dynamic SQL route. Instead, you can select the top four results using ROW_NUMBER, DENSE_RANK, or other windowing function. I don't know what criteria you would use, but for example the top four locations by total goal:
SELECT
dd.dimDate,
dd.dimYear,
dd.dimMonth,
dd.dimQuarter,
dd.dimWeekDayNumber,
SUM(CASE WHEN pg.rn = 1 THEN pg.goal END) AS [1],
SUM(CASE WHEN pg.rn = 2 THEN pg.goal END) AS [2],
SUM(CASE WHEN pg.rn = 3 THEN pg.goal END) AS [3],
SUM(CASE WHEN pg.rn = 4 THEN pg.goal END) AS [4]
FROM mdp.dimDate dd
left outer join (
SELECT *,
rn = DENSE_RANK() OVER (ORDER BY TotalGoal)
FROM (
SELECT *,
SUM(goal) OVER (PARTITION BY pg.location) AS TotalGoal
FROM mdp.ProductionGoals pg
) pg
WHERE rn <= 4
) pg on dd.dimdate between pg.startdate and isnull(pg.enddate, getdate())
where dd.dimDate between '20190101' and getdate()
GROUP BY
dd.dimDate,
dd.dimYear,
dd.dimMonth,
dd.dimQuarter,
dd.dimWeekDayNumber;

Related

SQL Query with Average and Grouping

I just want to ask you guys, especially those with MsSQL knowledge, regarding my query.
My goal is to get the average delivery time and group my data by delivery date and route id daily/weekly/monthly.
Here's my query:
SELECT
RouteID,
CONVERT(date, [DeliveryDate]) AS delivery_date,
AVG(
DATEDIFF(
day,
CONVERT(date, [UnloadDate]),
CONVERT(date, [DeliveryDate])
)
) as Averate_Delivery_Time
FROM [CARGODB].[dbo].[Cargo_Transactions]
WHERE
[DeliveryDate] IS NOT NULL AND
[UnloadDate] != 0 AND
[StageID] = 'D' AND
( CONVERT(date, [DeliveryDate]) LIKE '%2016%' or
CONVERT(date, [DeliveryDate]) LIKE '%2017%')
GROUP BY CONVERT(date, [DeliveryDate]), [RouteID]
ORDER BY CONVERT(date, [DeliveryDate]) DESC
I am not confident if the average delivery time is correct so if you think it's wrong or there are other things in my query that needs to be corrected, please let me know.
UPDATE:
I was able to get the right query:
SELECT [RouteID],
CAST(DATEPART(YEAR,[DeliveryDate]) as varchar) + ' Week ' +
CAST(DATEPART(WEEK,[DeliveryDate]) AS varchar) AS week_name,
AVG(DATEDIFF(day, CONVERT(date, [UnloadDate]), CONVERT(date,
[DeliveryDate]))) as Average_Delivery_Days
FROM [CARGODB].[dbo].[Cargo_Transactions]
WHERE [DeliveryDate] IS NOT NULL AND [DeliveryDate] != 0
AND CONVERT(date, [DeliveryDate]) BETWEEN '2016-01-01' AND GETDATE()
AND [UnloadDate] IS NOT NULL AND [UnloadDate] != 0 AND [DeliveryDate] >
[UnloadDate]
AND [Deleted] = 0 and [StageID] = 'D'
GROUP BY DATEPART(YEAR,[DeliveryDate]), DATEPART(WEEK,[DeliveryDate]),
[RouteID]
ORDER BY DATEPART(YEAR,[DeliveryDate]), DATEPART(WEEK,[DeliveryDate]),
Average_Delivery_Days desc
But I have a more complicated query to do now. I have this sample data:
RouteID | week_name | yearnum | weeknum | Average_Delivery_Days
=======================================================================
MK | 2016 Week 2 | 2016 | 2 | 1
-----------------------------------------------------------------------
TSM | 2016 Week 2 | 2016 | 2 | 1
-----------------------------------------------------------------------
E | 2016 Week 2 | 2016 | 2 | 1
-----------------------------------------------------------------------
A | 2016 Week 2 | 2016 | 2 | 1
-----------------------------------------------------------------------
D | 2016 Week 2 | 2016 | 2 | 1
-----------------------------------------------------------------------
MP | 2016 Week 2 | 2016 | 2 | 1
-----------------------------------------------------------------------
CTN | 2016 Week 3 | 2016 | 3 | 9
-----------------------------------------------------------------------
BIS | 2016 Week 3 | 2016 | 3 | 8
-----------------------------------------------------------------------
C | 2016 Week 3 | 2016 | 3 | 1
-----------------------------------------------------------------------
PN | 2016 Week 4 | 2016 | 4 |10
-----------------------------------------------------------------------
How can I make the above data be like:
MK and TSM are merged into 1 new routeID like Manila1
E, A, and D are merged into another as Manila2
MP, CTN, AND BIS as Visayas
C and PN as Mindanao
and so on..
And the average delivery days will be changed as well.
Your help is highly appreciated. Thank you!

Using a condition for cross join on months

Table 1 has column date , value
Table 2 has monthnumber , monthname (ie 1-12 for number and Jan - Dec)
Sample Data
|Table 1|
|ColDate | value|
|1-nov-2016 | 6|
Expected Output
ColDate | value | month | monthnumber
1-nov-2016 | 6 | Nov | 11
1-nov-2016 | 0 | Dec |12
..... 0 for all other months except Nov
I used cross join between table 1 and table 2 but it gives output as
1-nov-2016 | 6 | Nov | 11
1-nov-2016 | 6 | Dec |12
..... 6 for all other months though should be 0 except Nov.
How do i do that ?
Try this,
DECLARE #TB1 TABLE (COLDATE VARCHAR(20),VALUE INT)
INSERT INTO #TB1
SELECT '1-NOV-2016',6
DECLARE #TB2 TABLE(MONTH VARCHAR(10),MONTHNUMBER INT)
INSERT INTO #TB2
SELECT 'NOV',11
UNION ALL
SELECT 'DEC',12
SELECT COLDATE
,CASE WHEN MONTHNUMBER=11 THEN VALUE ELSE 0 END VALUE
,MONTH
,MONTHNUMBER
FROM #TB1
CROSS JOIN #TB2
Result:

SQL Server GROUP BY multiple columns

Let's assume I have in SQL Server the following table with only seven days available (SUN - SAT):
Orders
| Day | ProductType | Price |
| SUN | 1 | 10 |
| MON | 1 | 15 |
| MON | 2 | 20 |
| MON | 3 | 10 |
| TUE | 1 | 5 |
| TUE | 3 | 5 |
...
I need to group the data in a way so that to see the Total sum of Prices by each distinct Day and two groups of ProductType (= 1 and > 1):
| Day | FirstProductTypeTotal | RestProductsTypesTotal | GrandTotal |
| SUN | 10 | 0 | 10 |
| MON | 15 | 30 | 45 |
| TUE | 5 | 5 | 10 |
...
where FirstProductTypeTotal is ProductType = 1 and RestProductTypesTotal is ProductType > 1.
Is it possible to select this in one select instead of writing two different selects:
Select Day, SUM(Price) as FirstTotal from Orders where ProductType = 1 group by Day
and
Select Day, SUM(Price) as SecondTotal from Orders where ProductType > 1 group by Day
And then add FirstTotal and SecondTotal manually in the code to get the Grand total for each day of the week?
Use CASE Expression
Select Day, SUM(CASE WHEN ProductType = 1 THE Price ELSE 0 END) AS FirstTotal,
SUM(CASE WHEN ProductType > 1 THE Price ELSE 0 END) AS SecondTotal,
SUM(Price) AS GrandTotal
FROM Orders
group by Day
Try conditional aggregation;
Sample data;
CREATE TABLE #Orders ([Day] varchar(10), ProductType int, Price int)
INSERT INTO #Orders ([Day],ProductType, Price)
VALUES
('SUN',1,10)
,('MON',1,15)
,('MON',2,20)
,('MON',3,10)
,('TUE',1,5)
,('TUE',3,5)
Query;
SELECT
o.[Day]
,SUM(CASE WHEN o.ProductType = 1 THEN o.Price ELSE 0 END) FirstTotal
,SUM(CASE WHEN o.ProductType > 1 THEN o.Price ELSE 0 END) SecondTotal
,SUM(o.Price) GrandTotal
FROM #Orders o
GROUP BY o.[Day]
Result
Day FirstTotal SecondTotal GrandTotal
MON 15 30 45
SUN 10 0 10
TUE 5 5 10
You'd just need to sort out the ordering of the days because SQL Server by definition doesn't store the data in any particular order.

Unpivotting multiple columns - substring of column name as a new column with CROSS APPLY

I have a table with the following format
YEAR, MONTH, ITEM, REQ_QTY1, REQ_QTY2 , ....REQ_QTY31 ,CONVERTED1, CONVERTED2 ....CONVERTED31
Where the suffix of each column is the day of the month.
I need to convert it to the following format, where Day_of_month is the numeric suffix of each column
YEAR, MONTH, DAY_OF_MONTH, ITEM, REQ_QTY, CONVERTED
I thought of using CROSS APPLY to retrieve the data, but I can't use CROSS APPLY to get the "Day of Month"
SELECT A.YEAR, A.MONTH, A.ITEM, B.REQ_QTY, B.CONVERTED
FROM TEST A
CROSS APPLY
(VALUES
(REQ_QTY1, CONVERTED1),
(REQ_QTY2, CONVERTED2),
(REQ_QTY3, CONVERTED3),
......
(REQ_QTY31, CONVERTED31)
)B (REQ_QTY, CONVERTED)
The only way I found is to use a nested select with inner join
SELECT A.YEAR, A.MONTH, A.DAY_OF_MONTH, A.ITEM,A.REQ_QTY, D.CONVERTED FROM
(SELECT YEAR, MONTH, ITEM, SUBSTRING(DAY_OF_MONTH,8,2) AS DAY_OF_MONTH, REQ_QTY FROM TEST
UNPIVOT
(REQ_QTY FOR DAY_OF_MONTH IN ([REQ_QTY1],[REQ_QTY2],[REQ_QTY3],......[REQ_QTY30],[REQ_QTY31])
) B
) A
INNER JOIN (SELECT YEAR, MONTH, ITEM, SUBSTRING(DAY_OF_MONTH,10,2) AS DAY_OF_MONTH, CONVERTED FROM TEST
UNPIVOT
(CONVERTED FOR DAY_OF_MONTH IN ([CONVERTED1],[CONVERTED2],[CONVERTED3],....[CONVERTED30],[CONVERTED31])
) C
) D
ON D.YEAR = A.YEAR AND D.MONTH = A.MONTH AND D.ITEM = A.ITEM AND D.DAY_OF_MONTH = A.DAY_OF_MONTH
Is there a way to use CROSS APPLY and yet get the DAY_OF_MONTH out?
This is not a solution with CROSS APPLY but it will definitely make it a bit faster as it uses a bit simpler approach and simpler execution plan.
SQL Fiddle
MS SQL Server 2008 Schema Setup:
CREATE TABLE Test_Table([YEAR] INT, [MONTH] INT, [ITEM] INT, REQ_QTY1 INT
, REQ_QTY2 INT ,REQ_QTY3 INT , CONVERTED1 INT, CONVERTED2 INT, CONVERTED3 INT)
INSERT INTO Test_Table VALUES
( 2015 , 1 , 1 , 10 , 20 , 30 , 100 , 200 , 300),
( 2015 , 2 , 1 , 10 , 20 , 30 , 100 , 200 , 300),
( 2015 , 3 , 1 , 10 , 20 , 30 , 100 , 200 , 300)
Query 1:
SELECT *
FROM
(
SELECT [YEAR]
,[MONTH]
,ITEM
,Vals
,CASE WHEN LEFT(N,3) = 'REQ' THEN SUBSTRING(N,8 ,2)
WHEN LEFT(N,3) = 'CON' THEN SUBSTRING(N,10,2)
END AS Day_Of_Month
,CASE WHEN LEFT(N,3) = 'REQ' THEN LEFT(N,7)
WHEN LEFT(N,3) = 'CON' THEN LEFT(N,9)
END AS Tran_Type
FROM Test_Table t
UNPIVOT (Vals FOR N IN ([REQ_QTY1],[REQ_QTY2],[REQ_QTY3],
[CONVERTED1],[CONVERTED2],[CONVERTED3]))up
)t2
PIVOT (SUM(Vals)
FOR Tran_Type
IN (REQ_QTY, CONVERTED))p
Results:
| YEAR | MONTH | ITEM | Day_Of_Month | REQ_QTY | CONVERTED |
|------|-------|------|--------------|---------|-----------|
| 2015 | 1 | 1 | 1 | 10 | 100 |
| 2015 | 1 | 1 | 2 | 20 | 200 |
| 2015 | 1 | 1 | 3 | 30 | 300 |
| 2015 | 2 | 1 | 1 | 10 | 100 |
| 2015 | 2 | 1 | 2 | 20 | 200 |
| 2015 | 2 | 1 | 3 | 30 | 300 |
| 2015 | 3 | 1 | 1 | 10 | 100 |
| 2015 | 3 | 1 | 2 | 20 | 200 |
| 2015 | 3 | 1 | 3 | 30 | 300 |
Well, I found a way using CROSS APPLY, but instead of taking a substring, I'm basically hardcoding the days. Works well enough so...
SELECT A.YEAR, A.MONTH, A.ITEM, B.DAY_OF_MONTH, B.REQ_QTY, B.CONVERTED
FROM TEST A
CROSS APPLY
(
VALUES
('01', REQ_QTY1, CONVERTED1),
('02', REQ_QTY2, CONVERTED2),
('03', REQ_QTY3, CONVERTED3),
('04', REQ_QTY4, CONVERTED4),
......
('31', REQ_QTY31, CONVERTED31)
) B (DAY_OF_MONTH, REQ_QTY, CONVERTED)

SQL - how do I generate rows for each month based on date ranges in existing dataset?

assume I have a dataset:
rowID | dateStart | dateEnd | Year | Month
121 | 2013-10-03 | 2013-12-03 | NULL | NULL
143 | 2013-12-11 | 2014-03-11 | NULL | NULL
322 | 2014-01-02 | 2014-02-11 | NULL | NULL
And I want sql to generate the following datasource based on the dateStart and the dateEnd. Note the year and month grouping.
rowID | dateStart | dateEnd | Year | Month
121 | 2013-10-03 | 2013-12-03 | 2013 | 10
121 | 2013-10-03 | 2013-12-03 | 2013 | 11
121 | 2013-10-03 | 2013-12-03 | 2013 | 12
143 | 2013-12-11 | 2014-03-11 | 2013 | 12
143 | 2013-12-11 | 2014-03-11 | 2014 | 1
143 | 2013-12-11 | 2014-03-11 | 2014 | 2
143 | 2013-12-11 | 2014-03-11 | 2014 | 3
322 | 2014-01-02 | 2014-02-11 | 2014 | 1
322 | 2014-01-02 | 2014-02-11 | 2014 | 2
I'm having a hard time wrapping my head around this one. Any ideas?
I find it easiest to approach these problems by creating a list of integers and then using that to increment the dates. Here is an example:
with nums as (
select 0 as n
union all
select n + 1 as n
from nums
where n < 11
)
select rowid, datestart, dateend,
year(dateadd(month, n.n, datestart)) as yr,
month(dateadd(month, n.n, datestart)) as mon
from table t join
nums n
on dateadd(month, n.n - 1, datestart) <= dateend;
First, create a tabled-valued function that takes the 2 dates and returns the year and month as a table:
create function dbo.YearMonths(#StartDate DateTime, #EndDate DateTime)
returns #YearMonths table
([Year] int,
[Month] int)
as
begin
set #EndDate = DATEADD(month, 1, #EndDate)
while (#StartDate < #EndDate)
begin
insert into #YearMonths
select YEAR(#StartDate), MONTH(#StartDate)
set #StartDate = DATEADD(month, 1, #StartDate)
end
return
end
As an example the following:
select *
from dbo.YearMonths('1/1/2014', '5/1/2014')
returns:
Then you would join to it like this to get what you wanted:
select m.*, ym.Year, ym.Month
from myTable m
cross apply dbo.YearMonths(dateStart, dateEnd) ym
Try this:
declare #months table(mth int)
insert into #months values(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12)
declare #calendar table(yr int,mth int)
insert into #calendar
select distinct year(datestart),mth
from tbl cross join #months
union
select distinct year(dateend),mth
from tbl cross join #months
select t.rowID, t.datestart, t.dateend, y.yr [Year], y.mth [Month]
from
yourtable t
inner join #calendar y on year(datestart) = yr or year(dateend) = yr
where
(mth >= month(datestart) and mth <= month(dateend) and year(datestart) = year(dateend))
or
(year(datestart) < year(dateend))
and
(year(datestart) = yr and mth >= month(datestart) --All months of start year
or
(year(dateend) = yr and mth <= month(dateend))) -- All months of end year
order by t.rowID, [Year],[Month]
We create a 'Calendar table' which lists all the month and year combinations present in the source table. Then, we join the source table to the calendar table based on the year, and filter as required.

Resources