Count 2 different columns in one result set - sql

I am facing a problem and getting incorrect results. I'd like to count records that have leavedates and deceaseddates for each year and display them in one table.
This is my query so far:
select year(leavedate) as leave_year, count(year(leavedate)) as cnt_leave , count(year(DeceasedDate)) as cnt_dod
from clients
where leavedate is not null and DeceasedDate is not null
group by year(leavedate),year(DeceasedDate)
order by 1 desc
This is the goal I'd like like to have:
+------+-----------+---------+
| Year | Cnt_leave | Cnt_dod |
+------+-----------+---------+
| 2018 | 542 | 5685 |
| 2017 | 8744 | 5622 |
| 2016 | 556 | 325 |
| etc | | |
+------+-----------+---------+
Should I do it in two separate select statements or is it possible to do it in one?
I'd appreciate a feedback!

I would do:
with
l as (
select year(leavedate) as y, count(*) as c
from clients
group by year(leavedate)
),
d as (
select year(deceaseddate) as y, count(*) as c
from clients
group by year(deceaseddate)
)
select
coalesce(l.y, d.y) as year,
coalesce(l.c, 0) as cnt_leave,
coalesce(d.c, 0) as cnt_dod
from l
full outer join d on l.y = d.y
order by coalesce(l.y, d.y) desc

If i understand, you are looking for something like this :
select year(leavedate) as leave_year, sum(case leavedate when null then 0 else 1 end) as cnt_leave,
sum(case DeceasedDate when null then 0 else 1 end) as cnt_dod
from clients
where leavedate is not null or DeceasedDate is not null
group by year(leavedate)
order by 1 desc

You'll need to count in two steps as Year(LeaveDate) is not related to Year(DeceasedDate); but you combine those SELECTs via some CTE construction.
In fact, there are (at least) two ways you can do this, not sure which one will be the better performer; you'll need to try to figure out what works best on your data.
SELECT client_id = 1, LeaveDate = Convert(datetime, '20080118'), DeceasedDate = Convert(datetime, '20090323')
INTO #test
UNION ALL SELECT client_id = 2, LeaveDate = '20100118', DeceasedDate = '20100323'
UNION ALL SELECT client_id = 3, LeaveDate = '20100118', DeceasedDate = '20120323'
;WITH src
AS (SELECT *
FROM #test
WHERE LeaveDate IS NOT NULL
AND DeceasedDate IS NOT NULL),
leave
AS (SELECT yr = Year(LeaveDate), cnt = COUNT(*) FROM src GROUP BY Year(LeaveDate)),
deceased
AS (SELECT yr = Year(DeceasedDate), cnt = COUNT(*) FROM src GROUP BY Year(DeceasedDate))
SELECT yr = ISNULL(l.yr, d.yr),
cntLeaveDate = ISNULL(l.cnt, 0),
cntDeceasedDate = ISNULL(d.cnt, 0)
FROM leave l
FULL OUTER JOIN deceased d
ON d.yr = l.yr
ORDER BY 1 DESC
;WITH src
AS (SELECT *
FROM #test
WHERE LeaveDate IS NOT NULL
AND DeceasedDate IS NOT NULL),
years
AS (SELECT DISTINCT yr = Year(LeaveDate) FROM src
UNION -- Do not UNION ALL here, you want to avoid doubles!
SELECT DISTINCT yr = Year(DeceasedDate) FROM src)
SELECT yr,
cntLeaveDate = SUM ( CASE WHEN Year(LeaveDate) = yr THEN 1 ELSE 0 END),
cntDeceasedDate = SUM ( CASE WHEN Year(DeceasedDate) = yr THEN 1 ELSE 0 END)
FROM years, #test
WHERE LeaveDate IS NOT NULL
AND DeceasedDate IS NOT NULL
GROUP BY yr
ORDER BY 1 DESC
DROP TABLE #test

