create date range report based on history table - sql-server

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

Related

Displaying all columns in SQL and also sum of columns with same ID in the last Repeating row

I have 2 tables
OrderDetails:
Id Name type Quantity
------------------------------------------
2009 john a 10
2009 john a 20
2010 sam b 25
2011 sam c 50
2012 sam d 30
ValueDetails:
Id Value
-------------------
2009 300
2010 500
2011 200
2012 100
I need to get an output which displays the data as such :
Id Name type Quantity Price
-------------------------------------------------
2009 john a 10
2009 john a 20 9000
2010 sam b 25
2011 sam c 50
2012 sam d 30 25500
The price is calculated by Value x Quantity and the sum of the values is displayed in the last repeating row of the given Name.
I tired to use sum and group by but I get only two rows. I need to display all 5 rows. How can I write this query?
You can use Row_Number with max of Row_Number to get this formatted sum
;with cte as (
select od.*, sm= sum( od.Quantity*vd.value ) over (partition by Name),
RowN = row_number() over(partition by Name order by od.id)
from #yourOrderDetails od
inner join #yourValueDetails vd
on od.Id = vd.Id
)
select Id, Name, Type, Quantity,
case when max(RowN) over(partition by Name) = row_number() over(partition by Name order by Id)
then sm else null end as ActualSum
from cte
Your input tables:
create table #yourOrderDetails (Id int, Name varchar(20), type varchar(2), Quantity int)
insert into #yourOrderDetails (Id, Name, type, Quantity) values
(2009 ,'john','a', 10 )
,(2009 ,'john','a', 20 ) ,(2010 ,'sam ','b', 25 )
,(2011 ,'sam ','c', 50 ) ,(2012 ,'sam ','d', 30 )
create table #yourValueDetails(Id int, Value Int)
insert into #yourValueDetails(Id, value) values
( 2009 , 300 ) ,( 2010 , 500 )
,( 2011 , 200 ) ,( 2012 , 100 )
SELECT a.ID,
a.Name,
a.Type,
a.quantity,
price = (a.quantity * b.price)
FROM OrderDetails a LEFT JOIN
ValueDetails b on a.id = b.id
This will put the price on every row. If you want to do a SUM by Id,Name and Type it's not going to show the individual records like you show them above. If you want to put a SUM on one of the lines that share the same Id, Name and Type then you'd need a rule to figure out which one and then you could probably use a CASE statement to decide on which line you want to show the SUM total.

UPDATE SQL Query to update a table based on two dates

