Query trick - kind of unpivot - sql-server

I have the following table
SnapShotDay OperationalUnitNumber IsOpen StatusDate
1-01-2014 001 1 1-01-2014
2-01-2014 NULL NULL NULL
3-01-2014 001 0 3-01-2014
4-01-2014 NULL NULL NULL
5-01-2014 001 1 5-01-2014
I obtain this with a SELECT construct, but what I need to do now is fill in the "NULL"ed rows by taking values from the first Non nulled row before. The latter would give:
SnapShotDay OperationalUnitNumber IsOpen StatusDate
1-01-2014 001 1 1-01-2014
2-01-2014 001 1 1-01-2014
3-01-2014 001 0 3-01-2014
4-01-2014 001 0 3-01-2014
5-01-2014 001 1 5-01-2014
In functional words: I have events records that give me an event on a date for an oprrational unit; the event is: IsOpen or IsClosed. Chaining those events together according to the date gives a sort of Ranges. What I need is generate daily records for those ranges (target is a fact table).
I am trying to achieve this in plain SQL query (no stored procedure).
Can you think of a trick ?

Declare #t table(
SnapShotDay date,
OperationalUnitNumber int,
IsOpen bit,
StatusDate date
)
insert into #t
select '1-01-2014', 001 , 1 , '1-01-2014' union all
select '2-01-2014', NULL, NULL, NULL union all
select '3-01-2014', 001 , 0 ,'3-01-2014' union all
select '4-01-2014', NULL,NULL,NULL union all
select '5-01-2014', 001 ,1,'5-01-2014'
;
with CTE as
(
select *,row_number()over( order by (select 0))rn from #t
)
select *,
case when a.isopen is null then (
select IsOpen from cte where rn=a.rn-1
) else a.isopen end
from cte a
ok i got it create one more cte1 then,
,cte1 as
(
select top 1 rn ,IsOpen from cte where IsOpen is not null order by rn desc
)
--select * from Statuses
select *,
case
when a.rn<=(select b.rn from cte1 b) and a.IsOpen is null then
(
select
a1.IsOpen
from
cte a1
where
a1.rn=a.rn-1
)
when a.rn>=(select b.rn from cte1 b) and a.IsOpen is null then
(select IsOpen from cte1)
else
a.isopen
end
from
cte a

Try this. In the main query we're looking for the previous date with not null values. Then just JOIN this table with this LastDate.
WITH T1 AS
(
SELECT *, (SELECT MAX(SnapShotDay)
FROM T
WHERE SnapShotDay<=TMain.SnapShotDay
AND OPERATIONALUNITNUMBER IS NOT NULL)
as LastDate
FROM T as TMain
)
SELECT T1.SnapShotDay,
T.OperationalUnitNumber,
T.IsOpen,
T.StatusDate
FROM T1
JOIN T ON T1.LastDate=T.SnapShotDay
SQLFiddle demo

SELECT
t1.SnapShotDay,
CASE WHEN t1.OperationalUnitNumber IS NOT NUll
THEN t1.OperationalUnitNumber
ELSE (SELECT TOP 1 t2.OperationalUnitNumber FROM YourTable t2 WHERE t2.SnapShotDay < t1.SnapShotDay AND t2.OperationalUnitNumber IS NOT NULL ORDER BY SnapShotDay DESC)
END AS OperationalUnitNumber,
CASE WHEN t1.IsOpen IS NOT NUll
THEN t1.IsOpen
ELSE (SELECT TOP 1 t2.IsOpen FROM YourTable t2 WHERE t2.SnapShotDay < t1.SnapShotDay AND t2.IsOpen IS NOT NULL ORDER BY SnapShotDay DESC)
END AS IsOpen,
CASE WHEN t1.StatusDate IS NOT NUll
THEN t1.StatusDate
ELSE (SELECT TOP 1 t2.StatusDate FROM YourTable t2 WHERE t2.SnapShotDay < t1.SnapShotDay AND t2.StatusDate IS NOT NULL ORDER BY SnapShotDay DESC)
END AS StatusDate
FROM YourTable t1