I would use apply for this . . . I think it is the simplest solution:
select year(dte) as year, sum(isleave) as num_leaves, sum(isdeceased) as numdeceased
from clients c cross apply
(values (leavedate, 1, 0),
(deceaseddate, 0, 1)
) v(dte, isleave, isdeceased)
where dte is not null
group by year(dte)
order by 1 desc;

SQL FIDDLE
Based on your comments, all you need is to remove the where clause and limit the group by.
select
year(leavedate) as leave_year,
count(year(leavedate)) as cnt_leave ,
count(year(DeceasedDate)) as cnt_dod
from clients
--where leavedate is not null and DeceasedDate is not null
group by year(leavedate) --,year(DeceasedDate)
--order by 1 desc
Or, if your data could have NULL's in either column, which I assume above and thus omitted the where clause you'd want:
select
coalesce(year(leavedate),year(DeceasedDate)) as the_year,
count(year(leavedate)) as cnt_leave ,
count(year(DeceasedDate)) as cnt_dod
from clients
--where leavedate is not null and DeceasedDate is not null
group by coalesce(year(leavedate),year(DeceasedDate))
--order by 1 desc
Here's the example:
declare #clients table (leavedate date null, DeceasedDate date null)
insert into #clients
values
('20180101',NULL)
,(null,'20170101')
,('20150101','20150101')
select
year(leavedate) as leave_year,
count(year(leavedate)) as cnt_leave ,
count(year(DeceasedDate)) as cnt_dod
from #clients
--where leavedate is not null and DeceasedDate is not null
group by year(leavedate) --,year(DeceasedDate)
--order by 1 desc
select
coalesce(year(leavedate),year(DeceasedDate)) as the_year,
count(year(leavedate)) as cnt_leave ,
count(year(DeceasedDate)) as cnt_dod
from #clients
--where leavedate is not null and DeceasedDate is not null
group by coalesce(year(leavedate),year(DeceasedDate))
--order by 1 desc

Related

count rows which have max value less than specified parameter

I want to find in my table, max value which is less than specified in parameter and get count of rows that have the same value as max value. For example in my table I have values: (4,1,3,1,4,4,10), and it is list of parameters in string "2,9,10,4". I have to split string to separate parameters. Base on this sample values I want to get something like that:
param | max value | count
2 | 1 | 2
9 | 4 | 3
10 | 4 | 3
4 | 3 | 1
And it is my sample query:
select
[param]
, max([val]) [max_value_by_param]
, max(count) [count]
from(
select
n.value as [param]
,a.val
, count(*) as [count]
from (--mock of table
select 1 as val union all
select 3 as val union all
select 4 as val union all
select 1 as val union all
select 3 as val union all
select 4 as val union all
select 4 as val union all
select 10 as val
) a
join (select [value] from string_split('2,9,10,4', ',')) n--list of params
on a.val < n.[value]
group by n.value, a.val
) tmp
group by [param]
Is it possible to do it better/easier ?
Here is a way to express this using apply:
select s.value as param, a.val, a.cnt
from string_split('2,9,10,4', ',') s outer apply
(select top (1) a.val, count(*) as cnt
from a
group by a.val
having a.val < s.value
order by a.val desc
) a;
Here is a db<>fiddle.
But the fastest method is probably going to be:
with av as (
select a.val, count(*) as cnt
from a
group by a.val
union all
select s.value, null as cnt
from string_split('2,9,10,4', ',') s
)
select val, a_val, a_cnt
from (select av.*,
max(case when cnt is not null then val end) over (order by val, (case when cnt is null then 1 else 2 end)) as a_val,
max(case when cnt is not null then cnt end) over (order by val, (case when cnt is null then 1 else 2 end)) as a_cnt
from av
) av
where cnt is null;
This only aggregates the data once and should return all parameters, even those with no preceding values in a.

Oracle SQL: How to select only ID‘s which are member in specific groups?

