SQL Server : group data by 8 generated hours - sql-server
Here is my problem, I have a tickets table which stores tickets read and users work 8 hours shift. I need to group tickets read in 8 groups.
Basically I need something like
if HourStart is 15:20
Group Hour Quantity
1 15:20:00 20
2 16:20:00 20
3 17:20:00 40
4 18:20:00 0
5 19:20:00 0
6 20:20:00 0
7 21:20:00 0
8 22:20:00 0
so because i need 8 rows all the time i thought creating a temporary table would be the best so i could make a join and rows were still showing even with null data if no records were entered in those hours.
Problem is this is a bit slow in terms of performance and a bit dirty and i'm looking if there is a better way to group data by some generated rows without having to create a temporary table
CREATE TABLE Production.hProductionRecods
(
ID INT IDENTITY PRIMARY KEY,
HourStart TIME
)
CREATE TABLE Production.hTickets
(
ID INT IDENTITY PRIMARY KEY,
DateRead DATETIME,
ProductionRecordId INT
)
CREATE TABLE #TickersPerHour
(
Group INT,
Hour TIME
)
DECLARE #HourStart TIME = (SELECT HourStart
FROM Production.hProductionRecords
WHERE Id = 1)
INSERT INTO #TickersPerHour (Group, Hour)
VALUES (1, #HourStart),
(2, DATEADD(hh, 1, #HourStart)),
(3, DATEADD(hh, 2, #HourStart)),
(4, DATEADD(hh, 3, #HourStart)),
(5, DATEADD(hh, 4, #HourStart)),
(6, DATEADD(hh, 5, #HourStart)),
(7, DATEADD(hh, 6, #HourStart)),
(8, DATEADD(hh, 7, #HourStart))
SELECT
TEMP.Group,
TEMP.Hour,
ISNULL(SUM(E.Quantity),0) Quantity
FROM
Production.hProductionRecords P
LEFT JOIN
Production.hTickets E ON E.ProductionRecordId = P.Id
RIGHT JOIN
#TickersPerHour TEMP
ON TEMP.Hour = CASE
WHEN CAST(E.DateRead AS TIME) >= P.HourStart
AND CAST(E.DateRead AS TIME) < DATEADD(hour, 1, P.HourStart)
THEN DATEADD(hour, 1, P.HourStart)
WHEN CAST(E.DateRead AS TIME) >= P.HourStart
AND CAST(E.DateRead AS TIME) < DATEADD(hour, 2, P.HourStart)
THEN DATEADD(hour, 2, P.HourStart)
WHEN CAST(E.DateRead AS TIME) >= P.HourStart
AND CAST(E.DateRead AS TIME) < DATEADD(hour, 3, P.HourStart)
THEN DATEADD(hour, 3, P.HourStart)
WHEN CAST(E.DateRead AS TIME) >= P.HourStart
AND CAST(E.DateRead AS TIME) < DATEADD(hour, 4, P.HourStart)
THEN DATEADD(hour, 4, P.HourStart)
WHEN CAST(E.DateRead AS TIME) >= P.HourStart
AND CAST(E.DateRead AS TIME) < DATEADD(hour, 5, P.HourStart)
THEN DATEADD(hour,5, P.HourStart)
WHEN CAST(E.DateRead AS TIME) >= P.HourStart
AND CAST(E.DateRead AS TIME) < DATEADD(hour, 6, P.HourStart)
THEN DATEADD(hour, 6, P.HourStart)
WHEN CAST(E.DateRead AS TIME) >= P.HourStart
AND CAST(E.DateRead AS TIME) < DATEADD(hour, 7, P.HourStart)
THEN DATEADD(hour,7, P.HourStart)
WHEN CAST(E.DateRead AS TIME) >= P.HourStart
AND CAST(E.DateRead AS TIME) < DATEADD(hour, 8, P.HourStart)
THEN DATEADD(hour, 8, P.HourStart)
END
GROUP BY
TEMP.Group, TEMP.Hour
ORDER BY
Group
DROP TABLE #TickersPerHour
You could try aggregating the tickets without joining them with the temp table ranges, because the aggregation is quite static: Tickets whose minute part is before the minute of HourStart "belong" to the previous hour. The aggregation will return 8 groups and those 8 groups can be joined with the ranges (either temp table, or derived).
/*
--drop table Production.hTickets
--drop table Production.hProductionRecords
--drop schema Production
go
create schema Production
go
CREATE TABLE Production.hProductionRecords
(
ID INT IDENTITY PRIMARY KEY,
HourStart TIME
)
CREATE TABLE Production.hTickets
(
ID INT IDENTITY PRIMARY KEY,
DateRead DATETIME,
ProductionRecordId INT,
Quantity INT
)
go
insert into Production.hProductionRecords values('15:20')
insert into Production.hTickets(DateRead, ProductionRecordId, Quantity)
select dateadd(minute, abs(checksum(newid()))% 600, '15:20'), 1, abs(checksum(newid()))% 75
from sys.columns as a
cross join sys.columns as b
*/
DECLARE #HourStart TIME = (SELECT HourStart
FROM Production.hProductionRecords
WHERE Id = 1)
declare #MinuteBoundary int = datepart(minute, #HourStart);
select *
from
(
values (1, #HourStart),
(2, DATEADD(hh, 1, #HourStart)),
(3, DATEADD(hh, 2, #HourStart)),
(4, DATEADD(hh, 3, #HourStart)),
(5, DATEADD(hh, 4, #HourStart)),
(6, DATEADD(hh, 5, #HourStart)),
(7, DATEADD(hh, 6, #HourStart)),
(8, DATEADD(hh, 7, #HourStart))
) AS h(id, HourStart)
full outer join
(
select AdjustedHour, sum(Quantity) AS SumQuantity
from
(
select
--tickets read before the minute boundary, belong to the previous hour
case
when datepart(minute, DateRead) < #MinuteBoundary then datepart(hour, dateadd(hour, -1, DateRead))
else datepart(hour, DateRead)
end AS AdjustedHour,
Quantity
from Production.hTickets
where 1=1
--and --filter, for dates, hours outside the 8 hours period and whatnot...
) as src
group by AdjustedHour
) AS grp ON datepart(hour, h.HourStart) = grp.AdjustedHour;
i'm looking if there is a better way to group data by some generated
rows without having to create a temporary table
Instead of a temporary table you can build a lazy sequence, for that you need rangeAB (code at the end of this post). Lazy Sequences (AKA Tally Table or Virtual Auxiliary Table of Numbers) are nasty fast. Note this example:
DECLARE #HourStart TIME = '15:20:00';
SELECT
r.RN,
r.OP,
TimeAsc = DATEADD(HOUR,r.RN,#HourStart),
TimeDesc = DATEADD(HOUR,r.OP,#HourStart)
FROM dbo.rangeAB(0,7,1,0) AS r
ORDER BY r.RN;
Results:
RN OP TimeAsc TimeDesc
---- ---- ---------------- ----------------
0 7 15:20:00.0000000 22:20:00.0000000
1 6 16:20:00.0000000 21:20:00.0000000
2 5 17:20:00.0000000 20:20:00.0000000
3 4 18:20:00.0000000 19:20:00.0000000
4 3 19:20:00.0000000 18:20:00.0000000
5 2 20:20:00.0000000 17:20:00.0000000
6 1 21:20:00.0000000 16:20:00.0000000
7 0 22:20:00.0000000 15:20:00.0000000
Note that I am able to generate these dates in ASCending and/or DESCending order without a sort in the execution plan. This is because rangeAB leverages what I call a Virtual Index. You can order by, group by, etc, even join on the RN column without sorting. Note the execution plan - No Sort, that's huge!
Now to use RangeAB to solve you problem:
-- Create Some Sample Data
DECLARE #things TABLE (Something CHAR(1), SomeTime TIME);
WITH Something(x) AS (SELECT 1)
INSERT #things
SELECT x, xx
FROM Something
CROSS APPLY (VALUES('15:30:00'),('20:19:00'),('16:30:00'),('16:33:00'),
('17:10:00'),('18:13:00'),('19:01:00'),('21:35:00'),
('15:13:00'),('21:55:00'),('19:22:00'),('16:39:00')) AS f(xx);
-- Solution:
DECLARE #HourStart TIME = '15:20:00'; -- you get via a subquery
SELECT
GroupNumber = r.RN+1,
HourStart = MAX(f.HStart),
Quantity = COUNT(t.SomeThing)
FROM dbo.rangeAB(0,7,1,0) AS r
CROSS APPLY (VALUES(DATEADD(HOUR,r.RN,#HourStart))) AS f(HStart)
CROSS APPLY (VALUES(DATEADD(SECOND,3599,f.HStart))) AS f2(HEnd)
LEFT JOIN #things AS t
ON t.SomeTime BETWEEN f.HStart AND f2.HEnd
GROUP BY r.RN;
Results:
GroupNumber HourStart Quantity
------------- ----------------- -----------
1 15:20:00.0000000 1
2 16:20:00.0000000 4
3 17:20:00.0000000 1
4 18:20:00.0000000 1
5 19:20:00.0000000 2
6 20:20:00.0000000 0
7 21:20:00.0000000 2
8 22:20:00.0000000 0
Execution Plan:
Let me know if you have questions. dbo.rangeAB below.
CREATE FUNCTION dbo.rangeAB
(
#low bigint,
#high bigint,
#gap bigint,
#row1 bit
)
/****************************************************************************************
[Purpose]:
Creates up to 531,441,000,000 sequentia1 integers numbers beginning with #low and ending
with #high. Used to replace iterative methods such as loops, cursors and recursive CTEs
to solve SQL problems. Based on Itzik Ben-Gan's getnums function with some tweeks and
enhancements and added functionality. The logic for getting rn to begin at 0 or 1 is
based comes from Jeff Moden's fnTally function.
The name range because it's similar to clojure's range function. The name "rangeAB" as
used because "range" is a reserved SQL keyword.
[Author]: Alan Burstein
[Compatibility]:
SQL Server 2008+ and Azure SQL Database
[Syntax]:
SELECT r.RN, r.OP, r.N1, r.N2
FROM dbo.rangeAB(#low,#high,#gap,#row1) AS r;
[Parameters]:
#low = a bigint that represents the lowest value for n1.
#high = a bigint that represents the highest value for n1.
#gap = a bigint that represents how much n1 and n2 will increase each row; #gap also
represents the difference between n1 and n2.
#row1 = a bit that represents the first value of rn. When #row = 0 then rn begins
at 0, when #row = 1 then rn will begin at 1.
[Returns]:
Inline Table Valued Function returns:
rn = bigint; a row number that works just like T-SQL ROW_NUMBER() except that it can
start at 0 or 1 which is dictated by #row1.
op = bigint; returns the "opposite number that relates to rn. When rn begins with 0 and
ends with 10 then 10 is the opposite of 0, 9 the opposite of 1, etc. When rn begins
with 1 and ends with 5 then 1 is the opposite of 5, 2 the opposite of 4, etc...
n1 = bigint; a sequential number starting at the value of #low and incrementing by the
value of #gap until it is less than or equal to the value of #high.
n2 = bigint; a sequential number starting at the value of #low+#gap and incrementing
by the value of #gap.
[Dependencies]:
N/A
[Developer Notes]:
1. The lowest and highest possible numbers returned are whatever is allowable by a
bigint. The function, however, returns no more than 531,441,000,000 rows (8100^3).
2. #gap does not affect rn, rn will begin at #row1 and increase by 1 until the last row
unless its used in a query where a filter is applied to rn.
3. #gap must be greater than 0 or the function will not return any rows.
4. Keep in mind that when #row1 is 0 then the highest row-number will be the number of
rows returned minus 1
5. If you only need is a sequential set beginning at 0 or 1 then, for best performance
use the RN column. Use N1 and/or N2 when you need to begin your sequence at any
number other than 0 or 1 or if you need a gap between your sequence of numbers.
6. Although #gap is a bigint it must be a positive integer or the function will
not return any rows.
7. The function will not return any rows when one of the following conditions are true:
* any of the input parameters are NULL
* #high is less than #low
* #gap is not greater than 0
To force the function to return all NULLs instead of not returning anything you can
add the following code to the end of the query:
UNION ALL
SELECT NULL, NULL, NULL, NULL
WHERE NOT (#high&#low&#gap&#row1 IS NOT NULL AND #high >= #low AND #gap > 0)
This code was excluded as it adds a ~5% performance penalty.
8. There is no performance penalty for sorting by rn ASC; there is a large performance
penalty for sorting in descending order WHEN #row1 = 1; WHEN #row1 = 0
If you need a descending sort the use op in place of rn then sort by rn ASC.
Best Practices:
--===== 1. Using RN (rownumber)
-- (1.1) The best way to get the numbers 1,2,3...#high (e.g. 1 to 5):
SELECT RN FROM dbo.rangeAB(1,5,1,1);
-- (1.2) The best way to get the numbers 0,1,2...#high-1 (e.g. 0 to 5):
SELECT RN FROM dbo.rangeAB(0,5,1,0);
--===== 2. Using OP for descending sorts without a performance penalty
-- (2.1) The best way to get the numbers 5,4,3...#high (e.g. 5 to 1):
SELECT op FROM dbo.rangeAB(1,5,1,1) ORDER BY rn ASC;
-- (2.2) The best way to get the numbers 0,1,2...#high-1 (e.g. 5 to 0):
SELECT op FROM dbo.rangeAB(1,6,1,0) ORDER BY rn ASC;
--===== 3. Using N1
-- (3.1) To begin with numbers other than 0 or 1 use N1 (e.g. -3 to 3):
SELECT N1 FROM dbo.rangeAB(-3,3,1,1);
-- (3.2) ROW_NUMBER() is built in. If you want a ROW_NUMBER() include RN:
SELECT RN, N1 FROM dbo.rangeAB(-3,3,1,1);
-- (3.3) If you wanted a ROW_NUMBER() that started at 0 you would do this:
SELECT RN, N1 FROM dbo.rangeAB(-3,3,1,0);
--===== 4. Using N2 and #gap
-- (4.1) To get 0,10,20,30...100, set #low to 0, #high to 100 and #gap to 10:
SELECT N1 FROM dbo.rangeAB(0,100,10,1);
-- (4.2) Note that N2=N1+#gap; this allows you to create a sequence of ranges.
-- For example, to get (0,10),(10,20),(20,30).... (90,100):
SELECT N1, N2 FROM dbo.rangeAB(0,90,10,1);
-- (4.3) Remember that a rownumber is included and it can begin at 0 or 1:
SELECT RN, N1, N2 FROM dbo.rangeAB(0,90,10,1);
[Examples]:
--===== 1. Generating Sample data (using rangeAB to create "dummy rows")
-- The query below will generate 10,000 ids and random numbers between 50,000 and 500,000
SELECT
someId = r.rn,
someNumer = ABS(CHECKSUM(NEWID())%450000)+50001
FROM rangeAB(1,10000,1,1) r;
--===== 2. Create a series of dates; rn is 0 to include the first date in the series
DECLARE #startdate DATE = '20180101', #enddate DATE = '20180131';
SELECT r.rn, calDate = DATEADD(dd, r.rn, #startdate)
FROM dbo.rangeAB(1, DATEDIFF(dd,#startdate,#enddate),1,0) r;
GO
--===== 3. Splitting (tokenizing) a string with fixed sized items
-- given a delimited string of identifiers that are always 7 characters long
DECLARE #string VARCHAR(1000) = 'A601225,B435223,G008081,R678567';
SELECT
itemNumber = r.rn, -- item's ordinal position
itemIndex = r.n1, -- item's position in the string (it's CHARINDEX value)
item = SUBSTRING(#string, r.n1, 7) -- item (token)
FROM dbo.rangeAB(1, LEN(#string), 8,1) r;
GO
--===== 4. Splitting (tokenizing) a string with random delimiters
DECLARE #string VARCHAR(1000) = 'ABC123,999F,XX,9994443335';
SELECT
itemNumber = ROW_NUMBER() OVER (ORDER BY r.rn), -- item's ordinal position
itemIndex = r.n1+1, -- item's position in the string (it's CHARINDEX value)
item = SUBSTRING
(
#string,
r.n1+1,
ISNULL(NULLIF(CHARINDEX(',',#string,r.n1+1),0)-r.n1-1, 8000)
) -- item (token)
FROM dbo.rangeAB(0,DATALENGTH(#string),1,1) r
WHERE SUBSTRING(#string,r.n1,1) = ',' OR r.n1 = 0;
-- logic borrowed from: http://www.sqlservercentral.com/articles/Tally+Table/72993/
--===== 5. Grouping by a weekly intervals
-- 5.1. how to create a series of start/end dates between #startDate & #endDate
DECLARE #startDate DATE = '1/1/2015', #endDate DATE = '2/1/2015';
SELECT
WeekNbr = r.RN,
WeekStart = DATEADD(DAY,r.N1,#StartDate),
WeekEnd = DATEADD(DAY,r.N2-1,#StartDate)
FROM dbo.rangeAB(0,datediff(DAY,#StartDate,#EndDate),7,1) r;
GO
-- 5.2. LEFT JOIN to the weekly interval table
BEGIN
DECLARE #startDate datetime = '1/1/2015', #endDate datetime = '2/1/2015';
-- sample data
DECLARE #loans TABLE (loID INT, lockDate DATE);
INSERT #loans SELECT r.rn, DATEADD(dd, ABS(CHECKSUM(NEWID())%32), #startDate)
FROM dbo.rangeAB(1,50,1,1) r;
-- solution
SELECT
WeekNbr = r.RN,
WeekStart = dt.WeekStart,
WeekEnd = dt.WeekEnd,
total = COUNT(l.lockDate)
FROM dbo.rangeAB(0,datediff(DAY,#StartDate,#EndDate),7,1) r
CROSS APPLY (VALUES (
CAST(DATEADD(DAY,r.N1,#StartDate) AS DATE),
CAST(DATEADD(DAY,r.N2-1,#StartDate) AS DATE))) dt(WeekStart,WeekEnd)
LEFT JOIN #loans l ON l.lockDate BETWEEN dt.WeekStart AND dt.WeekEnd
GROUP BY r.RN, dt.WeekStart, dt.WeekEnd ;
END;
--===== 6. Identify the first vowel and last vowel in a along with their positions
DECLARE #string VARCHAR(200) = 'This string has vowels';
SELECT TOP(1) position = r.rn, letter = SUBSTRING(#string,r.rn,1)
FROM dbo.rangeAB(1,LEN(#string),1,1) r
WHERE SUBSTRING(#string,r.rn,1) LIKE '%[aeiou]%'
ORDER BY r.rn;
-- To avoid a sort in the execution plan we'll use op instead of rn
SELECT TOP(1) position = r.op, letter = SUBSTRING(#string,r.op,1)
FROM dbo.rangeAB(1,LEN(#string),1,1) r
WHERE SUBSTRING(#string,r.rn,1) LIKE '%[aeiou]%'
ORDER BY r.rn;
---------------------------------------------------------------------------------------
[Revision History]:
Rev 00 - 20140518 - Initial Development - Alan Burstein
Rev 01 - 20151029 - Added 65 rows to make L1=465; 465^3=100.5M. Updated comment section
- Alan Burstein
Rev 02 - 20180613 - Complete re-design including opposite number column (op)
Rev 03 - 20180920 - Added additional CROSS JOIN to L2 for 530B rows max - Alan Burstein
****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING AS RETURN
WITH L1(N) AS
(
SELECT 1
FROM (VALUES
(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
(0),(0)) T(N) -- 90 values
),
L2(N) AS (SELECT 1 FROM L1 a CROSS JOIN L1 b CROSS JOIN L1 c),
iTally AS (SELECT rn = ROW_NUMBER() OVER (ORDER BY (SELECT 1)) FROM L2 a CROSS JOIN L2 b)
SELECT
r.RN,
r.OP,
r.N1,
r.N2
FROM
(
SELECT
RN = 0,
OP = (#high-#low)/#gap,
N1 = #low,
N2 = #gap+#low
WHERE #row1 = 0
UNION ALL -- ISNULL required in the TOP statement below for error handling purposes
SELECT TOP (ABS((ISNULL(#high,0)-ISNULL(#low,0))/ISNULL(#gap,0)+ISNULL(#row1,1)))
RN = i.rn,
OP = (#high-#low)/#gap+(2*#row1)-i.rn,
N1 = (i.rn-#row1)*#gap+#low,
N2 = (i.rn-(#row1-1))*#gap+#low
FROM iTally AS i
ORDER BY i.rn
) AS r
WHERE #high&#low&#gap&#row1 IS NOT NULL AND #high >= #low AND #gap > 0;
GO
Related
Is it possible to use the SQL DATEADD function but exclude dates from a table in the calculation?
Is it possible to use the DATEADD function but exclude dates from a table? We already have a table with all dates we need to exclude. Basically, I need to add number of days to a date but exclude dates within a table. Example: Add 5 days to 01/08/2021. Dates 03/08/2021 and 04/08/2021 exist in the exclusion table. So, resultant date should be: 08/08/2021. Thank you
A bit of a "wonky" solution, but it works. Firstly we use a tally to create a Calendar table of dates, that exclude your dates in the table, then we get the nth row, where n is the number of days to add: DECLARE #DaysToAdd int = 5, #StartDate date = '20210801'; WITH N AS( SELECT N FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)), Tally AS( SELECT 0 AS I UNION ALL SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I FROM N N1, N N2, N N3), --Up to 1,000 Calendar AS( SELECT DATEADD(DAY,T.I, #StartDate) AS D, ROW_NUMBER() OVER (ORDER BY T.I) AS I FROM Tally T WHERE NOT EXISTS (SELECT 1 FROM dbo.DatesTable DT WHERE DT.YourDate = DATEADD(DAY,T.I, #StartDate))) SELECT D FROM Calendar WHERE I = #DaysToAdd+1;
A best solution is probably a calendar table. But if you're willing to traverse through every date, then a recursive CTE can work. It would require tracking the total iterations and another column to substract if any traversed date was in the table. The exit condition uses the total difference. An example dataset would be: CREATE TABLE mytable(mydate date); INSERT INTO mytable VALUES ('20210803'), ('20210804'); And an example function run in it's own batch: ALTER FUNCTION dbo.fn_getDays (#mydate date, #daysadd int) RETURNS date AS BEGIN DECLARE #newdate date; WITH CTE(num, diff, mydate) AS ( SELECT 0 AS [num] ,0 AS [diff] ,DATEADD(DAY, 0, #mydate) [mydate] UNION ALL SELECT num + 1 AS [num] ,CTE.diff + CASE WHEN DATEADD(DAY, num+1, #mydate) IN (SELECT mydate FROM mytable) THEN 0 ELSE 1 END AS [diff] ,DATEADD(DAY, num+1, #mydate) [mydate] FROM CTE WHERE (CTE.diff + CASE WHEN DATEADD(DAY, num+1, #mydate) IN (SELECT mydate FROM mytable) THEN 0 ELSE 1 END) <= #daysadd ) SELECT #newdate = (SELECT MAX(mydate) AS [mydate] FROM CTE); RETURN #newdate; END Running the function: SELECT dbo.fn_getDays('20210801', 5) Produces output, which is the MAX(mydate) from the function: ---------- 2021-08-08 For reference the MAX(mydate) is taken from this dataset: n diff mydate ----------- ----------- ---------- 0 0 2021-08-01 1 1 2021-08-02 2 1 2021-08-03 3 1 2021-08-04 4 2 2021-08-05 5 3 2021-08-06 6 4 2021-08-07 7 5 2021-08-08
You can use the IN clause. To perform the test, I used a W3Schools Test DB SELECT DATE_ADD(BirthDate, INTERVAL 10 DAY) FROM Employees WHERE FirstName NOT IN (Select FirstName FROM Employees WHERE FirstName LIKE 'N%') This query shows all the birth dates + 10 days except for the only employee with name starting with N (Nancy)
How to generate calendar table having begin month date and end month Date
I need to generate calendar table between two dates having begging month date and end month date. And if its greater than today, then it should stop at current date. Should look like this: As you can see the last value for column Eomonth has today's date (not the end of the month) Thank you
If you don't have a calendar table, you can use an ad-hoc tally table Example Declare #Date1 date = '2018-01-01' Declare #Date2 date = GetDate() Select [Month] = D ,[Eomonth] = case when EOMONTH(D)>#Date2 then convert(date,GetDate()) else EOMONTH(D) end From ( Select Top (DateDiff(Month,#Date1,#Date2)+1) D=DateAdd(Month,-1+Row_Number() Over (Order By (Select Null)),#Date1) From master..spt_values n1 ) A Returns Month Eomonth 2018-01-01 2018-01-31 2018-02-01 2018-02-28 2018-03-01 2018-03-31 2018-04-01 2018-04-30 2018-05-01 2018-05-31 2018-06-01 2018-06-30 2018-07-01 2018-07-31 2018-08-01 2018-08-31 2018-09-01 2018-09-30 2018-10-01 2018-10-31 2018-11-01 2018-11-30 2018-12-01 2018-12-31 2019-01-01 2019-01-31 2019-02-01 2019-02-13 <-- Today's date
Just to build on John's excellent answer... I made one change to his solution: Declare #Date1 date = '2018-01-01' Declare #Date2 date = '2018-03-02';--GETDATE() -- BEFORE SELECT [Month] = D ,[Eomonth] = case when EOMONTH(D)>#Date2 then convert(date,GetDate()) else EOMONTH(D) end FROM ( Select Top (DateDiff(Month,#Date1,#Date2)+1) D=DateAdd(Month,-1+Row_Number() Over (Order By (Select Null)),#Date1) From master..spt_values n1 ) A ORDER BY A.D; -- AFTER SELECT [Month] = D ,[Eomonth] = case when EOMONTH(D)>#Date2 then convert(date,GetDate()) else EOMONTH(D) end FROM ( Select Top (DateDiff(Month,#Date1,#Date2)+1) D=DateAdd(Month,-1+Row_Number() Over (Order By (Select Null)),#Date1), RN=ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) From master..spt_values n1 ) A ORDER BY A.RN GO Now the execution plans: How did we remove that sort? By leveraging, what I refer to as a virtual index. ROW_NUMBER returns an Ordered Stream of Numbers. This is why you can have column called RN defined as RN = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) and add an ORDER BY RN statement which does not cause a sort. It appears that all Window Ranking Functions do this (RANK, DENSE_RANK, NTILE and ROW_NUMBER). With this in mind let's examine my solution which leverages dbo.RangeAB, a function that fully exploits the power of the virtual index. -- 1. Solution DECLARE #startDate DATE = '2018-06-01' DECLARE #endDate DATE = '2019-02-21'; --GETDATE() SELECT f.Dt, dt.Mx FROM (VALUES(CAST(GETDATE() AS DATE))) AS x(Dt) CROSS APPLY (VALUES(IIF(#endDate<x.Dt,#endDate,x.Dt))) AS d(Mx) CROSS APPLY dates.ageInMonths(#startDate,d.Mx) AS m CROSS APPLY dbo.RangeAB(0,m.Months,1,0) AS r CROSS APPLY (VALUES(DATEADD(MONTH,r.RN,#startDate))) AS f(Dt) CROSS APPLY (VALUES(IIF(EOMONTH(f.Dt)>d.Mx,d.Mx,EOMONTH(f.Dt)))) AS dt(Mx) ORDER BY r.RN; -- not required; included to demo the virtual index GO Returns: Dt Mx ---------- ---------- 2018-06-01 2018-06-30 2018-07-01 2018-07-31 2018-08-01 2018-08-31 2018-09-01 2018-09-30 2018-10-01 2018-10-31 2018-11-01 2018-11-30 2018-12-01 2018-12-31 2019-01-01 2019-01-31 2019-02-01 2019-02-14 If you check the execution plan you will not see a sort despite my order by. But what about Descending Sorts? The ROW_NUMBER virtual index does not handle DESCending sorts? If you change the above query to ORDER BY r.RN DESC you will see a sort in the execution plan. To change that we simply change the reference of r.RN to r.OP. r.OP is ROW_NUMBER's opposite number. Let's compare these two queries that return the same results. What I'm doing here is returning the most recent five months: DECLARE #startDate DATE = '2018-06-01' DECLARE #endDate DATE = '2019-01-21'; --GETDATE() -- INCORRECT!!! SELECT TOP (5) f.Dt, dt.Mx FROM (VALUES(CAST(GETDATE() AS DATE))) AS x(Dt) CROSS APPLY (VALUES(IIF(#endDate<x.Dt,#endDate,x.Dt))) AS d(Mx) CROSS APPLY dates.ageInMonths(#startDate,d.Mx) AS m CROSS APPLY dbo.RangeAB(0,m.Months,1,0) AS r CROSS APPLY (VALUES(DATEADD(MONTH,r.RN,#startDate))) AS f(Dt) CROSS APPLY (VALUES(IIF(EOMONTH(f.Dt)>d.Mx,d.Mx,EOMONTH(f.Dt)))) AS dt(Mx) ORDER BY r.RN DESC; -- the virtual index cannot handle Descending sorts, this will sort! -- CORRECT -- ONE TINY CHANGE! CHANGE r.R1 to r.OP SELECT TOP (5) f.Dt, dt.Mx FROM (VALUES(CAST(GETDATE() AS DATE))) AS x(Dt) CROSS APPLY (VALUES(IIF(#endDate<x.Dt,#endDate,x.Dt))) AS d(Mx) CROSS APPLY dates.ageInMonths(#startDate,d.Mx) AS m CROSS APPLY dbo.RangeAB(0,m.Months,1,0) AS r CROSS APPLY (VALUES(DATEADD(MONTH,r.OP,#startDate))) AS f(Dt) CROSS APPLY (VALUES(IIF(EOMONTH(f.Dt)>d.Mx,d.Mx,EOMONTH(f.Dt)))) AS dt(Mx) ORDER BY r.RN; And the execution plans: Leveraging what I call Finite Opposite Numbers I am able to use ROW_NUMBER's (RN) Opposite Number (OP) I can return the numbers in reverse order while still sorting by RN in Ascending order. With OP, dbo.rangeAB's finite opposite number column, you can make full use of the virtual index instead of only for ASCending sorts. The DDL for the functions I used is below. RangeAB CREATE FUNCTION dbo.rangeAB ( #low bigint, #high bigint, #gap bigint, #row1 bit ) /**************************************************************************************** [Purpose]: Creates up to 531,441,000,000 sequentia1 integers numbers beginning with #low and ending with #high. Used to replace iterative methods such as loops, cursors and recursive CTEs to solve SQL problems. Based on Itzik Ben-Gan's getnums function with some tweeks and enhancements and added functionality. The logic for getting rn to begin at 0 or 1 is based comes from Jeff Moden's fnTally function. The name range because it's similar to clojure's range function. The name "rangeAB" as used because "range" is a reserved SQL keyword. [Author]: Alan Burstein [Compatibility]: SQL Server 2008+ and Azure SQL Database [Syntax]: SELECT r.RN, r.OP, r.N1, r.N2 FROM dbo.rangeAB(#low,#high,#gap,#row1) AS r; [Parameters]: #low = a bigint that represents the lowest value for n1. #high = a bigint that represents the highest value for n1. #gap = a bigint that represents how much n1 and n2 will increase each row; #gap also represents the difference between n1 and n2. #row1 = a bit that represents the first value of rn. When #row = 0 then rn begins at 0, when #row = 1 then rn will begin at 1. [Returns]: Inline Table Valued Function returns: rn = bigint; a row number that works just like T-SQL ROW_NUMBER() except that it can start at 0 or 1 which is dictated by #row1. op = bigint; returns the "opposite number that relates to rn. When rn begins with 0 and ends with 10 then 10 is the opposite of 0, 9 the opposite of 1, etc. When rn begins with 1 and ends with 5 then 1 is the opposite of 5, 2 the opposite of 4, etc... n1 = bigint; a sequential number starting at the value of #low and incrimentingby the value of #gap until it is less than or equal to the value of #high. n2 = bigint; a sequential number starting at the value of #low+#gap and incrimenting by the value of #gap. [Dependencies]: N/A [Developer Notes]: 1. The lowest and highest possible numbers returned are whatever is allowable by a bigint. The function, however, returns no more than 531,441,000,000 rows (8100^3). 2. #gap does not affect rn, rn will begin at #row1 and increase by 1 until the last row unless its used in a query where a filter is applied to rn. 3. #gap must be greater than 0 or the function will not return any rows. 4. Keep in mind that when #row1 is 0 then the highest row-number will be the number of rows returned minus 1 5. If you only need is a sequential set beginning at 0 or 1 then, for best performance use the RN column. Use N1 and/or N2 when you need to begin your sequence at any number other than 0 or 1 or if you need a gap between your sequence of numbers. 6. Although #gap is a bigint it must be a positive integer or the function will not return any rows. 7. The function will not return any rows when one of the following conditions are true: * any of the input parameters are NULL * #high is less than #low * #gap is not greater than 0 To force the function to return all NULLs instead of not returning anything you can add the following code to the end of the query: UNION ALL SELECT NULL, NULL, NULL, NULL WHERE NOT (#high&#low&#gap&#row1 IS NOT NULL AND #high >= #low AND #gap > 0) This code was excluded as it adds a ~5% performance penalty. 8. There is no performance penalty for sorting by rn ASC; there is a large performance penalty for sorting in descending order WHEN #row1 = 1; WHEN #row1 = 0 If you need a descending sort the use op in place of rn then sort by rn ASC. Best Practices: --===== 1. Using RN (rownumber) -- (1.1) The best way to get the numbers 1,2,3...#high (e.g. 1 to 5): SELECT RN FROM dbo.rangeAB(1,5,1,1); -- (1.2) The best way to get the numbers 0,1,2...#high-1 (e.g. 0 to 5): SELECT RN FROM dbo.rangeAB(0,5,1,0); --===== 2. Using OP for descending sorts without a performance penalty -- (2.1) The best way to get the numbers 5,4,3...#high (e.g. 5 to 1): SELECT op FROM dbo.rangeAB(1,5,1,1) ORDER BY rn ASC; -- (2.2) The best way to get the numbers 0,1,2...#high-1 (e.g. 5 to 0): SELECT op FROM dbo.rangeAB(1,6,1,0) ORDER BY rn ASC; --===== 3. Using N1 -- (3.1) To begin with numbers other than 0 or 1 use N1 (e.g. -3 to 3): SELECT N1 FROM dbo.rangeAB(-3,3,1,1); -- (3.2) ROW_NUMBER() is built in. If you want a ROW_NUMBER() include RN: SELECT RN, N1 FROM dbo.rangeAB(-3,3,1,1); -- (3.3) If you wanted a ROW_NUMBER() that started at 0 you would do this: SELECT RN, N1 FROM dbo.rangeAB(-3,3,1,0); --===== 4. Using N2 and #gap -- (4.1) To get 0,10,20,30...100, set #low to 0, #high to 100 and #gap to 10: SELECT N1 FROM dbo.rangeAB(0,100,10,1); -- (4.2) Note that N2=N1+#gap; this allows you to create a sequence of ranges. -- For example, to get (0,10),(10,20),(20,30).... (90,100): SELECT N1, N2 FROM dbo.rangeAB(0,90,10,1); -- (4.3) Remember that a rownumber is included and it can begin at 0 or 1: SELECT RN, N1, N2 FROM dbo.rangeAB(0,90,10,1); [Examples]: --===== 1. Generating Sample data (using rangeAB to create "dummy rows") -- The query below will generate 10,000 ids and random numbers between 50,000 and 500,000 SELECT someId = r.rn, someNumer = ABS(CHECKSUM(NEWID())%450000)+50001 FROM rangeAB(1,10000,1,1) r; --===== 2. Create a series of dates; rn is 0 to include the first date in the series DECLARE #startdate DATE = '20180101', #enddate DATE = '20180131'; SELECT r.rn, calDate = DATEADD(dd, r.rn, #startdate) FROM dbo.rangeAB(1, DATEDIFF(dd,#startdate,#enddate),1,0) r; GO --===== 3. Splitting (tokenizing) a string with fixed sized items -- given a delimited string of identifiers that are always 7 characters long DECLARE #string VARCHAR(1000) = 'A601225,B435223,G008081,R678567'; SELECT itemNumber = r.rn, -- item's ordinal position itemIndex = r.n1, -- item's position in the string (it's CHARINDEX value) item = SUBSTRING(#string, r.n1, 7) -- item (token) FROM dbo.rangeAB(1, LEN(#string), 8,1) r; GO --===== 4. Splitting (tokenizing) a string with random delimiters DECLARE #string VARCHAR(1000) = 'ABC123,999F,XX,9994443335'; SELECT itemNumber = ROW_NUMBER() OVER (ORDER BY r.rn), -- item's ordinal position itemIndex = r.n1+1, -- item's position in the string (it's CHARINDEX value) item = SUBSTRING ( #string, r.n1+1, ISNULL(NULLIF(CHARINDEX(',',#string,r.n1+1),0)-r.n1-1, 8000) ) -- item (token) FROM dbo.rangeAB(0,DATALENGTH(#string),1,1) r WHERE SUBSTRING(#string,r.n1,1) = ',' OR r.n1 = 0; -- logic borrowed from: http://www.sqlservercentral.com/articles/Tally+Table/72993/ --===== 5. Grouping by a weekly intervals -- 5.1. how to create a series of start/end dates between #startDate & #endDate DECLARE #startDate DATE = '1/1/2015', #endDate DATE = '2/1/2015'; SELECT WeekNbr = r.RN, WeekStart = DATEADD(DAY,r.N1,#StartDate), WeekEnd = DATEADD(DAY,r.N2-1,#StartDate) FROM dbo.rangeAB(0,datediff(DAY,#StartDate,#EndDate),7,1) r; GO -- 5.2. LEFT JOIN to the weekly interval table BEGIN DECLARE #startDate datetime = '1/1/2015', #endDate datetime = '2/1/2015'; -- sample data DECLARE #loans TABLE (loID INT, lockDate DATE); INSERT #loans SELECT r.rn, DATEADD(dd, ABS(CHECKSUM(NEWID())%32), #startDate) FROM dbo.rangeAB(1,50,1,1) r; -- solution SELECT WeekNbr = r.RN, WeekStart = dt.WeekStart, WeekEnd = dt.WeekEnd, total = COUNT(l.lockDate) FROM dbo.rangeAB(0,datediff(DAY,#StartDate,#EndDate),7,1) r CROSS APPLY (VALUES ( CAST(DATEADD(DAY,r.N1,#StartDate) AS DATE), CAST(DATEADD(DAY,r.N2-1,#StartDate) AS DATE))) dt(WeekStart,WeekEnd) LEFT JOIN #loans l ON l.lockDate BETWEEN dt.WeekStart AND dt.WeekEnd GROUP BY r.RN, dt.WeekStart, dt.WeekEnd ; END; --===== 6. Identify the first vowel and last vowel in a along with their positions DECLARE #string VARCHAR(200) = 'This string has vowels'; SELECT TOP(1) position = r.rn, letter = SUBSTRING(#string,r.rn,1) FROM dbo.rangeAB(1,LEN(#string),1,1) r WHERE SUBSTRING(#string,r.rn,1) LIKE '%[aeiou]%' ORDER BY r.rn; -- To avoid a sort in the execution plan we'll use op instead of rn SELECT TOP(1) position = r.op, letter = SUBSTRING(#string,r.op,1) FROM dbo.rangeAB(1,LEN(#string),1,1) r WHERE SUBSTRING(#string,r.rn,1) LIKE '%[aeiou]%' ORDER BY r.rn; --------------------------------------------------------------------------------------- [Revision History]: Rev 00 - 20140518 - Initial Development - Alan Burstein Rev 01 - 20151029 - Added 65 rows to make L1=465; 465^3=100.5M. Updated comment section - Alan Burstein Rev 02 - 20180613 - Complete re-design including opposite number column (op) Rev 03 - 20180920 - Added additional CROSS JOIN to L2 for 530B rows max - Alan Burstein ****************************************************************************************/ RETURNS TABLE WITH SCHEMABINDING AS RETURN WITH L1(N) AS ( SELECT 1 FROM (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0), (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0), (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0), (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0), (0),(0)) T(N) -- 90 values ), L2(N) AS (SELECT 1 FROM L1 a CROSS JOIN L1 b CROSS JOIN L1 c), iTally AS (SELECT rn = ROW_NUMBER() OVER (ORDER BY (SELECT 1)) FROM L2 a CROSS JOIN L2 b) SELECT r.RN, r.OP, r.N1, r.N2 FROM ( SELECT RN = 0, OP = (#high-#low)/#gap, N1 = #low, N2 = #gap+#low WHERE #row1 = 0 UNION ALL -- COALESCE required in the TOP statement below for error handling purposes SELECT TOP (ABS((COALESCE(#high,0)-COALESCE(#low,0))/COALESCE(#gap,0)+COALESCE(#row1,1))) RN = i.rn, OP = (#high-#low)/#gap+(2*#row1)-i.rn, N1 = (i.rn-#row1)*#gap+#low, N2 = (i.rn-(#row1-1))*#gap+#low FROM iTally AS i ORDER BY i.rn ) AS r WHERE #high&#low&#gap&#row1 IS NOT NULL AND #high >= #low AND #gap > 0; date.AgeInMonths CREATE FUNCTION dates.ageInMonths(#startDate DATETIME, #endDate DATETIME) /***************************************************************************************** [Purpose]: Calculates the number of months between #startDate and #endDate. This is something that cannot be done using DATEDIFF. Note how the following query returns a "1": SELECT DATEDIFF(MM,'Dec 30 2001', 'Jan 3 2002'); -- Returns 1 [Compatibility]: SQL Server 2005+ [Syntax]: --===== Autonomous SELECT f.months FROM dates.ageInMonths(#startDate, #endDate) f; --===== Against a table using APPLY SELECT t.*, f.months FROM dbo.someTable t FROM dates.ageInMonths(t.col1, t.col2) f; [Parameters]: #startDate = datetime; first date to compare #endDate = datetime; date to compare #startDate to [Returns]: Inline Table Valued Function returns: months = int; number of months between #startdate and #enddate [Developer Notes]: 1. NULL when either input parameter is NULL, 2. This function is what is referred to as an "inline" scalar UDF." Technically it's an inline table valued function (iTVF) but performs the same task as a scalar valued user defined function (UDF); the difference is that it requires the APPLY table operator to accept column values as a parameter. For more about "inline" scalar UDFs see this article by SQL MVP Jeff Moden: http://www.sqlservercentral.com/articles/T-SQL/91724/ and for more about how to use APPLY see the this article by SQL MVP Paul White: http://www.sqlservercentral.com/articles/APPLY/69953/. Note the above syntax example and usage examples below to better understand how to use the function. Although the function is slightly more complicated to use than a scalar UDF it will yield notably better performance for many reasons. For example, unlike a scalar UDFs or multi-line table valued functions, the inline scalar UDF does not restrict the query optimizer's ability generate a parallel query execution plan. 3. ageInMonths requires that #enddate be equal to or later than #startDate. Otherwise a NULL is returned. 4. ageInMonths is deterministic. For more deterministic functions see: https://msdn.microsoft.com/en-us/library/ms178091.aspx [Examples]: --===== 1. Basic Use SELECT a.months FROM dates.ageInMonths('20120109', '20180108') a --===== 2. Against a table DECLARE #sometable TABLE (date1 date, date2 date); BEGIN INSERT #sometable VALUES ('20111114','20111209'),('20090401','20110506'),('20091101','20160511'); SELECT t.date1, t.date2, a.months FROM #sometable t CROSS APPLY dates.ageInMonths(t.date1, t.date2) a; END ----------------------------------------------------------------------------------------- [Revision History]: Rev 00 - 20180624 - Initial Creation - Alan Burstein *****************************************************************************************/ RETURNS TABLE WITH SCHEMABINDING AS RETURN SELECT months = CASE WHEN SIGN(DATEDIFF(dd,#startDate,#endDate)) > -1 THEN DATEDIFF(month,#startDate,#endDate) - CASE WHEN DATEPART(dd,#startDate) > DATEPART(dd,#endDate) THEN 1 ELSE 0 END END; Note that you will have to adjust for the custom schema's I use (e.g. change to DBO)
Count the number of specific days of the month that have passed since a given date
I'm writing a function in SQL Server 2012 that will need to know the number of 3 specific days of the month that have passed since a given date. I can do this with a while loop, but its slow and I was looking for a better way. Here is what I have so far: Let's assume that GETDATE() = '11/14/2016' and #productDate = '10/1/2016' --Get the number of "units" that have passed since the date on the label DECLARE #unitCount INT = 0; DECLARE #countingDate DATE SET #countingDate = DATEADD(DAY,1,#productDate);--add 1 to prevent counting the date on the label as the first unit WHILE (#countingDate < CAST(GETDATE() As date )) BEGIN SELECT #unitCount = #unitCount + CASE WHEN DAY(#countingDate) = 1 OR DAY(#countingDate) = 10 OR DAY(#countingDate) = 20 THEN 1 ELSE 0 END SET #countingDate = DATEADD(DAY,1,#countingDate); END This will result in #unitCount = 4 GETDATE() of '11/20/2016' would result in #unitCount = 5
Without using a numbers table create function dbo.fn_DateCounter ( #datefrom date, #dateto date ) returns int as begin return -- number of complete months 3 * ( (DATEPART(YYYY, #dateto) * 12 + DATEPART(MM, #dateto)) -(DATEPART(YYYY, #datefrom) * 12 + DATEPART(MM, #datefrom)) - 1 ) -- add on the extras from the first month + case when DATEPART(DD, #datefrom) < 10 then 2 when DATEPART(DD, #datefrom) < 20 then 1 else 0 end -- add on the extras from the last month + case when DATEPART(DD, #dateto) > 20 then 3 when DATEPART(DD, #dateto) > 10 then 2 else 1 end end go select dbo.fn_DateCounter('01-jan-2000','01-jan-2000') -- 0 select dbo.fn_DateCounter('01-jan-2000','10-jan-2000') -- 0 select dbo.fn_DateCounter('01-jan-2000','11-jan-2000') -- 1 select dbo.fn_DateCounter('01-jan-2000','20-jan-2000') -- 1 select dbo.fn_DateCounter('01-jan-2000','21-jan-2000') -- 2 select dbo.fn_DateCounter('11-jan-2000','21-jan-2000') -- 1 select dbo.fn_DateCounter('11-jan-2000','21-feb-2000') -- 4 select dbo.fn_DateCounter('01-jan-2000','01-jan-2001') -- 36 select dbo.fn_DateCounter('01-jan-2000','11-jan-2001') -- 37
You can use a combination of sum, case, and the dbo.spt_values table: declare #productDate datetime = '11/01/2016', #unitCount int ;with nums as ( -- use a CTE to build a number list select top 1000 number from master..spt_values ) select #unitCount = sum( case when day(dateadd(day, n, #productDate)) in (1, 10, 20) then 1 else 0 end ) -- add 1 for each 1,10,20 we find from ( select n = row_number() over (order by nums.number) from nums cross join nums as num -- 1000*1000 = 1 million rows ) n where dateadd(day, n, #productDate) < getdate() select #unitCount This will grab each date between #productDate and getdate(). The case statement will select 1 for each 1/10/20, and 0 for every other date. Finally, we take the sum of the result. For 11/1 - 11/11, it returns 1. For 1/1 - 11/11, the result is 31. EDIT: In the CTE (with nums as...), we select 1-1000, and then we do a cross join which gives us a million records to work with. The answer is still limited, but now you can go ~2700 years with this.
Counting records in a subquery
I have a table with records Holding patrols of guards in SQL Server 2008R2. Whenever a duty starts a new alert number is created and within this alert number there a patrols with a starting time. Per 12 hours we can bill a flat rate when at least one patrol has been performed. When under the same alert number the 12 hour range is exceeded, a further flat rate has to be billed. The calculation of the 12 hours starts with the time of the first patrol. I tried with a temp table but could not solve it so far. DECLARE #t1 TABLE ( AlertNo INT, Starttime SMALLDATETIME, Endtime SMALLDATETIME ) INSERT INTO #t1 (AlertNo, Starttime, Endtime) SELECT AlertNo, Starttimepatrol, DATEADD(HOUR, 12, Starttimepatrol) FROM tblAllPatrols WHERE PatrolNo = 1 SELECT AlertNo, ( SELECT COUNT(*) FROM [tblAllPatrols] a INNER JOIN #t1 b ON b.AlertNo = a.AlertNo WHERE a.Starttimepatrol BETWEEN b.Starttime AND b.Endtime ) AS patrols FROM [vwAlleDatensaetze] GROUP BY AlertNo I know that this is not the end of the Story, but as I cannot even count the numbers of patrols I cannot find a way to solve the Problem. It should somehow "group" the patrols over 12-hour ranges per alert number and then count how many groups exists under the same alert number. Hope, someone of you can lead me to the result I Need. Thanks your help Michael
Try this, it assumes that after the first patrol the billing period is a multiple of 8 hours from this time: SQL Fiddle MS SQL Server 2008 Schema Setup: Query 1: DECLARE #Patrols TABLE ( AlertNo INT IDENTITY PRIMARY KEY, StartTime DateTime ) INSERT INTO #Patrols (StartTime) VALUES ('20160126 09:57'), ('20160126 10:21'), ('20160126 19:54'), ('20160126 23:21'), ('20160127 08:13'), ('20160127 16:43'), ('20160128 07:33') ;WITH FirstBillingPeriodCTE AS ( SELECT MIN(StartTime) as BillingStartTime, DateAdd(HOUR, 12, MIN(StartTime)) As BillingEndTime, 1 As BillingPeriod FROM #Patrols ), Numbers As ( SELECT num FROM (Values (0),(1), (2), (3), (4), (5), (6), (7), (8), (9)) AS n(Num) ), BillingPeriodsCTE AS ( SELECT DATEADD(Hour, 8 * (BillingPeriod + Numbers.Num), BillingStartTime) AS BillingStartTime, DATEADD(Hour, 8 * (BillingPeriod + Numbers.Num), BillingEndTime) AS BillingEndTime, BillingPeriod + Numbers.Num As BillingPeriod FROM FirstBillingPeriodCTE CROSS JOIN Numbers ) SELECT COUNT(DISTINCT BillingPeriod) FROM #Patrols P INNER JOIN BillingPeriodsCTE B ON P.StartTime >= B.BillingStartTime AND P.StartTime < B.BillingEndTime Results: | | |---| | 4 |
Here is a query that will give each billing period, up to 65,535 billing periods, accurate to the second. My solution uses a calculated "Tally" table, but you would be better off in the long run to create your own physical "Tally" table in your database. See What is the best way to create and populate a numbers table? for more details. You should be able to replace #tblPatrols with your patrol table. DECLARE #tblPatrols TABLE (alertNo int, startTime datetime); DECLARE #hoursPerBillingPeriod int, #toHoursConversion float; SET #hoursPerBillingPeriod = 12; SET #toHoursConversion = 60 * 60; INSERT INTO #tblPatrols (alertNo, startTime) VALUES (1, '2016-01-28 05:57') , (1, '2016-01-28 07:23') , (1, '2016-01-28 08:10') , (2, '2016-01-28 09:05') , (2, '2016-01-28 12:22') , (2, '2016-01-28 16:06') , (2, '2016-01-28 23:45') , (2, '2016-01-29 00:05') , (3, '2016-01-28 12:00') , (3, '2016-01-28 16:06') , (3, '2016-01-29 00:00') , (4, '2016-01-28 12:00') , (4, '2016-01-28 16:06') , (4, '2016-01-28 23:59:59.997') ; ;WITH --...................... --This section used to simulate a "Tally" table... you would be better off to Create a physical Tally table -- see: https://stackoverflow.com/questions/1393951/what-is-the-best-way-to-create-and-populate-a-numbers-table Pass0 as (select 1 as C union all select 1) --2 rows , Pass1 as (select 1 as C from Pass0 as A, Pass0 as B) --4 rows , Pass2 as (select 1 as C from Pass1 as A, Pass1 as B) --16 rows , Pass3 as (select 1 as C from Pass2 as A, Pass2 as B) --256 rows , Pass4 as (select 1 as C from Pass3 as A, Pass3 as B)--65536 rows , Tally as (select row_number() over(order by C) - 1 as N from Pass4) --65536 rows --........................ ,cteNumBillings as ( SELECT fp.alertNo , firstPatrolTime = min(fp.startTime) , lastPatrolTime = max(fp.startTime) , hoursBetweenStartMinMax = datediff(second, min(fp.startTime), max(fp.startTime)) / #toHoursConversion , numberOfBillingPeriods = floor(((datediff(second, min(fp.startTime), max(fp.startTime)) / #toHoursConversion) / #hoursPerBillingPeriod) + 1) FROM #tblPatrols fp GROUP BY fp.alertNo ) SELECT b.alertNo --This is the "x" value of the expression "Billing Period x of y" , BillingPeriodNumber = t.N + 1 , BillingPeriodPatrolCount = (select count(*) from #tblPatrols p where p.alertNo = b.alertNo and p.startTime >= dateadd(hour, 12 * t.N, b.firstPatrolTime) and p.startTime < dateadd(hour, 12 * (t.N+1), b.firstPatrolTime) ) , BillingStart = dateadd(hour, 12 * t.N, b.firstPatrolTime) , BillingEnd = dateadd(second, -1, dateadd(hour, 12 * (t.N + 1), b.firstPatrolTime)) --This is the "y" value of the expression "Billing Period x of y" , TotalBillingPeriodCount = b.numberOfBillingPeriods FROM cteNumBillings b INNER JOIN Tally t ON t.N >= 0 and t.N < b.numberOfBillingPeriods ORDER BY 1,2 ;
I found a solution by myself, which seems to be easier and I could not find any mistake using it. I take the first Startime of the first patrol in a variable. Then I use datediff for die difference of the all StartTimePatrol to the startime of the first patrol and divide it by 12 hours set #BillingPeriod=(select (datediff(hour,#StartTime,#StartTimePatrol)/12)+1) then I put the result of each record in a temp table insert into #t2 ( Alertno, Starttime, Billings ) values ( #Alertno, #StartTimePatrol, #BillingPeriod ) then I group the altertno and Billings and count them select alertno, count(Billings ) from (select alertno, Billings from #t2 group by alertno, Billings ) temp group by alertno The result looks correct for me. Thanks for all replies. Michael
Conditional counting based on comparison to previous row sql
Let's start with a sample of the data I'm working with: Policy No | start date 1 | 2/15/2006 1 | 2/15/2009 1 | 2/15/2012 2 | 3/15/2006 3 | 3/19/2006 3 | 3/19/2012 4 | 3/31/2006 4 | 3/31/2009 I'm trying to write code in SQL Server 2008 that counts a few things. The principle is that the policyholder's earliest start date is when the policy began. Every three years an increase is offered to the client. If they agree to the increase, the start date is refreshed with the same date as the original, three years later. If they decline, nothing is added to the database at all. I'm trying to not only count the number of times a customer accepted the offer (or increased the start date by three years), but separate it out by first offer or second offer. Taking the original start date and dividing the number of days between now and then by 1095 gets me the total number of offers, so I've gotten that far. What I really want it to do is compare each policy number to the one before it to see if it's the same (it's already ordered by policy number), then count the date change in a new "accepted" column and count the times it didn't change but could have as "declined". Is this a case where I would need to self-join the table to itself to compare the dates? Or is there an easier way?
are you looking for this :- Set Nocount On; Declare #Test Table ( PolicyNo Int ,StartDate Date ) Declare #PolicyWithInc Table ( RowId Int Identity(1,1) Primary Key ,PolicyNo Int ,StartDate Date ) Insert Into #Test(PolicyNo,StartDate) Values (1,'2/15/2006') ,(1,'2/15/2009') ,(1,'2/15/2012') ,(2,'3/15/2006') ,(3,'3/19/2006') ,(3,'3/19/2012') ,(4,'3/31/2006') ,(4,'3/31/2009') Insert Into #PolicyWithInc(PolicyNo,StartDate) Select t.PolicyNo ,t.StartDate From #Test As t Select pw.PolicyNo ,Sum(Case When Datediff(Year,t.StartDate, pw.StartDate) = 3 Then 1 Else 0 End) As DateArrived ,Sum(Case When Datediff(Year,t.StartDate, pw.StartDate) > 3 Then 1 Else 0 End) As DateNotArrived ,Sum(Case When Isnull(Datediff(Year,t.StartDate,pw.StartDate),0) = 3 Then 1 Else 0 End) As Years3IncrementCount From #PolicyWithInc As pw Left Join #PolicyWithInc As t On pw.PolicyNo = t.PolicyNo And pw.RowId = (t.RowId + 1) Group By pw.PolicyNo
Probably below could help: Set Nocount On; Declare #Test Table ( PolicyNo Int ,StartDate Date ) Insert Into #Test(PolicyNo,StartDate) Values (1,'2/15/2006') ,(1,'2/15/2009') ,(1,'2/15/2012') ,(2,'3/15/2006') ,(3,'3/19/2006') ,(3,'3/19/2012') ,(4,'3/31/2006') ,(4,'3/31/2009') select PolicyNo, StartDate, dateadd(yy, 3, StartDate)Offer1, dateadd(yy, 6, StartDate)Offer2, dateadd(yy, 9, StartDate)Offer3 from (select * , row_number() over (partition by PolicyNo order by StartDate) rn from #Test)A where rn = 1 select count(*) * 3 TotalOffersMade, count(Data1.StartDate) FirstOfferAccepted, count(Data2.StartDate) SecondOfferAccepted, count(Data3.StartDate) ThirdOfferAccepted, count(*) - count(Data1.StartDate) FirstOfferDeclined, count(*) - count(Data2.StartDate) SecondOfferDeclined, count(*) - count(Data3.StartDate) ThirdOfferDeclined from ( select PolicyNo, StartDate, dateadd(yy, 3, StartDate)Offer1, dateadd(yy, 6, StartDate)Offer2, dateadd(yy, 9, StartDate)Offer3 from (select * , row_number() over (partition by PolicyNo order by StartDate) rn from #Test)A where rn = 1 )Offers LEFT JOIN #Test Data1 on Offers.PolicyNo = Data1.PolicyNo and Offers.Offer1 = Data1.StartDate LEFT JOIN #Test Data2 on Offers.PolicyNo = Data2.PolicyNo and Offers.Offer2 = Data2.StartDate LEFT JOIN #Test Data3 on Offers.PolicyNo = Data3.PolicyNo and Offers.Offer3 = Data3.StartDate