You asked for 'plain sql', here is a tested attempt using SQL, with comments, that gives the required answer.
I have tested the code using 'sqlite' and 'mysql' on windows xp. It is pure SQL and should work everywhere.
SQL is about 'sets' and combining them and ordering the results.
This problem seems to be about two separate sets:
1) The 'snap shot day' that have readings.
2) the 'snap shot day' that don't have readings.
I have added extra columns so that we can easily see where values came from.
let us deal with the easy set first:
This is the set of 'supplied' readings.
SELECT dss.SnapShotDay theDay,
'supplied' readingExists,
dss.OperationalUnitNumber,
dss.IsOpen,
dss.StatusDate
FROM dailysnapshot dss
WHERE dss.OperationalUnitNumber IS NOT NULL
results:
theDay readingExists OperationalUnitNumber IsOpen StatusDate
2014-01-01 supplied 001 1 2014-01-01
2014-01-03 supplied 001 0 2014-01-03
2014-01-05 supplied 001 1 2014-01-05
Now let us deal with the set of 'days that have missing readings'. We need to get the 'most recent day that has readings that is closest to the day with the missing readings' and assume the same values from the 'most recent day' that is before the 'current' missing day.
It sounds complex but it isn't. It asks:
foreach day without a reading - get me the closest, earlier, date that has readings and i will use those readings.
Here is the query:
SELECT emptyDSS.SnapShotDay,
'missing' readingExists,
maxPrevDSS.OperationalUnitNumber,
maxPrevDSS.IsOpen,
maxPrevDSS.StatusDate
FROM dailysnapshot emptyDSS
INNER JOIN dailysnapshot maxPrevDSS ON maxPrevDSS.SnapShotDay =
(SELECT MAX(dss.SnapShotDay)
FROM dailysnapshot dss
WHERE dss.SnapShotDay < emptyDSS.SnapShotDay
AND dss.OperationalUnitNumber IS NOT NULL)
WHERE emptyDSS.OperationalUnitNumber IS NULL
results:
SnapShotDay readingExists OperationalUnitNumber IsOpen StatusDate
2014-01-02 missing 001 1 2014-01-01
2014-01-04 missing 001 0 2014-01-03
This is not about efficiency! It is about getting the correct 'result set' with the easiest to understand SQL code. I assume the database engine will optimize the query. The query can be 'tweaked' later if required.
We now need to combine the two queries and order the results in the manner we require.
The standard way of combining results from SQL queries is with set operators (union, intersection, minus).
we use 'union' and an 'order by' on the result set.
this gives the final query of:
SELECT dss.SnapShotDay theDay,
'supplied' readingExists,
dss.OperationalUnitNumber,
dss.IsOpen,
dss.StatusDate
FROM dailysnapshot dss
WHERE `OperationalUnitNumber` IS NOT NULL
UNION
SELECT emptyDSS.SnapShotDay theDay,
'missing' readingExists,
maxPrevDSS.OperationalUnitNumber,
maxPrevDSS.IsOpen,
maxPrevDSS.StatusDate
FROM dailysnapshot emptyDSS
INNER JOIN dailysnapshot maxPrevDSS ON maxPrevDSS.SnapShotDay =
(SELECT MAX(dss.SnapShotDay)
FROM dailysnapshot dss
WHERE dss.SnapShotDay < emptyDSS.SnapShotDay
AND dss.OperationalUnitNumber IS NOT NULL)
WHERE emptyDSS.OperationalUnitNumber IS NULL
ORDER BY theDay ASC
result:
theDay readingExists dss.OperationalUnitNumber dss.IsOpen dss.StatusDate
2014-01-01 supplied 001 1 2014-01-01
2014-01-02 missing 001 1 2014-01-01
2014-01-03 supplied 001 0 2014-01-03
2014-01-04 missing 001 0 2014-01-03
2014-01-05 supplied 001 1 2014-01-05
I enjoyed doing this.
It should work with most SQL engines.

Related

create date range report based on history table

