Last day of month in a WHERE clause

  • Hi all!

    Have looked a bit around to find something here, but only found something about how to calculate last day of month.

    Given this extremly simple table:

    CREATE TABLE [dbo].[a_dates](

    [id] [int] NOT NULL,

    [Check_date] [datetime] NULL,

    CONSTRAINT [PK_a_dates] PRIMARY KEY CLUSTERED

    (

    [id] ASC

    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

    ) ON [PRIMARY]

    What i need is to SELECT count(*) WHERE [Check_date] is last day in month including leap years etc.

    Best regards

    Edvard Korsbæk

  • Edvard

    If you add one day to the last day of the month, you get the first day of the next month. You could therefore try something like this:

    WHERE DATEPART(day,DATEADD(day,1,Check_date)) = 1

    If you have an index on Check_date then this query isn't going to be able to use it - I'm not sure whether there's a sargable way of doing the above.

    John

  • SELECT

    t.[Date],

    cnt = COUNT(*)

    FROM Trans t -- 36M rows

    CROSS APPLY (SELECT ldom = DATEADD(MONTH,1+DATEDIFF(MONTH,0,t.[Date]),0)-1) x

    WHERE DAY([Date]) > 27

    AND t.[Date] = x.ldom

    GROUP BY t.[Date]

    -- (100 row(s) affected) / 00:00:02

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • Just a minor adjustment to Chris solution, It will not handle if there is date and time are coming in the check_date column.

    Here is Chris updated script:

    SELECT

    t.[Check_date],

    cnt = COUNT(*)

    FROM [dbo].[a_dates] t

    CROSS APPLY (SELECT ldom = DATEADD(MONTH,1+DATEDIFF(MONTH,0,t.[Check_date]),0)-1, DATEADD(day,DATEDIFF(day,0,t.[Check_date]),0) as checkDate) x

    WHERE DAY([Check_date]) > 27

    AND x.checkDate = x.ldom

    GROUP BY t.[Check_date]

    Or You can also use the EOFMONTH Function like this:

    SELECT

    t.[Check_date],

    cnt = COUNT(*)

    FROM [dbo].[a_dates] t

    CROSS APPLY (SELECT ldom = EOMONTH([Check_date]), Adj_CheckDate = CAST([Check_date] as date) )x

    WHERE DAY([Check_date]) > 27

    AND ldom = Adj_CheckDate

    GROUP BY t.[Check_date]

    hope it helps.

  • twin.devil (8/11/2016)


    Just a minor adjustment to Chris solution, It will not handle if there is date and time are coming in the check_date column.

    Here is Chris updated script:

    SELECT

    t.[Check_date],

    cnt = COUNT(*)

    FROM [dbo].[a_dates] t

    CROSS APPLY (SELECT ldom = DATEADD(MONTH,1+DATEDIFF(MONTH,0,t.[Check_date]),0)-1, DATEADD(day,DATEDIFF(day,0,t.[Check_date]),0) as checkDate) x

    WHERE DAY([Check_date]) > 27

    AND x.checkDate = x.ldom

    GROUP BY t.[Check_date]

    Did you test it with datetime?

    Edit: New version for datetimes with a time component:

    SELECT

    CAST(t.[Check_date] AS DATE),

    cnt = COUNT(*)

    FROM [a_dates] t -- 36M rows

    CROSS APPLY (SELECT ldom = DATEADD(MONTH,1+DATEDIFF(MONTH,0,t.[Check_date]),0)-1) x

    WHERE DAY([Check_date]) > 27

    AND CAST(t.[Check_date] AS DATE) = x.ldom

    GROUP BY CAST(t.[Check_date] AS DATE)

    ORDER BY CAST(t.[Check_date] AS DATE)

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • Did you test it with datetime?

    Yes i did.

  • I quickly slapped this together. Can someone do some scalability testing for me to see how it stacks up with the other code? Sadly I have to grab a shower and head to a client for some real (i.e. billable) work!! 😎

    SELECT DATEADD(DAY,DATEDIFF(DAY,0,t.d1),0) AS EOM,

    cnt = COUNT(*)

    FROM #tmp t

    WHERE DATEADD(DAY,DATEDIFF(DAY,0,t.d1),0) = DATEADD(MONTH,1+DATEDIFF(MONTH,0,DATEADD(DAY,DATEDIFF(DAY,0,t.d1),0)),0)-1

    GROUP BY DATEADD(DAY,DATEDIFF(DAY,0,t.d1),0)

    ORDER BY 1

    Best,
    Kevin G. Boles
    SQL Server Consultant
    SQL MVP 2007-2012
    TheSQLGuru on googles mail service

  • TheSQLGuru (8/11/2016)


    I quickly slapped this together. Can someone do some scalability testing for me to see how it stacks up with the other code? Sadly I have to grab a shower and head to a client for some real (i.e. billable) work!! 😎

    SELECT DATEADD(DAY,DATEDIFF(DAY,0,t.d1),0) AS EOM,

    cnt = COUNT(*)

    FROM #tmp t

    WHERE DATEADD(DAY,DATEDIFF(DAY,0,t.d1),0) = DATEADD(MONTH,1+DATEDIFF(MONTH,0,DATEADD(DAY,DATEDIFF(DAY,0,t.d1),0)),0)-1

    GROUP BY DATEADD(DAY,DATEDIFF(DAY,0,t.d1),0)

    ORDER BY 1

    Sure, same 36 million row table as earlier. Script run four or five times, here's a standard result:

    Chris v2

    SQL Server Execution Times:

    CPU time = 8769 ms, elapsed time = 1942 ms.

    ================================================

    Kevin v1

    SQL Server Execution Times:

    CPU time = 22401 ms, elapsed time = 3982 ms.

    SET NOCOUNT ON

    PRINT 'Chris v2'

    set statistics time on

    SELECT

    CAST(t.[date] AS DATE),

    cnt = COUNT(*)

    FROM trans t -- 36M rows

    CROSS APPLY (SELECT ldom = DATEADD(MONTH,1+DATEDIFF(MONTH,0,t.[date]),0)-1) x

    WHERE DAY([date]) > 27

    AND CAST(t.[date] AS DATE) = x.ldom

    GROUP BY CAST(t.[date] AS DATE)

    ORDER BY CAST(t.[date] AS DATE)

    set statistics time off

    print '================================================'

    PRINT 'Kevin v1'

    set statistics time on

    SELECT DATEADD(DAY,DATEDIFF(DAY,0,t.[date]),0) AS EOM,

    cnt = COUNT(*)

    FROM trans t

    WHERE DATEADD(DAY,DATEDIFF(DAY,0,t.[date]),0) = DATEADD(MONTH,1+DATEDIFF(MONTH,0,DATEADD(DAY,DATEDIFF(DAY,0,t.[date]),0)),0)-1

    GROUP BY DATEADD(DAY,DATEDIFF(DAY,0,t.[date]),0)

    ORDER BY 1

    set statistics time off

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • SQL Server 2014, four processors, 16GB RAM. Test harness:USE tempdb

    CREATE TABLE Dates (MyDate datetime);

    WITH N10(n) AS (

    SELECT CAST(n AS bigint) FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9))m(n)

    )

    , N100 AS (

    SELECT m1.n FROM N10 m1 CROSS JOIN N10 m2

    )

    , N10000 AS (

    SELECT m1.n FROM N100 m1 CROSS JOIN N100 m2

    )

    , N100000000 AS (

    SELECT m1.n FROM N10000 m1 CROSS JOIN N10000 m2

    )

    INSERT INTO Dates

    SELECT TOP 36000000 DATEADD(hour,CAST(RAND(CHECKSUM(NEWID()))*10000000 AS int),'19000101')

    FROM N100000000

    Four queries tested:-- John

    SELECT

    CAST(MyDate AS date)

    ,COUNT(*)

    FROM Dates

    WHERE DATEPART(day,DATEADD(day,1,MyDate)) = 1

    GROUP BY CAST(MyDate AS date)

    -- Chris

    SELECT

    CAST(MyDate AS DATE),

    cnt = COUNT(*)

    FROM Dates t -- 36M rows

    CROSS APPLY (SELECT ldom = DATEADD(MONTH,1+DATEDIFF(MONTH,0,t.MyDate),0)-1) x

    WHERE DAY(MyDate) > 27

    AND CAST(t.MyDate AS DATE) = x.ldom

    GROUP BY CAST(t.MyDate AS DATE)

    -- Kevin

    SELECT DATEADD(DAY,DATEDIFF(DAY,0,t.MyDate),0) AS EOM,

    cnt = COUNT(*)

    FROM Dates t

    WHERE DATEADD(DAY,DATEDIFF(DAY,0,t.MyDate),0) = DATEADD(MONTH,1+DATEDIFF(MONTH,0,DATEADD(DAY,DATEDIFF(DAY,0,t.MyDate),0)),0)-1

    GROUP BY DATEADD(DAY,DATEDIFF(DAY,0,t.MyDate),0)

    -- twin.devil's EOMONTH

    SELECT

    CAST(t.MyDate AS DATE),

    cnt = COUNT(*)

    FROM Dates t

    CROSS APPLY (SELECT ldom = EOMONTH(MyDate), Adj_CheckDate = CAST(MyDate as date) )x

    WHERE DAY(MyDate) > 27

    AND ldom = Adj_CheckDate

    GROUP BY CAST(t.MyDate AS DATE)

    Results (each query executed three times):

    John

    ----

    CPU Elapsed

    --- -------

    4242 1170

    4245 1176

    4135 1255

    Chris

    -----

    CPU Elapsed

    --- -------

    4712 1381

    4679 1289

    4650 1287

    Kevin

    -----

    CPU Elapsed

    --- -------

    9345 2611

    9267 2507

    9343 2465

    twin.devil

    ----------

    CPU Elapsed

    --- -------

    4258 1313

    4274 1184

    4291 1193

    John

  • The results I get are much closer than that, John (except for Kevin's).

    This might be the simplest option:

    SELECT

    Adj_CheckDate,

    cnt = COUNT(*)

    FROM trans t

    CROSS APPLY (SELECT ldom = EOMONTH([date]), Adj_CheckDate = CAST([date] as date) )x

    WHERE DAY([date]) > 27

    AND ldom = Adj_CheckDate

    GROUP BY Adj_CheckDate

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • Chris, yes, that's very similar to mine now.CPU Elapsed

    --- -------

    4303 1185

    4260 1184

    4273 1255

    John

  • IF you can afford a nonclustered index on check_date, this will drastically reduce the i/o.

    ;with Eomonths (Eom) as (select top(500) EoMonth(DateAdd(MONTH,(N-1),'1/1/2016')) from tally)

    select Eom as Check_date, Total

    from Eomonths

    cross apply (select sum(1) as Total from a_dates where check_date = eom) ca

    order by check_date

    Also, I have a question. Check_date is a datetime column. Are times ever reflected in any rows in that column?

    __________________________________________________

    Against stupidity the gods themselves contend in vain. -- Friedrich Schiller
    Stop, children, what's that sound? Everybody look what's going down. -- Stephen Stills

  • The Dixie Flatline (8/11/2016)


    IF you can afford a nonclustered index on check_date, this will drastically reduce the i/o.

    ;with Eomonths (Eom) as (select top(500) EoMonth(DateAdd(MONTH,(N-1),'1/1/2016')) from tally)

    select Eom as Check_date, Total

    from Eomonths

    cross apply (select sum(1) as Total from a_dates where check_date = eom) ca

    order by check_date

    Also, I have a question. Check_date is a datetime column. Are times ever reflected in any rows in that column?

    OP only gave the Table structure, didn't gave us any sample data to work with. So its an assumption that there could be Time involved as the datatype allows it.

  • The Dixie Flatline (8/11/2016)


    IF you can afford a nonclustered index on check_date, this will drastically reduce the i/o.

    ;with Eomonths (Eom) as (select top(500) EoMonth(DateAdd(MONTH,(N-1),'1/1/2016')) from tally)

    select Eom as Check_date, Total

    from Eomonths

    cross apply (select sum(1) as Total from a_dates where check_date = eom) ca

    order by check_date

    Also, I have a question. Check_date is a datetime column. Are times ever reflected in any rows in that column?

    I made the following changes to my test harness to make it more like the original poster's table, and to add the suggested non-clustered index.AL TER TABLE Dates ADD ID int NULL;

    WITH DatestoUpdate AS (

    SELECT

    ID

    ,ROW_NUMBER() OVER (ORDER BY NEWID()) RowNo

    FROM Dates

    )

    UPDATE DatestoUpdate

    SET ID = RowNo;

    AL TER TABLE Dates ALTER COLUMN ID int NOT NULL;

    AL TER TABLE DATES

    ADD CONSTRAINT PK_Dates_ID

    PRIMARY KEY CLUSTERED (ID);

    CREATE NONCLUSTERED INDEX IX_Dates_MyDate

    ON Dates(MyDate);

    I tweaked your query thus to generate the tally on the fly and to change the column names to fit in with my test harness:with N10(n) AS (

    SELECT CAST(n AS bigint) FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9))m(n)

    )

    , N100 AS (

    SELECT m1.n FROM N10 m1 CROSS JOIN N10 m2

    )

    , N10000 AS (

    SELECT m1.n FROM N100 m1 CROSS JOIN N100 m2

    )

    , tally AS (

    SELECT m1.n FROM N10000 m1 CROSS JOIN N10000 m2

    )

    , Eomonths (Eom) as (select TOP 500 EoMonth(DateAdd(MONTH,(n-1),'1/1/2016')) from tally)

    select Eom as Check_date, Total

    from Eomonths

    cross apply (select sum(1) as Total from Dates where MyDate = eom) ca

    order by check_date

    Unfortunately, all I got back was 500 lots of 2015-12-31 2

    I don't know whether that's down to your code or my tweaks!

    In other news, I retested the other contenders on the updated test harness, and here are the results:

    John

    ----

    CPU Elapsed

    --- -------

    3293 964

    3321 1046

    3324 947

    Chris (latest)

    --------------

    CPU Elapsed

    --- -------

    3290 921

    3278 1113

    3262 947

    Kevin

    -----

    CPU Elapsed

    --- -------

    8783 2882

    8595 2822

    8657 2734

    twin.devil

    ----------

    CPU Elapsed

    --- -------

    3291 1132

    3198 942

    3307 1036

    Interestingly enough, all four queries have the same execution plan, but the row estimates aren't the same in each. I've attached the plans in case anyone's interested.

    John

  • I tweaked your query thus to generate the tally on the fly and to change the column names to fit in with my test harness:with N10(n) AS (

    SELECT CAST(n AS bigint) FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9))m(n)

    )

    , N100 AS (

    SELECT m1.n FROM N10 m1 CROSS JOIN N10 m2

    )

    , N10000 AS (

    SELECT m1.n FROM N100 m1 CROSS JOIN N100 m2

    )

    , tally AS (

    SELECT m1.n FROM N10000 m1 CROSS JOIN N10000 m2

    )

    , Eomonths (Eom) as (select TOP 500 EoMonth(DateAdd(MONTH,(n-1),'1/1/2016')) from tally)

    select Eom as Check_date, Total

    from Eomonths

    cross apply (select sum(1) as Total from Dates where MyDate = eom) ca

    order by check_date

    Unfortunately, all I got back was 500 lots of 2015-12-31 2

    I don't know whether that's down to your code or my tweaks!

    Its happening because the logic is not proper in your tally CTE, It should be something this:

    with N10(n) AS (

    SELECT CAST(n AS bigint) FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9))m(n)

    )

    , N100 AS (

    SELECT m1.n FROM N10 m1 CROSS JOIN N10 m2

    )

    , N10000 AS (

    SELECT m1.n FROM N100 m1 CROSS JOIN N100 m2

    )

    , tally AS (

    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS n FROM N10000 m1 CROSS JOIN N10000 m2

    )

    , Eomonths (Eom) as (select TOP 500 EoMonth(DateAdd(MONTH,(n-1),'1/1/2016')) from tally)

    select Eom as Check_date, Total

    from Eomonths

    cross apply (select sum(1) as Total from Dates where MyDate = eom) ca

    order by check_date

    There are few issues regarding this solution:

    1. This logic will not work if MyDate column have TIME data in it, So MyDate should be cast as date.

    2. You need to have distinct value in Eom to avoid getting the same result.

    3. Its would be ending up like a Sub Query written Select statement ( Old School Style).

Viewing 15 posts - 1 through 15 (of 25 total)

You must be logged in to reply to this topic. Login to reply