I want to select only those ID‘s which are in specific groups.
For example:
ID GroupID
1 11
1 12
2 11
2 12
2 13
Here I want to select the ID's which are in the groups 11 and 12 but in no other groups.
So the result should show just the ID 1 and not 2.
Can someone provide a SQL for that?
I tried it with
SELECT ID FROM table
WHERE GroupID = 11 AND GroupID = 12 AND GroupID != 13;
But that didn't work.
You can use aggregation:
select id
from mytable
group by id
having min(groupID) = 11 and max(groupID) = 12
This having condition ensures that the given id belongs to groupIDs 11 and 12, and to no other group. This works because 11 and 12 are sequential numbers.
Other options: if you want ids that belong to group 11 or 12 (not necessarily both), and to no other group, then:
having sum(case when groupId in (11, 12) then 1 end) = count(*)
If numbers are not sequential, and you want ids in both groups (necessarily) and in no other group:
having
max(case when groupID = 11 then 1 end) = 1
and max(case when groupID = 12 then 1 end) = 1
and max(case when groupID in (11, 12) then 0 else 1 end) = 0
SELECT t.id FROM table t
where exists(
SELECT * FROM table
where group = 11
and t.id = id
)
and exists(
SELECT * FROM table
where group = 12
and t.id = id
)
and not exists(
SELECT * FROM table
where group = 13
and t.id = id
)
group by t.id
One method is conditional aggregation:
select id
from t
group by id
having sum(case when groupid = 1 then 1 else 0 end) > 0 and
sum(case when groupid = 2 then 1 else 0 end) > 0 and
sum(case when groupid in (1, 2) then 1 else 0 end) = 0 ;
You can use GROUP BY with HAVING and a conditional COUNT:
SELECT id
FROM table_name
GROUP BY ID
HAVING COUNT( CASE Group_ID WHEN 11 THEN 1 END ) > 0
AND COUNT( CASE Group_ID WHEN 12 THEN 1 END ) > 0
AND COUNT( CASE WHEN Group_ID NOT IN ( 11, 12 ) THEN 1 END ) = 0
Or you can use collections:
CREATE TYPE int_list IS TABLE OF NUMBER(8,0);
and:
SELECT id
FROM table_name
GROUP BY id
HAVING int_list( 11, 12 ) SUBMULTISET OF CAST( COLLECT( group_id ) AS int_list )
AND CARDINALITY( CAST( COLLECT( group_id ) AS int_list )
MULTISET EXCEPT int_list( 11, 12 ) ) = 0
(Using collections has the advantage that you can pass the collection of required values as a single bind parameter whereas using conditional aggregation is probably going to require dynamic SQL if you want to pass a variable number of items to the query.)
Both output:
| ID |
| -: |
| 1 |
db<>fiddle here
Use joins:
SELECT DISTINCT c11.ID
FROM (SELECT ID FROM WORK_TABLE WHERE GROUPID = 11) c11
INNER JOIN (SELECT ID FROM WORK_TABLE WHERE GROUPID = 12) c12
ON c12.ID = c11.ID
LEFT OUTER JOIN (SELECT ID FROM WORK_TABLE WHERE GROUPID NOT IN (11, 12)) co
ON co.ID = c11.ID
WHERE co.ID IS NULL;
The INNER JOIN between the first two subqueries ensures that rows exist for both GROUPID 11 and 12, and the LEFT OUTER JOIN and WHERE verify that there are no rows for any other GROUPIDs.
dbfiddle here

SQL Server subquery returns nothing based on another subquery