We have been keeping track of some changes in a History Table like this:
ChangeID EmployeeID PropertyName OldValue NewValue ModifiedDate
100 10 EmploymentStart Not Set 1 2013-01-01
101 10 SalaryValue Not Set 55000 2013-01-01
102 10 SalaryValue 55000 61500 2013-03-20
103 10 SalaryEffectiveDate 2013-01-01 2013-04-01 2013-03-20
104 11 EmploymentStart Not Set 1 2013-01-21
105 11 SalaryValue Not Set 43000 2013-01-21
106 10 SalaryValue 61500 72500 2013-09-20
107 10 SalaryEffectiveDate 2013-04-01 2013-10-01 2013-09-20
Basically if an Employee's Salary changes, we log two rows in the history table. One row for the Salary value itself and the other row for the salary effective date. So these two have identical Modification Date/Time and are kind safe to assume that are always after each other in the database. We can also assume that Salary Value is always logged first (so it is one record before the corresponding effective date
Now we are looking into creating reports based on a given date range into a table like this:
Annual Salary Change Report (2013)
EmployeeID Date1 Date2 Salary
10 2013-01-01 2013-04-01 55000
10 2013-04-01 2013-10-01 61500
10 2013-10-01 2013-12-31 72500
11 2013-03-21 2013-12-31 43000
I have done something similar in the past by joining the table to itself but in those cases the effective date and the new value where in the same row. Now I have to create each row of the output table by looking into a few rows of the existing history table. Is there an straightforward way of doing this whitout using cursors?
Edit #1:
Im reading on this and apparently its doable using PIVOTs
Thank you very much in advance.
You can use self join to get the result you want. The trick is to create a cte and add two rows for each EmployeeID as follows (I call the history table ht):
with cte1 as
(
select EmployeeID, PropertyName, OldValue, NewValue, ModifiedDate
from ht
union all
select t1.EmployeeID,
(case when t1.PropertyName = "EmploymentStart" then "SalaryEffectiveDate" else t1.PropertyName end),
(case when t1.PropertyName = "EmploymentStart" then t1.ModifiedDate else t1.NewValue end),
(case when t1.PropertyName = "SalaryValue" then t1.NewValue
when t1.PropertyName = "SalaryEffectiveDate" then "2013-12-31"
when t1.PropertyName = "EmploymentStart" then "2013-12-31" end),
"2013-12-31"
from ht t1
where t1.ModifiedDate = (select max(t2.ModifiedDate) from ht t2 where t1.EmployeeID = t2.EmployeeID)
)
select t3.EmployeeID, t4.OldValue Date1, t4.NewValue Date2, t3.OldValue Salary
from cte1 t3
inner join cte1 t4 on t3.EmployeeID = t4.EmployeeID
and t3.ModifiedDate = t4.ModifiedDate
where t3.PropertyName = "SalaryValue"
and t4.PropertyName = "SalaryEffectiveDate"
order by t3.EmployeeID, Date1
I hope this helps.
It is a little over kill to use pivot since you only need two properties. Use GROUP BY can also achieve this:
;WITH cte_salary_history(EmployeeID,SalaryEffectiveDate,SalaryValue)
AS
(
SELECT EmployeeID,
MAX(CASE WHEN PropertyName='SalaryEffectiveDate' THEN NewValue ELSE NULL END) AS SalaryEffectiveDate,
MAX(CASE WHEN PropertyName='SalaryValue' THEN NewValue ELSE NULL END) AS SalaryValue
FROM yourtable
GROUP BY EmployeeID,ModifiedDate
)
SELECT EmployeeID,SalaryEffectiveDate,
LEAD(SalaryEffectiveDate,1,'9999-12-31') OVER(PARTITION BY EmployeeID ORDER BY SalaryEffectiveDate) AS SalaryEndDate,
SalaryValue
FROM cte_salary_history

TSQL get COUNT of rows that are missing from right table