i would like to update my query table based on two dates, i tried the following code but it didn't work
UPDATE [Stock Report] INNER JOIN Report ON [Stock Report].ItemID = Report.ItemID SET [Stock Report].Amount = SUM(Report.Amount) WHERE [Date Product Was Made] BETWEEN #AA AND #BB
My two query tables are Report and Stock Report
Report Table
ItemID| Item |Date Product Was Made|Amount|ProductID|Product Name
10 Flour 11/17/2015 100 54 Saltbread
11 Bran Flakes 11/17/2015 100 54 Saltbread
10 Flour 11/17/2015 100 54 Saltbread
11 Bran Flakes 11/17/2015 100 54 Saltbread
What i would like to see in Stock Report Table
ItemID| Item | Start Date | End Date |Amount
10 Flour 11/16/2015 11/25/2015 200
11 Bran Flakes 11/16/2015 11/25/2015 200
The dates can be any two random dates but the table should generate a total based on the amount used in between the date ranges. Wht]at would be the SQL code to complete this process
As EuphoriaGrogi mentioned, you were pretty close.
SELECT ItemID, Item, #AA AS StartDate, #BB AS EndDate, SUM(Amount) AS TotalAmount
FROM (YourReportTableQueryWith#AAAnd#BBParametersHere) AS SubQuery
GROUP BY SubQuery.ItemID,SubQuery.Item
More on GROUP BY here: http://www.w3schools.com/sql/sql_groupby.asp
You tagged with mysql, sql-server and ms-access.
But answers will differ for each case.
Here SQLServer solution:
WITH SumAmount AS (
SELECT
ItemId
,SUM(Report.amount) as sum_amount
FROM
Report
GROUP BY
ItemId
)
UPDATE SR
SET
amount = SA.sum_amount
FROM
[Stock Report] SR
JOIN SumAmount SA ON SR.ItemID = SA.ItemID
WHERE
[Date Product Was Made] BETWEEN #AA AND #BB
Supposed MS Access query (I'm not proficient with MSACCESS, going by http://www.fmsinc.com/microsoftaccess/query/snytax/update-query.html):
UPDATE [Stock Report]
JOIN (
SELECT
ItemId
,SUM(Report.amount) as sum_amount
FROM
Report
GROUP BY
ItemId
) SA ON [Stock Report].ItemID = SA.ItemID
SET
[Stock Report].amount = SA.sum_amount
WHERE
[Date Product Was Made] BETWEEN #AA AND #BB

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.

Query trick - kind of unpivot

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.

Selecting rows with the nearest date using SQL

I have a SQL statement.
SELECT
ID, LOCATION, CODE,MAX(DATE),FLAG
FROM
TABLE1
WHERE
DATE <= CONVERT(DATETIME,'11-11-2012')
AND EXISTS (SELECT * FROM #TEMP_CODE WHERE TABLE1.CODE = #TEMP_CODE.CODE)
AND ID IN (14, 279)
GROUP BY
ID, LOCATION, CODE
I need rows with the nearest date to the 11-11-2012, but the table returns all the values. What am I doing wrong. Thanks
ID LOCATION CODE DATE FLAG
-------------------------------------------------------------------
14 CAR STREET,UDUPI 234 2012-08-08 00:00:00.000 0
14 CAR STREET,UDUPI 234 2012-08-10 00:00:00.000 1
14 CAR STREET,UDUPI 234 2012-08-14 00:00:00.000 0
279 MADHUGIRI 234 2012-08-08 00:00:00.000 1
279 MADHUGIRI 234 2012-08-11 00:00:00.000 0
I want to show only the rows with dates less than or equal to the given date. The required result is
ID LOCATION CODE DATE FLAG
-------------------------------------------------------------------
14 CAR STREET,UDUPI 234 2012-08-10 00:00:00.000 1
279 MADHUGIRI 234 2012-08-11 00:00:00.000 0
;WITH x AS
(
SELECT ID, Location, Code, Date, Flag,
rn = ROW_NUMBER() OVER
(PARTITION BY ID, Location, Code ORDER BY [Date] DESC)
FROM dbo.TABLE1 AS t1
WHERE [Date] <= '20121111'
AND ID IN (14, 279) -- sorry, missed this
AND EXISTS (SELECT 1 FROM #TEMP_CODE WHERE CODE = t1.CODE)
)
SELECT ID, Location, Code, Date, Flag
FROM x WHERE rn = 1;
This yields:
ID LOCATION CODE [Date] FLAG
--- ---------------- ---- ---------- ----
14 CAR STREET,UDUPI 234 2012-08-14 0
279 MADHUGIRI 234 2012-08-11 0
This disagrees with your required results, but I think those are wrong and I think you should check them.
Use a subquery to get the max date for each ID, and then join that to your table:
SELECT
ID, LOCATION, CODE, DATE, FLAG
FROM
TABLE1
JOIN (
SELECT ID AS SubID, MAX(DATE) AS SubDATE
FROM TABLE1
WHERE DATE < '11/11/2012'
AND EXISTS (SELECT * FROM #TEMP_CODE WHERE TABLE1.CODE = #TEMP_CODE.CODE)
AND ID IN (14, 279)
GROUP BY ID
) AS SUB ON ID = SubID AND DATE = SubDATE
add a Order BY DATE LIMIT 0,2
With the order by you will make the date order by the closest to your condition in where and with the limit will return only the top 2 values!
SET ROWCOUNT 2
SELECT
ID, LOCATION, CODE,MAX(DATE),FLAG
FROM
TABLE1
WHERE
DATE <= CONVERT(DATETIME,'11-11-2012')
AND EXISTS (SELECT * FROM #TEMP_CODE WHERE TABLE1.CODE = #TEMP_CODE.CODE)
AND ID IN (14, 279)
GROUP BY
ID, LOCATION, CODE
ORDER BY DATE

Resources