I have a 2 subqueries where based on the first subquery, the second subquery's result changes which I don't want to happen.
The 2 queries are the same exactly other than the variable they use for the date.
The first query always works correctly, but the second query seems to be affected sometimes by the first query.
If I have the first query before the second query then they will both return the same date. If I have the second query before the first, then it sometimes returns nothing.
declare #curDate date = DATEADD(week,0,'2016/02/01')
declare #shortDate date = DATEADD(week,-2,#curDate)
SELECT -- Select Columns
quere1.Location,
quere1.Product,
quere1.Short,
MAX( quere1.Date ) [Date],
MAX( quere1.Volume ) [Volume],
MAX( quere1.Cost ) [Cost],
MAX( quere2.Date ) [DateAgo],
MAX( quere2.Volume ) [VolumeAgo],
MAX( quere2.Cost ) [CostAgo]
FROM
(SELECT --subQuery 1
cst_mac_a.loc [Location], --Branch
cst_mac_a.product [Product], --Product
pro_duct.desc4 [Short],
MAX( cst_mac_a.datecreated ) [Date], --Date
MAX( cst_mac_a.ohvol ) [Volume], --Volume
MAX( cst_mac_a.ohextcost ) [Cost] --Cost
FROM cst_mac as cst_mac_a
JOIN pro_type ON pro_type.proType = cst_mac_a.proType
JOIN pro_duct ON pro_duct.proType = cst_mac_a.proType AND pro_duct.product = cst_mac_a.product
WHERE
cst_mac_a.protype = 'HW' AND
cst_mac_a.datecreated in (SELECT MAX( sub.datecreated ) from cst_mac as sub WHERE
cst_mac_a.product = sub.product AND sub.datecreated <= #curDate) AND
cst_mac_a.timecreated in (SELECT MAX( sub.timecreated ) from cst_mac as sub WHERE
cst_mac_a.product = sub.product AND sub.datecreated in
(SELECT MAX( sub2.datecreated ) from cst_mac as sub2 WHERE
sub2.datecreated <= #curDate and sub2.product = sub.product)) AND
cst_mac_a.PROGRESS_RECID in (SELECT MAX( sub.PROGRESS_RECID ) from cst_mac as sub WHERE
cst_mac_a.product = sub.product AND sub.datecreated in
(SELECT MAX( sub2.datecreated ) from cst_mac as sub2 WHERE
sub2.datecreated <= #curDate AND sub2.product = sub.product)) AND
cst_mac_a.ohvol <> 0 AND
cst_mac_a.product = 'A4ROUP'
GROUP BY
cst_mac_a.loc,
cst_mac_a.product,
pro_duct.desc4)
as quere1
LEFT OUTER JOIN (SELECT --Basically same code, subquery 2
cst_mac_a2.loc [Location], --Branch
cst_mac_a2.product [Product], --Product
pro_duct.desc4 [Short],
MAX( cst_mac_a2.datecreated ) [Date], --Date
MAX( cst_mac_a2.ohvol ) [Volume], --Volume
MAX( cst_mac_a2.ohextcost ) [Cost] --Cost
FROM cst_mac as cst_mac_a2
JOIN pro_type ON pro_type.proType = cst_mac_a2.proType
JOIN pro_duct ON pro_duct.proType = cst_mac_a2.proType AND pro_duct.product = cst_mac_a2.product
WHERE
cst_mac_a2.protype = 'HW' AND
cst_mac_a2.datecreated in (SELECT MAX( sub3.datecreated ) from cst_mac as sub3 WHERE
cst_mac_a2.product = sub3.product AND sub3.datecreated <= #shortDate) AND
cst_mac_a2.timecreated in (SELECT MAX( sub3.timecreated ) from cst_mac as sub3 WHERE
cst_mac_a2.product = sub3.product AND sub3.datecreated in
(SELECT MAX( sub4.datecreated ) from cst_mac as sub4 WHERE
sub4.datecreated <= #shortDate and sub4.product = sub3.product)) AND
cst_mac_a2.PROGRESS_RECID in (SELECT MAX( sub3.PROGRESS_RECID ) from cst_mac as sub3 WHERE
cst_mac_a2.product = sub3.product AND sub3.datecreated in
(SELECT MAX( sub4.datecreated ) from cst_mac as sub4 WHERE
sub4.datecreated <= #shortDate AND sub4.product = sub3.product))
GROUP BY
cst_mac_a2.loc,
cst_mac_a2.product,
pro_duct.desc4)
as quere2
ON quere1.Product = quere2.Product AND quere1.Location = quere2.Location
GROUP BY
quere1.Location,
quere1.Product,
quere1.Short
Order BY
quere1.Location,
quere1.Product,
quere1.Short
Sample Output:
curDate = Today
shortDate = 2 weeks ago:
Location | Product | Short | Date | Volume | Cost | DateAgo | VolumeAgo | CostAgo
Mill | A4ROUP | BN. | 2/1/2016|40 | 36 | | |
curDate = 1 week ago
shortDate = 2 weeks ago
Location | Product | Short | Date | Volume | Cost | DateAgo | VolumeAgo | CostAgo
Mill | A4ROUP | BN. |1/25/2016|27 | 25 |1/18/2016| 29 | 26
That's the way LEFT JOIN works. It gets all rows from the first query, but only gets matching rows (per the JOIN conditions in the ON clause) from the second query.
If you want to always get all rows from both queries, even if there is no matching row in the opposite query, then you need to use a FULL OUTER JOIN.

SQL Query in finding out the max number from a table with Null value

How Can I work out this requirement. Please help.
Client Table - CT
ClientID Balance
123 10
123 20
123 30
123 40
124 50
124 60
124 Null
I want to find our the max(Balance) from the CT Table.
Condition - > If there is no null value then I have to find out max(Balance) otherwise It should be Null. See below result, that am expecting.
ClientID Balance
123 40
124 Null
Am writing the query as below. But Is there any more dynamic way to do?
Select ClientID,
CASE WHEN MIN(Balance) = NULL THEN
NULL
ELSE
MAX(Balance) END AS 'MaxBalance'
From CT
Group by clientID
Please let me know, is there any otheralternative?
Try this:
Select ClientID,
(CASE WHEN count(balance) < count(*)
THEN NULL
ELSE MAX(Balance)
END) AS MaxBalance
From CT
Group by clientID
Or, a bit more cumbsersome, but perhaps clearer:
Select ClientID,
(CASE WHEN sum(case when balance is null then 1 else 0 end) > 0
THEN NULL
ELSE MAX(Balance)
END) AS MaxBalance
From CT
Group by clientID
How about:
SELECT clientid
, balance
FROM
(
SELECT clientid
, balance
, row_number()
over( partition by clientid
order by CASE WHEN balance IS NULL THEN 0 ELSE 1 END
, balance DESC
) r
FROM ct
) n
WHERE r = 1
I'm not sure what datatype [Balance] is, but if it is an int, you can do the following:
Select ClientID, NULLIF(MAX(ISNULL(Balance,2147483647)),2147483647)
From CT
GROUP BY ClientID
If [Balance] is not an int, just replace 2147483647 with the max value of that datatype.
The danger, of course, would be if you really do have a client with a balance of 2147483647. In such a case their max balance would show as null.
This will work on SQL 2005+
; WITH a AS (
SELECT DISTINCT ClientID
FROM CT
WHERE Balance IS NULL )
SELECT t.ClientID, MAX(t.Balance) "MaxBalance"
FROM CT t
LEFT JOIN a ON t.CLientID = a.ClientID
WHERE a.ClientID IS NULL
GROUP BY t.CLientID
UNION ALL
SELECT a.ClientID, NULL
FROM a
This works but is not very elegant:
SELECT SUB.ClientID, CASE (SELECT COUNT(ClientID)
FROM MyTable MT
WHERE Balance IS NULL
AND MT.ClientID = SUB.ClientID)
WHEN 0 THEN (SELECT MAX(Balance)
FROM MyTable MT
WHERE MT.ClientID = SUB.ClientID)
ELSE NULL END AS Balance
FROM (SELECT ClientID
FROM [MyTable]
GROUP BY ClientID) SUB

Creating groups of consecutive days meeting a given criteria

I have table the following data structure in SQL Server:
ID Date Allocation
1, 2012-01-01, 0
2, 2012-01-02, 2
3, 2012-01-03, 0
4, 2012-01-04, 0
5, 2012-01-05, 0
6, 2012-01-06, 5
etc.
What I need to do is get all consecutive day periods where Allocation = 0, and in the following form:
Start Date End Date DayCount
2012-01-01 2012-01-01 1
2012-01-03 2012-01-05 3
etc.
Is it possible to do this in SQL, and if so how?
In this answer, I'll assume that the "id" field numbers the rows consecutively when sorted by increasing date, like it does in the example data. (Such a column can be created if it does not exist).
This is an example of a technique described here and here.
1) Join the table to itself on adjacent "id" values. This pairs adjacent rows. Select rows where the "allocation" field has changed. Store the result in a temporary table, also keeping a running index.
SET #idx = 0;
CREATE TEMPORARY TABLE boundaries
SELECT
(#idx := #idx + 1) AS idx,
a1.date AS prev_end,
a2.date AS next_start,
a1.allocation as allocation
FROM allocations a1
JOIN allocations a2
ON (a2.id = a1.id + 1)
WHERE a1.allocation != a2.allocation;
This gives you a table having "the end of the previous period", "the start of the next period", and "the value of 'allocation' in the previous period" in each row:
+------+------------+------------+------------+
| idx | prev_end | next_start | allocation |
+------+------------+------------+------------+
| 1 | 2012-01-01 | 2012-01-02 | 0 |
| 2 | 2012-01-02 | 2012-01-03 | 2 |
| 3 | 2012-01-05 | 2012-01-06 | 0 |
+------+------------+------------+------------+
2) We need the start and end of each period in the same row, so we need to combine adjacent rows again. Do this by creating a second temporary table like boundaries but having an idx field 1 greater:
+------+------------+------------+
| idx | prev_end | next_start |
+------+------------+------------+
| 2 | 2012-01-01 | 2012-01-02 |
| 3 | 2012-01-02 | 2012-01-03 |
| 4 | 2012-01-05 | 2012-01-06 |
+------+------------+------------+
Now join on the idx field and we get the answer:
SELECT
boundaries2.next_start AS start,
boundaries.prev_end AS end,
allocation
FROM boundaries
JOIN boundaries2
USING(idx);
+------------+------------+------------+
| start | end | allocation |
+------------+------------+------------+
| 2012-01-02 | 2012-01-02 | 2 |
| 2012-01-03 | 2012-01-05 | 0 |
+------------+------------+------------+
** Note that this answer gets the "internal" periods correctly but misses the two "edge" periods where allocation = 0 at the beginning and allocation = 5 at the end. Those can be pulled in using UNION clauses but I wanted to present the core idea without that complication.
Following would be one way to do it. The gist of this solution is
Use a CTE to get a list of all consecutive start and enddates with Allocation = 0
Use the ROW_NUMBER window function to assign rownumbers depending on both start- and enddates.
Select only those records where both ROW_NUMBERS equal 1.
Use DATEDIFFto calculate the DayCount
SQL Statement
;WITH r AS (
SELECT StartDate = Date, EndDate = Date
FROM YourTable
WHERE Allocation = 0
UNION ALL
SELECT r.StartDate, q.Date
FROM r
INNER JOIN YourTable q ON DATEDIFF(dd, r.EndDate, q.Date) = 1
WHERE q.Allocation = 0
)
SELECT [Start Date] = s.StartDate
, [End Date ] = s.EndDate
, [DayCount] = DATEDIFF(dd, s.StartDate, s.EndDate) + 1
FROM (
SELECT *
, rn1 = ROW_NUMBER() OVER (PARTITION BY StartDate ORDER BY EndDate DESC)
, rn2 = ROW_NUMBER() OVER (PARTITION BY EndDate ORDER BY StartDate ASC)
FROM r
) s
WHERE s.rn1 = 1
AND s.rn2 = 1
OPTION (MAXRECURSION 0)
Test script
;WITH q (ID, Date, Allocation) AS (
SELECT * FROM (VALUES
(1, '2012-01-01', 0)
, (2, '2012-01-02', 2)
, (3, '2012-01-03', 0)
, (4, '2012-01-04', 0)
, (5, '2012-01-05', 0)
, (6, '2012-01-06', 5)
) a (a, b, c)
)
, r AS (
SELECT StartDate = Date, EndDate = Date
FROM q
WHERE Allocation = 0
UNION ALL
SELECT r.StartDate, q.Date
FROM r
INNER JOIN q ON DATEDIFF(dd, r.EndDate, q.Date) = 1
WHERE q.Allocation = 0
)
SELECT s.StartDate, s.EndDate, DATEDIFF(dd, s.StartDate, s.EndDate) + 1
FROM (
SELECT *
, rn1 = ROW_NUMBER() OVER (PARTITION BY StartDate ORDER BY EndDate DESC)
, rn2 = ROW_NUMBER() OVER (PARTITION BY EndDate ORDER BY StartDate ASC)
FROM r
) s
WHERE s.rn1 = 1
AND s.rn2 = 1
OPTION (MAXRECURSION 0)
Alternative way with CTE but without ROW_NUMBER(),
Sample data:
if object_id('tempdb..#tab') is not null
drop table #tab
create table #tab (id int, date datetime, allocation int)
insert into #tab
select 1, '2012-01-01', 0 union
select 2, '2012-01-02', 2 union
select 3, '2012-01-03', 0 union
select 4, '2012-01-04', 0 union
select 5, '2012-01-05', 0 union
select 6, '2012-01-06', 5 union
select 7, '2012-01-07', 0 union
select 8, '2012-01-08', 5 union
select 9, '2012-01-09', 0 union
select 10, '2012-01-10', 0
Query:
;with cte(s_id, e_id, b_id) as (
select s.id, e.id, b.id
from #tab s
left join #tab e on dateadd(dd, 1, s.date) = e.date and e.allocation = 0
left join #tab b on dateadd(dd, -1, s.date) = b.date and b.allocation = 0
where s.allocation = 0
)
select ts.date as [start date], te.date as [end date], count(*) as [day count] from (
select c1.s_id as s, (
select min(s_id) from cte c2
where c2.e_id is null and c2.s_id >= c1.s_id
) as e
from cte c1
where b_id is null
) t
join #tab t1 on t1.id between t.s and t.e and t1.allocation = 0
join #tab ts on ts.id = t.s
join #tab te on te.id = t.e
group by t.s, t.e, ts.date, te.date
Live example at data.SE.
Using this sample data:
CREATE TABLE MyTable (ID INT, Date DATETIME, Allocation INT);
INSERT INTO MyTable VALUES (1, {d '2012-01-01'}, 0);
INSERT INTO MyTable VALUES (2, {d '2012-01-02'}, 2);
INSERT INTO MyTable VALUES (3, {d '2012-01-03'}, 0);
INSERT INTO MyTable VALUES (4, {d '2012-01-04'}, 0);
INSERT INTO MyTable VALUES (5, {d '2012-01-05'}, 0);
INSERT INTO MyTable VALUES (6, {d '2012-01-06'}, 5);
GO
Try this:
WITH DateGroups (ID, Date, Allocation, SeedID) AS (
SELECT MyTable.ID, MyTable.Date, MyTable.Allocation, MyTable.ID
FROM MyTable
LEFT JOIN MyTable Prev ON Prev.Date = DATEADD(d, -1, MyTable.Date)
AND Prev.Allocation = 0
WHERE Prev.ID IS NULL
AND MyTable.Allocation = 0
UNION ALL
SELECT MyTable.ID, MyTable.Date, MyTable.Allocation, DateGroups.SeedID
FROM MyTable
JOIN DateGroups ON MyTable.Date = DATEADD(d, 1, DateGroups.Date)
WHERE MyTable.Allocation = 0
), StartDates (ID, StartDate, DayCount) AS (
SELECT SeedID, MIN(Date), COUNT(ID)
FROM DateGroups
GROUP BY SeedID
), EndDates (ID, EndDate) AS (
SELECT SeedID, MAX(Date)
FROM DateGroups
GROUP BY SeedID
)
SELECT StartDates.StartDate, EndDates.EndDate, StartDates.DayCount
FROM StartDates
JOIN EndDates ON StartDates.ID = EndDates.ID;
The first section of the query is a recursive SELECT, which is anchored by all rows that are allocation = 0, and whose previous day either doesn't exist or has allocation != 0. This effectively returns IDs: 1 and 3 which are the starting dates of the periods of time you want to return.
The recursive part of this same query starts from the anchor rows, and finds all subsequent dates that also have allocation = 0. The SeedID keeps track of the anchored ID through all the iterations.
The result so far is this:
ID Date Allocation SeedID
----------- ----------------------- ----------- -----------
1 2012-01-01 00:00:00.000 0 1
3 2012-01-03 00:00:00.000 0 3
4 2012-01-04 00:00:00.000 0 3
5 2012-01-05 00:00:00.000 0 3
The next sub query uses a simple GROUP BY to filter out all the start dates for each SeedID, and also counts the days.
The last sub query does the same thing with the end dates, but this time the day count isn't needed as we already have this.
The final SELECT query joins these two together to combine the start and end dates, and returns them along with the day count.
Give it a try if it works for you
Here SDATE for your DATE remains same as your table.
SELECT SDATE,
CASE WHEN (SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) >0 THEN(
CASE WHEN (SELECT SDATE FROM TABLE1 WHERE ID =(SELECT MAX(ID) FROM TABLE1 WHERE ID >TBL1.ID AND ID<(SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0))) IS NULL THEN SDATE
ELSE (SELECT SDATE FROM TABLE1 WHERE ID =(SELECT MAX(ID) FROM TABLE1 WHERE ID >TBL1.ID AND ID<(SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0))) END
)ELSE (SELECT SDATE FROM TABLE1 WHERE ID = (SELECT MAX(ID) FROM TABLE1 WHERE ID > TBL1.ID ))END AS EDATE
,CASE WHEN (SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) <0 THEN
(SELECT COUNT(*) FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MAX(ID) FROM TABLE1 WHERE ID > TBL1.ID )) ELSE
(SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) END AS DAYCOUNT
FROM TABLE1 TBL1 WHERE ALLOCATION = 0
AND (((SELECT ALLOCATION FROM TABLE1 WHERE ID=(SELECT MAX(ID) FROM TABLE1 WHERE ID < TBL1.ID))<> 0 ) OR (SELECT MAX(ID) FROM TABLE1 WHERE ID < TBL1.ID)IS NULL);
A solution without CTE:
SELECT a.aDate AS StartDate
, MIN(c.aDate) AS EndDate
, (datediff(day, a.aDate, MIN(c.aDate)) + 1) AS DayCount
FROM (
SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
JOIN table1 y ON y.aDate <= x.aDate
GROUP BY x.id, x.aDate, x.allocation
) AS a
LEFT JOIN (
SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
JOIN table1 y ON y.aDate <= x.aDate
GROUP BY x.id, x.aDate, x.allocation
) AS b ON a.idn = b.idn + 1 AND b.allocation = a.allocation
LEFT JOIN (
SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
JOIN table1 y ON y.aDate <= x.aDate
GROUP BY x.id, x.aDate, x.allocation
) AS c ON a.idn <= c.idn AND c.allocation = a.allocation
LEFT JOIN (
SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
JOIN table1 y ON y.aDate <= x.aDate
GROUP BY x.id, x.aDate, x.allocation
) AS d ON c.idn = d.idn - 1 AND d.allocation = c.allocation
WHERE b.idn IS NULL AND c.idn IS NOT NULL AND d.idn IS NULL AND a.allocation = 0
GROUP BY a.aDate
Example