There was one other SIMILAR answer but it is 2 pages long and my requirement doesn't need that. I have 2 tables, tableA and a tableB, and I need to find the COUNTS of rows that are present in tableA but are not present in tableB OR if update_on in tableB is not today's date.
My tables:
tableA:
release_id book_name release_begin_date
----------------------------------------------------
1122 midsummer 2016-01-01
1123 fool's errand 2016-06-01
1124 midsummer 2016-04-01
1125 fool's errand 2016-08-01
tableB:
release_id book_name updated_on
-----------------------------------------
1122 midsummer 2016-08-17
1123 fool's errand 2016-08-16**
Expected result: Since each book is missing one release id, 1 is count. But in addition fool's errand's existing row in tableB has updated_on date of yesterday and not today, it needs to be counted in count_of_not_updated.
book_name count_of_missing count_of_not_updated
-------------------------------------------------------
midsummer 1 0
fool's errand 1 1
Note: Even though fool's errand is present in tableB, I need to show it in count_of_missing because it's updated_on date is yesterday and not today. I know it has to be a combination of a left join and something else, but the kicker here is not only getting the missing rows from left table but at the same time checking if the updated_on table was today's date and if not, count that row in count_of_not_updated.
select sum(case when b.release_id is null then 1 else 0 end) as noReleaseID
, sum(case when datediff(d, b.release_date, getdate()) > 0 then 1 else 0 end) as releaseDateNotToday
, a.release_id
from tableA a
left outer join tableB b on a.release_id = b.release_id
Group by a.release_id
This example uses a sum function on a case statement to add up the instances where the case statement returns true. Note that the current code assumes, as in your example, that you are looking to count all old release dates from table b - more steps would be required if each book has multiple old release dates in table b, and you only want to compare to the most recent release date.
Try this
DECLARE #tableA TABLE (release_id INT, book_name NVARCHAR(50), release_begin_date DATETIME)
DECLARE #tableB TABLE (release_id INT, book_name NVARCHAR(50), updated_on DATETIME)
INSERT INTO #tableA
VALUES
(1122, 'midsummer', '2016-01-01'),
(1123, 'fool''s errand', '2016-06-01'),
(1124, 'midsummer', '2016-04-01'),
(1125, 'fool''s errand', '2016-08-01')
INSERT INTO #tableB
VALUES
(1122, 'midsummer', '2016-08-17'),
(1123, 'fool''s errand', '2016-08-16')
;WITH TmpTableA
AS
(
SELECT
book_name,
COUNT(1) CountOfTableA
FROM
#tableA
GROUP BY
book_name
), TmpTableB
AS
(
SELECT
book_name,
COUNT(1) CountOfTableB,
SUM(CASE WHEN CONVERT(VARCHAR(11), updated_on, 112) = CONVERT(VARCHAR(11), GETDATE(), 112) THEN 0 ELSE 1 END) count_of_not_updated
FROM
#tableB
GROUP BY
book_name
)
SELECT
A.book_name ,
A.CountOfTableA - ISNULL(B.CountOfTableB, 0) AS count_of_missing,
ISNULL(B.count_of_not_updated, 0) AS count_of_not_updated
FROM
TmpTableA A LEFT JOIN
TmpTableB B ON A.book_name = B.book_name
Result:
book_name count_of_missing count_of_not_updated
-------------------- ---------------- --------------------
fool's errand 1 1
midsummer 1 1

SQL Server : sorting NULL Marks with ORDER BY clause

I have this query :
SELECT orderid, shippeddate
FROM Sales.Orders
WHERE custid = 20
ORDER BY shippedate;
and the output is:
orderid shippeddate
------- -----------
11008 NULL
11072 NULL
10258 2006-07-23 00:00:00.000
10263 2006-07-31 00:00:00.000
10351 2006-11-20 00:00:00.000
10368 2006-12-02 00:00:00.000
...
As an exercise, I am trying to figure out how to sort the orders by shippeddate ascending, but have NULLs sort last.
I know that standard SQL support the options NULL FIRST and NULL LAST, but T-SQL doesn't support this option.
Any suggestions?
Thanks
You can do something like this:
SELECT orderid, shippeddate
FROM Sales.Orders
WHERE custid = 20
ORDER BY case when shippeddate is null then 2 else 1 end, shippedate;
In addition,
You could replace value in the field if it is null by using ISNULL
ISNULL ( check_expression , replacement_value )
So your query would look something like this
SELECT ISNULL(orderid, '1900-01-01'),
shippeddate
FROM Sales.Orders
WHERE custid = 20
ORDER BY shippedate asc;
So this would replace any NULLs in that field with a value (1900-01-01 in this case) you could sort on

SQL query to split records by intervals

