T-SQL - How to use a recursive calculation in a CTE? - tsql

I want to calculate the avarage earnings per month of an employee during his time he worked for a company.
I build a recursive CTE based on a similar thread and I get the results only for one employee. What kind of modification do I need to do to make the employee ID variable and to get the same kind of results for every employee in the table?
Here is my code:
DECLARE #tbl AS TABLE
(
ID VARCHAR(50) ,
Earning FLOAT,
StartDate DATE ,
EndDate DATE
)INSERT INTO #tbl
( ID, Earning, StartDate, EndDate
SELECT employee_ID AS ID
,([Total Earning]/ (SELECT datediff(MONTH,[EndDate],
[StartDate])+1
FROM [employee_table] WHERE employee_ID = 'EKA-0004562'))
,[StarTDate]
,[EndDate]
FROM [employee_table]
WHERE employee_ID = 'EKA-0004562'
--final query using recursive cte
;
WITH cte
AS ( SELECT T.ID ,
T.Earning ,
T.StartDate ,
T.EndDatum ,
CONVERT(DATE, NULL) AS Dt ,
n = 0
FROM #tbl AS T
UNION ALL
SELECT cte.ID ,
cte.Earning ,
cte.StartDate ,
cte.EndDatum ,
DATEADD(MONTH, n, cte.StartDate) ,
cte.n + 1
FROM cte
WHERE n <= DATEDIFF(MONTH, cte.StartDate, cte.EndDatum)
)
SELECT cte.ID ,
cte.Earning,
dt AS Months
FROM cte
WHERE cte.Dt IS NOT NULL
This is my goal:
Well, I have a table with a lot of data with information concerning employees.
Every employee has a "startdate" (the time when he startet to work for the company) and and "enddate" (the time when he quit the job).
I would like to write into a table the same count of rows as the employee worked for the company in month. For example:
My basic table:
Employee Number | StartDate | EndDate | Total Earnings (Total Earnings/(EndDate-StartDate)
4711 20150101 20150523 24110
This example shows that the employee worked for the company for 5 Month and earned 24110€. In Avarage he earned 4822€ per month.
So I want to insert in the new table 5 rows with the following information:
New Table:
Employee Number | StartDate | EndDate | AVG Earnings
row1: 4711 20150101 20150523 4822
row2: 4711 20150201 20150523 4822
row3: 4711 20150301 20150523 4822
row4: 4711 20150401 20150523 4822
row5: 4711 20150501 20150523 4822

Here you go:
SAMPLE DATA
create table #test
(
ID INT,
StartDate DATE,
EndDatum DATE,
Earning INT
)
insert into #test values
(4711,'20150101','20150523',24110),
(4712,'20150101','20150625',32550)
QUERY
--final query using recursive cte
;
WITH cte
AS ( SELECT T.ID ,
T.Earning ,
T.StartDate ,
T.EndDatum ,
CONVERT(DATE, NULL) AS Dt ,
n = 0
FROM #test AS T
UNION ALL
SELECT cte.ID ,
cte.Earning ,
cte.StartDate ,
cte.EndDatum ,
DATEADD(MONTH, n, cte.StartDate) ,
cte.n + 1
FROM cte
WHERE n <= DATEDIFF(MONTH, cte.StartDate, cte.EndDatum)
)
SELECT cte.ID ,
dt StartDate,
cte.EndDatum EndDate,
cte.Earning / ((DATEDIFF(MONTH, cte.StartDate, cte.EndDatum)) +1) [AVG Earnings]
FROM cte
WHERE cte.Dt IS NOT NULL
OUTPUT
ID StartDate EndDate AVG Earnings
4712 2015-01-01 2015-06-25 5425
4712 2015-02-01 2015-06-25 5425
4712 2015-03-01 2015-06-25 5425
4712 2015-04-01 2015-06-25 5425
4712 2015-05-01 2015-06-25 5425
4712 2015-06-01 2015-06-25 5425
4711 2015-01-01 2015-05-23 4822
4711 2015-02-01 2015-05-23 4822
4711 2015-03-01 2015-05-23 4822
4711 2015-04-01 2015-05-23 4822
4711 2015-05-01 2015-05-23 4822

I may be wrong but I think your query seems overly complicated and the recursive CTE is not needed. It may be become inneficient when dealing with a lot of employees and consecutive years.
This query give the correct output for the 2 sample users:
Sample
declare #employee_table table(Employee_Number int, StartDate date, EndDate date, Total_Earnings bigint);
Insert Into #employee_table(Employee_Number, StartDate, EndDate, Total_Earnings) values
(4711, '20150101', '20150523', 24110)
, (4712, '20150101', '20150423', 24110);
Query
with list(n) as (
Select ROW_NUMBER() over(Order By n)
From (
Select 1 From (values(1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) as l1(n)
Cross Join (values(1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) as l2(n)
Cross Join (values(1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) as l3(n)
) as l(n)
)
Select l.n, e.Employee_Number
, [StartDate] = DATEADD(MONTH, l.n-1, e.StartDate)
, [EndDate] = e.EndDate
, [AVG Earnings] = e.Total_Earnings/(DATEDIFF(MONTH, e.StartDate, e.EndDate)+1)
From #employee_table as e
Inner Join list as l on l.n <= DATEDIFF(MONTH, e.StartDate, e.EndDate)+1
Order By e.Employee_Number, l.n
Output
n Employee_Number StartDate EndDate AVG Earnings
1 4711 2015-01-01 2015-05-23 4822
2 4711 2015-02-01 2015-05-23 4822
3 4711 2015-03-01 2015-05-23 4822
4 4711 2015-04-01 2015-05-23 4822
5 4711 2015-05-01 2015-05-23 4822
1 4712 2015-01-01 2015-04-23 6027
2 4712 2015-02-01 2015-04-23 6027
3 4712 2015-03-01 2015-04-23 6027
4 4712 2015-04-01 2015-04-23 6027
notes:
#employee_table contains 2 employee. 4711 is similar to your sample
The list CTE efficiently creates a list from 1 to 1000 which should be good enough for 1000 months (> 83 years)
The main query uses it to create the list of consecutive months using just a simple INNER JOIN.

Related

Find Accounts with X Number of Transactions within Y Days of Each Other in a Larger Date Range

I am trying to write a SQL statement that will find the accounts that have had 3 or more transactions within 3 days whose absolute value is greater than $10.00 over the course of a week and then return those transactions.
Consider this data...
TransactionID AccountNumber TransactionDate TransactionAmount
------------- ------------- --------------- -----------------
1 0123 2020-09-01 45.75
2 0123 2020-09-02 5.23
3 0123 2020-09-03 9.94
4 0123 2020-09-05 8.35
5 0123 2020-09-06 -16.23
6 0123 2020-09-07 14.71
7 0123 2020-09-08 15.03
8 0123 2020-09-08 23.10
9 0123 2020-09-09 94.20
10 0123 2020-09-09 5.01
11 0123 2020-09-10 3.02
12 0123 2020-09-11 4.37
13 0123 2020-09-12 4.54
14 9876 2020-09-01 -45.75
15 9876 2020-09-02 5.27
16 9876 2020-09-05 19.79
17 9876 2020-09-05 -11.64
18 9876 2020-09-06 12.42
If the week under review is 2020-09-01 to 2020-09-07 I would expect only AccountNumber 9876 to fit the criteria with TransactionIDs 16, 17, and 18 being the 3 transactions within 3 days with an absolute value greater than $10.00.
It seems like I should be able to use window functions (and perhaps framing), but I can't figure out how to start.
I have attempted without the use of window functions based on the answers to this question...
multiple transactions within a certain time period, limited by date range
DECLARE
#BeginDate DATE
, #EndDate DATE
, #ThresholdAmount DECIMAL(10, 2)
, #ThresholdCount INT
, #NumberOfDays INT;
SET #BeginDate = '09/01/2020';
SET #EndDate = '09/07/2020';
SET #ThresholdAmount = 10.00;
SET #ThresholdCount = 3;
SET #NumberOfDays = 3;
SELECT t.*
FROM (
SELECT
t1.*
, (
SELECT COUNT(*)
FROM Transactions t2
WHERE t2.AccountNumber = t1.AccountNumber
AND t2.TransactionID <> t1.TransactionID
AND t2.TransactionDate >= t1.TransactionDate
AND t2.TransactionDate < DATEADD(DAY, #NumberOfDays, t1.TransactionDate)
AND ABS(t2.TransactionAmount) > #ThresholdAmount
) AS NumberWithinXDays
FROM Transactions t1
WHERE t1.TransactionDate BETWEEN #BeginDate AND #EndDate
AND ABS(t1.TransactionAmount) > #ThresholdAmount
) t
WHERE t.NumberWithinXDays >= #ThresholdCount;
SELECT *
FROM Transactions t
WHERE EXISTS (
SELECT *
FROM (
SELECT t1.AccountNumber
FROM Transactions t1
INNER JOIN Transactions t2 ON t1.AccountNumber = t2.AccountNumber
AND t1.TransactionID <> t2.TransactionID
AND DATEDIFF(DAY, t1.TransactionDate, t2.TransactionDate) BETWEEN 0 AND (#NumberOfDays-1)
WHERE t1.TransactionDate BETWEEN #BeginDate AND #EndDate
AND t2.TransactionDate BETWEEN #BeginDate AND #EndDate
AND ABS(t1.TransactionAmount) > #ThresholdAmount
AND ABS(t2.TransactionAmount) > #ThresholdAmount
GROUP BY t1.AccountNumber
HAVING COUNT(t1.TransactionID) >= #ThresholdCount
) x
WHERE x.AccountNumber = t.AccountNumber
)
AND t.TransactionDate BETWEEN #BeginDate AND #EndDate
AND ABS(t.TransactionAmount) > #ThresholdAmount
My first query comes back with...
TransactionID AccountNumber TransactionDate TransactionAmount NumberWithinXDays
------------- ------------- --------------- ----------------- -----------------
5 0123 2020-09-06 -16.23 3
6 0123 2020-09-07 14.71 3
Not even close. And the second query returns...
TransactionID AccountNumber TransactionDate TransactionAmount
------------- ------------- --------------- -----------------
14 9876 2020-09-01 -45.75
16 9876 2020-09-05 19.79
17 9876 2020-09-05 -11.64
18 9876 2020-09-06 12.42
Closer, but not restricted to just transaction within 3 days of each other. This is the result I want.
TransactionID AccountNumber TransactionDate TransactionAmount
------------- ------------- --------------- -----------------
16 9876 2020-09-05 19.79
17 9876 2020-09-05 -11.64
18 9876 2020-09-06 12.42
Now it is certainly possible I have not implemented these suggested queries correctly. Or maybe there is some subtle difference I am missing and they just don't fit my situation.
Any suggestions on fixing either of my attempted queries or something completely different with or without window functions?
Here is full dbfiddle of my code.
I was not able to come up with a solution using window functions. As I thought about it more I thought I might be able to use a CTE, but I could not figure that out either.
I solve it using a couple of subqueries. I was concerned about performance given my transaction table has 86 million rows. However, it runs in less than 30 seconds and that is good enough for me.
-- distinct is need because a particular transaction may fit into more than
-- one transaction window but we only want to see it once in the results
SELECT DISTINCT
t.TransactionID
, t.AccountNumber
, t.TransactionDate
, t.TransactionAmount
FROM (
SELECT
t1.AccountNumber
, t1.TransactionDateWindowBegin
, t1.TransactionDateWindowEnd
, COUNT(DISTINCT t2.TransactionID) AS Count
FROM (
-- establish the transaction window for each transaction within the
-- larger date range and an absolute value above the threshold
SELECT
TransactionID
, AccountNumber
, TransactionDate AS [TransactionDateWindowBegin]
, DATEADD(DAY, #NumberOfDays - 1, TransactionDate) AS [TransactionDateWindowEnd]
, TransactionAmount
FROM Transactions
WHERE TransactionDate BETWEEN #BeginDate AND #EndDate
AND ABS(TransactionAmount) > #ThresholdAmount
) t1
-- join back to the transaction table to find transactions within the transaction window for
-- each transaction, count them, and only keep those that are above the threshold count
INNER JOIN Transactions t2 ON t1.AccountNumber = t2.AccountNumber
AND t1.TransactionDateWindowBegin <= t2.TransactionDate
AND t1.TransactionDateWindowEnd >= t2.TransactionDate
WHERE t2.TransactionDate BETWEEN #BeginDate AND #EndDate
AND ABS(t2.TransactionAmount) > #ThresholdAmount
GROUP BY t1.AccountNumber
, t1.TransactionDateWindowBegin
, t1.TransactionDateWindowEnd
HAVING COUNT(DISTINCT t2.TransactionID) >= #ThresholdCount
) x
-- join back to the transaction table again to get the details for the
-- transactions that meet the threshold amount and count criteria
INNER JOIN Transactions t ON x.AccountNumber = t.AccountNumber
AND x.TransactionDateWindowBegin <= t.TransactionDate
AND x.TransactionDateWindowEnd >= t.TransactionDate
AND ABS(t.TransactionAmount) > #ThresholdAmount;
Here is the full demo.

Repeating value of previous row in a join

I have one table including accounts and their balance. I would like to report the balance for each day while for missing days report the last day.
Table accounts:
AccountName Date Balance
thomas 2008-10-09 1000
thomas 2008-10-20 5000
david 2008-02-18 2000
david 2008-03-10 200000
let's say we want the report for 2018-10 I need to get something like this
thomas 2008-10-01 0
...
thomas 2008-10-09 1000
thomas 2008-10-10 1000
...
thomas 2008-10-20 5000
...
thomas 2008-10-31 5000
I went this far:
DECLARE #StartDate datetime = '2008/10/9';
DECLARE #EndDate datetime = '2008/10/20';
WITH theDates AS
(
SELECT #StartDate as theDate
UNION ALL
SELECT DATEADD(day, 1, theDate)
FROM theDates
WHERE DATEADD(day, 1, theDate) <= #EndDate
)
select * from accounts a
right outer join thedates d on a.date=d.theDate
order by thedate
Results:
AccountNo Date Balance theDate
----------- ---------- -------- ----------
thomas 2008-10-09 1000 2008-10-09
NULL NULL NULL 2008-10-10
NULL NULL NULL 2008-10-11
NULL NULL NULL 2008-10-12
NULL NULL NULL 2008-10-13
NULL NULL NULL 2008-10-14
NULL NULL NULL 2008-10-15
NULL NULL NULL 2008-10-16
NULL NULL NULL 2008-10-17
NULL NULL NULL 2008-10-18
NULL NULL NULL 2008-10-19
thomas 2008-10-20 5000 2008-10-20
Any idea?
Update:
I end up using cursor. This is version working perfectly including the situation where an account has no entry.
DECLARE #Date datetime
declare #result table (accountname nvarchar(50), balance int, date datetime)
DECLARE #StartDate datetime = '2008/10/1';
DECLARE #EndDate datetime = '2008/10/29';
declare cur cursor for
WITH theDates AS
(
SELECT #StartDate as theDate
UNION ALL
SELECT DATEADD(day, 1, theDate)
FROM theDates
WHERE DATEADD(day, 1, theDate) <= #EndDate
)
select * from theDates
open cur
fetch next from cur into #date
while ##FETCH_STATUS=0
begin
insert into #result
select b.accountName, isnull(balance,
(select isnull((select top 1 balance from accounts where date<#date and accountName=b.accountName order by date desc),0))
), #date from
(select * from accounts where date = #date) a
right outer join (select distinct(accountname) from accounts ) b on a.accountname = b.accountname
fetch next from cur into #date
end
close cur
deallocate cur
select * from #result
Try this:
DECLARE #StartDate datetime = '2008/10/9';
DECLARE #EndDate datetime = '2008/10/20';
WITH theDates AS
(
SELECT #StartDate as theDate
UNION ALL
SELECT DATEADD(day, 1, theDate)
FROM theDates
WHERE DATEADD(day, 1, theDate) <= #EndDate
),
acc AS(
SELECT a.AccountName,
a.Balance,
a.Date,
isnull(c.CloseDate, cast(GETDATE()as date)) as CloseDate
FROM accounts a
CROSS APPLY(SELECT MIN(b.Date) as CloseDate
FROM accounts b
WHERE b.Date > a.Date) c
)
SELECT a.AccountName, a.Balance, a.Date, d.theDate
FROM acc a, theDates d
WHERE a.Date <= d.theDate
AND a.CloseDate > d.theDate
option (maxrecursion 0)
Results:
AccountName Balance Date theDate
----------- ----------- ------------------- -----------------------
thomas 1000 2008-10-09 00:00:00 2008-10-09 00:00:00.000
thomas 1000 2008-10-09 00:00:00 2008-10-10 00:00:00.000
thomas 1000 2008-10-09 00:00:00 2008-10-11 00:00:00.000
thomas 1000 2008-10-09 00:00:00 2008-10-12 00:00:00.000
thomas 1000 2008-10-09 00:00:00 2008-10-13 00:00:00.000
thomas 1000 2008-10-09 00:00:00 2008-10-14 00:00:00.000
thomas 1000 2008-10-09 00:00:00 2008-10-15 00:00:00.000
thomas 1000 2008-10-09 00:00:00 2008-10-16 00:00:00.000
thomas 1000 2008-10-09 00:00:00 2008-10-17 00:00:00.000
thomas 1000 2008-10-09 00:00:00 2008-10-18 00:00:00.000
thomas 1000 2008-10-09 00:00:00 2008-10-19 00:00:00.000
thomas 5000 2008-10-20 00:00:00 2008-10-20 00:00:00.000
You can try to use aggregate function MIN and MAX make calendar table then OUTER JOIN
WITH theDates AS
(
SELECT AccountName, MIN(Date) as StartDt,MAX(Date) EndDt
FROM accounts
GROUP BY AccountName
UNION ALL
SELECT AccountName,DATEADD(day, 1, StartDt),EndDt
FROM theDates
WHERE DATEADD(day, 1, StartDt) <= EndDt
)
select d.AccountName,
d.StartDt [date],
ISNULL(a.Balance,0) Balance
from accounts a
LEFT join thedates d on a.date=d.StartDt
order by StartDt

Get Data Week Wise in SQL Server

I have a Table with columns ProductId, DateofPurchase, Quantity.
I want a report in which week it belongs to.
Suppose if I give March Month I can get the quantity for the march month.
But I want as below if I give date as parameter.
Here Quantity available for March month on 23/03/2018 is 100
Material Code Week1 Week2 Week3 Week4
12475 - - - 100
The logic is 1-7 first week, 8-15 second week, 16-23 third week, 24-30 fourth week
#Sasi, this can get you started. YOu will need to use CTE to build a template table that describes what happens yearly. Then using your table with inner join you can link it up and do a pivot to group the weeks.
Let me know if you need any tweaking.
DECLARE #StartDate DATE='20180101'
DECLARE #EndDate DATE='20180901'
DECLARE #Dates TABLE(
Workdate DATE Primary Key
)
DECLARE #tbl TABLE(ProductId INT, DateofPurchase DATE, Quantity INT);
INSERT INTO #tbl
SELECT 12475, '20180623', 100
;WITH Dates AS(
SELECT Workdate=#StartDate,WorkMonth=DATENAME(MONTH,#StartDate),WorkYear=YEAR(#StartDate), WorkWeek=datename(wk, #StartDate )
UNION ALL
SELECT CurrDate=DateAdd(WEEK,1,Workdate),WorkMonth=DATENAME(MONTH,DateAdd(WEEK,1,Workdate)),YEAR(DateAdd(WEEK,1,Workdate)),datename(wk, DateAdd(WEEK,1,Workdate)) FROM Dates D WHERE Workdate<#EndDate ---AND (DATENAME(MONTH,D.Workdate))=(DATENAME(MONTH,D.Workdate))
)
SELECT *
FROM
(
SELECT
sal.ProductId,
GroupWeek='Week'+
CASE
WHEN WorkWeek BETWEEN 1 AND 7 THEN '1'
WHEN WorkWeek BETWEEN 8 AND 15 THEN '2'
WHEN WorkWeek BETWEEN 16 AND 23 THEN '3'
WHEN WorkWeek BETWEEN 24 AND 30 THEN '4'
WHEN WorkWeek BETWEEN 31 AND 37 THEN '5'
WHEN WorkWeek BETWEEN 38 AND 42 THEN '6'
END,
Quantity
FROM
Dates D
JOIN #tbl sal on
sal.DateofPurchase between D.Workdate and DateAdd(DAY,6,Workdate)
)T
PIVOT
(
SUM(Quantity) FOR GroupWeek IN (Week1, Week2, Week3, Week4, Week5, Week6, Week7, Week8, Week9, Week10, Week11, Week12, Week13, Week14, Week15, Week16, Week17, Week18, Week19, Week20, Week21, Week22, Week23, Week24, Week25, Week26, Week27, Week28, Week29, Week30, Week31, Week32, Week33, Week34, Week35, Week36, Week37, Week38, Week39, Week40, Week41, Week42, Week43, Week44, Week45, Week46, Week47, Week48, Week49, Week50, Week51, Week52
/*add as many as you need*/)
)p
--ORDER BY
--1
option (maxrecursion 0)
Sample Data :
DECLARE #Products TABLE(Id INT PRIMARY KEY,
ProductName NVARCHAR(50))
DECLARE #Orders TABLE(ProductId INT,
DateofPurchase DATETIME,
Quantity BIGINT)
INSERT INTO #Products(Id,ProductName)
VALUES(1,N'Product1'),
(2,N'Product2')
INSERT INTO #Orders( ProductId ,DateofPurchase ,Quantity)
VALUES (1,'2018-01-01',130),
(1,'2018-01-09',140),
(1,'2018-01-16',150),
(1,'2018-01-24',160),
(2,'2018-01-01',30),
(2,'2018-01-09',40),
(2,'2018-01-16',50),
(2,'2018-01-24',60)
Query :
SELECT P.Id,
P.ProductName,
Orders.MonthName,
Orders.Week1,
Orders.Week2,
Orders.Week3,
Orders.Week4
FROM #Products AS P
INNER JOIN (SELECT O.ProductId,
SUM((CASE WHEN DATEPART(DAY,O.DateofPurchase) BETWEEN 1 AND 7 THEN O.Quantity ELSE 0 END)) AS Week1,
SUM((CASE WHEN DATEPART(DAY,O.DateofPurchase) BETWEEN 8 AND 15 THEN O.Quantity ELSE 0 END)) AS Week2,
SUM((CASE WHEN DATEPART(DAY,O.DateofPurchase) BETWEEN 16 AND 23 THEN O.Quantity ELSE 0 END)) AS Week3,
SUM((CASE WHEN DATEPART(DAY,O.DateofPurchase) >= 24 THEN O.Quantity ELSE 0 END)) AS Week4,
DATENAME(MONTH,O.DateofPurchase) AS MonthName
FROM #Orders AS O
GROUP BY O.ProductId,DATENAME(MONTH,O.DateofPurchase)) AS Orders ON P.Id = Orders.ProductId
Result :
-----------------------------------------------------------------------
| Id | ProductName | MonthNumber | Week1 | Week2 | Week3 | Week4 |
-----------------------------------------------------------------------
| 1 | Product1 | January | 130 | 140 | 150 | 160 |
| 2 | Product2 | January | 30 | 40 | 50 | 60 |
-----------------------------------------------------------------------

t-sql select max value between two columns, or col one when col two is null

This is not easy for me to describe in the title (please forgive me), but here is my problem:
Suppose you have the following table:
CREATE TABLE Subscriptions (product char(3), start_date datetime, end_date datetime);
INSERT INTO #Subscriptions
VALUES('ABC', '2015-01-28 00:00:00', '2016-02-15 00:00:00'),
('ABC', '2016-02-04 12:08:00', NULL),
('DEF', '2013-04-15 00:00:00', '2013-06-10 00:00:00'),
('GHI', '2013-01-11 00:00:00', '2013-04-08 00:00:00');
Now I want to find out for how long a subscription has been either active or passive. I thus need to select the newest end_dates grouped by product, BUT if end_date is null, then I want start_date.
So - I have:
product start_date end_date
ABC 28-01-2015 00:00 15-02-2016 00:00
ABC 04-02-2016 12:08 NULL
DEF 15-04-2013 00:00 10-06-2013 00:00
GHI 11-01-2013 00:00 08-04-2013 00:00
What I want to find in my query:
product relevant_date
ABC 04-02-2016 12:08
DEF 10-06-2013 00:00
GHI 08-04-2013 00:00
I have tried using a union, and that seems to work, but it is very slow, and my question is: is there a more efficient way to solve this (I am using MS SQL Server 2012):
SELECT [product]
,MAX([start_date]) AS start_date
,NULL AS [end_date]
,MAX([start_date]) AS relevant_date
FROM Subscriptions
where end_date IS NULL
GROUP BY product
UNION
SELECT [product]
,NULL
,MAX([end_date])
,MAX([end_date])
FROM Subscriptions
where end_date IS not NULL and product not in (SELECT product FROM Subscriptions
where end_date IS NULL)
GROUP BY product
(If you have a suggestion for another title for my question, I am also all ears!)
For version 2012 or higher you can use a combination of distinct, first_value and isnull, like this:
SELECT DISTINCT
product,
FIRST_VALUE(ISNULL(end_date,start_date))
OVER(PARTITION BY product
ORDER BY ISNULL(end_date, '9999-12-31') DESC) AS EndDate
FROM Subscriptions
Results:
product EndDate
ABC 04.02.2016 12:08:00
DEF 10.06.2013 00:00:00
GHI 08.04.2013 00:00:00
For versions between 2008 and 2012, you can use a cte with row_number to get the same effect:
;WITH CTE AS
(
SELECT product,
ISNULL(end_date,start_date) As relevant_date,
ROW_NUMBER() OVER(PARTITION BY product ORDER BY ISNULL(end_date, '9999-12-31') DESC) As rn
FROM Subscriptions
)
SELECT product,
relevant_date
FROM CTE
WHERE rn = 1
See a live demo on rextester.
If the second ABC row is showing the incorrect start_date then this query should work
SELECT S.product
, relevant_date = MAX(ISNULL(S.end_date,S.start_date))
FROM dbo.Subscriptions S
GROUP BY S.product
This should do it:
select s1.product,MAX(case when useStartDate=1 then s1.startDate else s1.endDate end) 'SubscriptionDate'
from #Subscriptions s1
join (select s2s1.product, max(case when s2s1.endDate is null then 1 else 0 end) 'useStartDate' from #Subscriptions s2s1 group by s2s1.product) s2 on s1.product=s2.product
group by s1.product

Break into multiple rows based on date range of a single row

I have a table which captures appointments, some are single day appointments and some are multi day appointments, so the data looks like
AppointmentId StartDate EndDate
9 2017-04-12 2017-04-12
10 2017-05-01 2017-05-03
11 2017-06-01 2017-06-01
I want to split the multi day appointment as single days, so the result I am trying to achieve is like
AppointmentId StartDate EndDate
9 2017-04-12 2017-04-12
10 2017-05-01 2017-05-01
10 2017-05-02 2017-05-02
10 2017-05-03 2017-05-03
11 2017-06-01 2017-06-01
So I have split the appointment id 10 into multiple rows. I checked a few other questions like
here but those are to split just based on a single start date and end date and not based on table data
You can use a Calendar or dates table for this sort of thing.
For only 152kb in memory, you can have 30 years of dates in a table with this:
/* dates table */
declare #fromdate date = '20000101';
declare #years int = 30;
/* 30 years, 19 used data pages ~152kb in memory, ~264kb on disk */
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
select top (datediff(day, #fromdate,dateadd(year,#years,#fromdate)))
[Date]=convert(date,dateadd(day,row_number() over(order by (select 1))-1,#fromdate))
into dbo.Dates
from n as deka cross join n as hecto cross join n as kilo
cross join n as tenK cross join n as hundredK
order by [Date];
create unique clustered index ix_dbo_Dates_date
on dbo.Dates([Date]);
Without taking the actual step of creating a table, you can use it inside a common table expression with just this:
declare #fromdate date = '20161229';
declare #thrudate date = '20170103';
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, dates as (
select top (datediff(day, #fromdate, #thrudate)+1)
[Date]=convert(date,dateadd(day,row_number() over(order by (select 1))-1,#fromdate))
from n as deka cross join n as hecto cross join n as kilo
cross join n as tenK cross join n as hundredK
order by [Date]
)
select [Date]
from dates;
Use either like so:
select
t.AppointmentId
, StartDate = d.date
, EndDate = d.date
from dates d
inner join appointments t
on d.date >= t.StartDate
and d.date <= t.EndDate
rextester demo: http://rextester.com/TNWQ64342
returns:
+---------------+------------+------------+
| AppointmentId | StartDate | EndDate |
+---------------+------------+------------+
| 9 | 2017-04-12 | 2017-04-12 |
| 10 | 2017-05-01 | 2017-05-01 |
| 10 | 2017-05-02 | 2017-05-02 |
| 10 | 2017-05-03 | 2017-05-03 |
| 11 | 2017-06-01 | 2017-06-01 |
+---------------+------------+------------+
Number and Calendar table reference:
Generate a set or sequence without loops - 1 - Aaron Bertrand
Generate a set or sequence without loops - 2 - Aaron Bertrand
Generate a set or sequence without loops - 3 - Aaron Bertrand
The "Numbers" or "Tally" Table: What it is and how it replaces a loop - Jeff Moden
Creating a Date Table/Dimension in sql Server 2008 - David Stein
Calendar Tables - Why You Need One - David Stein
Creating a date dimension or calendar table in sql Server - Aaron Bertrand
tsql Function to Determine Holidays in sql Server - Aaron Bertrand
F_table_date - Michael Valentine Jones
Clearly a Calendar/Tally table would be the way to go as SqlZim illustrated (+1), however you can use an ad-hoc tally table with a CROSS APPLY.
Example
Select A.AppointmentId
,StartDate = B.D
,EndDate = B.D
From YourTable A
Cross Apply (
Select Top (DateDiff(DD,A.StartDate,A.EndDate)+1) D=DateAdd(DD,-1+Row_Number() Over (Order By Number),A.StartDate)
From master..spt_values
) B
Returns
AppointmentId StartDate EndDate
9 2017-04-12 2017-04-12
10 2017-05-01 2017-05-01
10 2017-05-02 2017-05-02
10 2017-05-03 2017-05-03
11 2017-06-01 2017-06-01