Let's assume I have a table which has columns From and To which are dates and a bit type column which identifies whether it is a cancel (1 = cancel). Also an Id which is a PK and CancelId which references what is cancelled.
Let's say I have records which look like:
Id From To IsCancel CancelId
1 2015-01-01 2015-01-31 0 NULL
2 2015-01-03 2015-01-09 1 1
3 2015-01-27 2015-01-31 1 1
I am expecting the result to show what intervals of then non-cancel records are still uncancelled:
Id From To
1 2015-01-01 2015-01-02
1 2015-01-10 2015-01-26
I can make it so it would split each record into dates, then subtract cancelled dates from the records then merge the intervals but since I have quite a lot of records, I find this very inefficient and am pretty sure that I am overlooking something simple.
The task you want to achieve is non trivial. A possible solution involves placing all From / To dates in an ordered sequence. The following UNPIVOT operation:
SELECT ID, EventDate, StartStop,
ROW_NUMBER() OVER (ORDER BY ID, EventDate, StartStop) AS EventRowNum,
IsCancel
FROM
(SELECT ID, IsCancel, [From], [To]
FROM Event) Src
UNPIVOT (
EventDate FOR StartStop IN ([From], [To])
) AS Unpvt
produces this result set:
ID EventDate StartStop EventRowNum IsCancel
--------------------------------------------------
1 2015-01-01 From 1 0
2 2015-01-03 From 2 1
2 2015-01-09 To 3 1
3 2015-01-27 From 4 1
3 2015-01-31 To 5 1
1 2015-01-31 To 6 0
Using a CTE, you can subsequently simulate LEAD function (available from SQL Server 2012 onwards) in order to place in a single record the current and the next date from the sequence above:
;WITH StretchEventDates AS
(
-- above query goes here
), CTE AS
(
SELECT s.ID, s.EventDate, s.StartStop, s.IsCancel,
sLead.EventDate As LeadEventDate, sLead.StartStop AS LeadStartStop, sLead.IsCancel AS LeadIsCancel
FROM StretchEventDates AS s
LEFT JOIN StretchEventDates AS sLead ON s.EventRowNum + 1 = sLead.EventRowNum
)
The above produces the following result set:
ID EventDate StartStop IsCancel LeadEventDate LeadStartStop LeadIsCancel
--------------------------------------------------------------------------------------
1 2015-01-01 From 0 2015-01-03 From 1
2 2015-01-03 From 1 2015-01-09 To 1
2 2015-01-09 To 1 2015-01-27 From 1
3 2015-01-27 From 1 2015-01-31 To 1
3 2015-01-31 To 1 2015-01-31 To 0
1 2015-01-31 To 0 NULL NULL NULL
Using CASE statements you can filter these records in order to get the desired output.
Putting it all together:
;WITH StretchEventDates AS
(
SELECT ID, EventDate, StartStop,
ROW_NUMBER() OVER (ORDER BY EventDate, StartStop) AS EventRowNum,
IsCancel
FROM
(SELECT ID, IsCancel, [From], [To]
FROM Event) Src
UNPIVOT (
EventDate FOR StartStop IN ([From], [To])
) AS Unpvt
), CTE AS
(
SELECT s.ID, s.EventDate, s.StartStop, s.IsCancel,
sLead.EventDate As LeadEventDate, sLead.StartStop AS LeadStartStop, sLead.IsCancel AS LeadIsCancel
FROM StretchEventDates AS s
LEFT JOIN StretchEventDates AS sLead ON s.EventRowNum + 1 = sLead.EventRowNum
), CTE_FINAL AS
(SELECT *,
CASE WHEN StartStop = 'From' AND IsCancel = 0 THEN EventDate
WHEN StartStop = 'To' AND IsCancel = 1 THEN DATEADD(d, 1, EventDate)
END AS [From],
CASE WHEN LeadStartStop = 'From' AND LeadIsCancel = 1 THEN DATEADD(d, -1, LeadEventDate)
WHEN LeadStartStop = 'To' AND LeadIsCancel = 0 THEN LeadEventDate
END AS [To]
FROM CTE
)
SELECT ID, [From], [To]
FROM CTE_FINAL
WHERE [From] IS NOT NULL AND [To] IS NOT NULL AND [From] <= [To]
You may have to add additional CASEs in the query above to handle additional combinations of 'cancelations' following 'non-canceled' (and vice-versa) events.
With the data provided in the OP the above yields the following output:
ID From To
---------------------------
1 2015-01-01 2015-01-02
2 2015-01-10 2015-01-26

SQL Server 2005 - Update column where DATEDIFF between two dates is minimum

I have two tables, defined as following:
PTable:
[StartDate], [EndDate], [Type], PValue
.................................................
2011-07-01 2011-07-07 001 5
2011-07-08 2011-07-14 001 10
2011-07-01 2011-07-07 002 15
2011-07-08 2011-07-14 002 20
TTable:
[Date], [Type], [TValue]
..................................
2011-07-01 001 11
2011-07-02 001 4
2011-07-03 001 0
2011-07-08 002 12
2011-07-09 002 12
2011-07-10 002 0
I want to update Tvalue column in TTable with the PValue in PTable, where [Date] in TTable is between [StartDate] and [EndDate] in PTable and DATEDIFF(DAY,TTable.[Date],PTable.[EndDate]) is minimum, AND PTable.Type = TTable.Type
The final TTable should look like this:
[Date], [Type], [TValue]
..................................
2011-07-01 001 11
2011-07-02 001 4
2011-07-03 001 5 --updated
2011-07-08 002 12
2011-07-09 002 12
2011-07-10 002 20 --updated
What I have tried is this:
UPDATE [TTable]
SET
TValue = T1.PValue
FROM TTable
INNER JOIN PTable T1 ON
[Date] BETWEEN T1.StartDate AND T1.EndDate
AND DATEDIFF(DAY,[Date],T1.EndDate) =
(SELECT MIN( DATEDIFF(DAY,TTable.[Date],T.EndDate) )
FROM PTable T WHERE TTable.[Date] BETWEEN T.StartDate AND T.EndDate
)
AND
T1.[Type] = TTable.[Type]
It gives me this error :
"Multiple columns are specified in an aggregated expression containing an outer reference. If an expression being aggregated contains an outer reference, then that outer reference must be the only column referenced in the expression."
Later edit:
Considering TTable AS T and PTable AS P, the condition for update are:
1. T.Type = P.Type
2. T.Date BETWEEN P.StartDate AND P.EndDate
3. DATEDIFF(DAY,T.Date,P.EndDate) = minimum value of all DATEDIFFs WHERE P.Type = T.Type AND T.Date BETWEEN P.StartDate AND P.EndDate
Later Edit 2:
Sorry, because I typed wrong the last row in PTable (2011-08-10 instead 2011-07-14), the final result was wrong.
I also managed to update in a simpler way, which I obviously should have tried from the start:
UPDATE TTABLE
SET
TValue = T1.PValue
FROM TTable
INNER JOIN PTABLE T1 ON
[Date] = (SELECT TOP(1) MAX(Date) FROM [TTABLE] WHERE [Date] BETWEEN T1.StartDate AND T1.EndDate)
AND
T1.Type = [TTABLE].Type
Sorry about this.
So you said something about "DATEDIFF(DAY,TTable.[Date],PTable.[EndDate]) is minimum" which confused me. Itt would seem like if there a weekly entry per Type, then for a particular Date, Type combination it would ever only match one. You might give this a try:
UPDATE TTABLE
SET TValue = T1.PValue
FROM TTable
INNER JOIN PTABLE T1 ON T1.Type = [TTABLE].Type -- find row in PTable that the Date falls between
and [Date] BETWEEN T1.StartDate AND T1.EndDate)
where
TValue = ( select MIN(TValue) -- finds the lowest TValue, 0 in example
from TTable))
...updated...
So it appears I read the problem incorrectly the first time. I had thought we update the TTable entries that have the lowest TValue. Not sure how I got that impression. Still seems like there needs to be a check for if it is 0?
UPDATE TTable
SET TValue = T1.PValue
FROM TTable
INNER JOIN PTable T1 ON T1.Type = TTable.Type
and T1.EndDate = (
SELECT top 1 EndDate
FROM PTable
WHERE Type=TTable.Type
ORDER BY abs(DATEDIFF(day,TTable.Date,PTable.EndDate)) desc)
WHERE
TValue = 0 -- only updating entries that aren't set, have a 0
This only works if there is one is one row in PTable with an EndDate of 7/7 or whatever for a given type. If there are two entries for Type 001 with an end date of 7/7, then it will join to two entries. Also if there is two entries that are equal distant from the date in question, so an EndDate of 7/7 and one of 7/13 are both 3 days from 7/10. If the EndDates are all 7 days apart (weekly) you should be ok.

Resources