• jcelko212 32090 - Friday, October 20, 2017 10:28 AM

    Jason A. Long - Tuesday, October 17, 2017 11:57 PM

    Build a calendar table with one column for the calendar data and other columns to show whatever your business needs in the way of temporal information. Do not try to calculate holidays in SQL -- Easter alone requires too much math and there are two of them (Orthodox and Catholic).

    The Ordinal business day is a good trick. Number the days from whenever your calendar starts and repeat a number for a weekend or company holiday.

    CREATE TABLE Calendar
    (cal_date DATE NOT NULL PRIMARY KEY,
    ordinal_business_nbr INTEGER NOT NULL,
    ...);

    INSERT INTO Calendar
    VALUES ('2007-04-05', 42);
     ('2007-04-06', 43); -- Good Friday
     ('2007-04-07', 43);
     ('2007-04-08', 43); -- Easter Sunday
     ('2007-04-09', 44);
     ('2007-04-10', 45); -- Tuesday, back to work

    To compute the business days from '2007-04-05', Thursday before Easter in 2007 to '2007-04-10', the next Tuesday:

    SELECT (C2.ordinal_business_nbr - C1.ordinal_business_nbr -1) AS business_day_cnt
    FROM Calendar AS C1, Calendar AS C2
    WHERE C1.cal_date = '2007-04-05'
     AND C2.cal_date = '2007-04-10';

    This saves having to scan all of the rows within the range to get a sum. The -1 is to avoid counting the current day as one of the business days, but you might have a different rule in your company. 

    Joe, We're thinking along the same lines... and you're dead on the money with the ordinal positions... That was, in fact the "Eureka"  moment that made it all come together. 
    The implementation is where we differ... Turns out I was able to dump both the table constructor (VALUES table)  AND any reference to an external table, by using simple CASE expressions... :w00t:

           cte_holiday (f, l) AS (
                SELECT
                    CASE
                        WHEN '2000-05-29' >= @BegDate THEN 0
                        WHEN '2000-07-04' >= @BegDate THEN 1
                        WHEN '2000-09-04' >= @BegDate THEN 2
                        WHEN '2000-11-23' >= @BegDate THEN 3
                        WHEN '2000-11-24' >= @BegDate THEN 4
                        WHEN '2000-12-25' >= @BegDate THEN 5
                        --....
                        WHEN '2029-07-04' >= @BegDate THEN 204
                        WHEN '2029-09-03' >= @BegDate THEN 205
                        WHEN '2029-11-22' >= @BegDate THEN 206
                        WHEN '2029-11-23' >= @BegDate THEN 207
                        WHEN '2029-12-25' >= @BegDate THEN 208
                    END,
                     CASE
                        WHEN '2000-05-29' >= @EndDate THEN 0
                        WHEN '2000-07-04' >= @EndDate THEN 1
                        WHEN '2000-09-04' >= @EndDate THEN 2
                        WHEN '2000-11-23' >= @EndDate THEN 3
                        WHEN '2000-11-24' >= @EndDate THEN 4
                        WHEN '2000-12-25' >= @EndDate THEN 5
                        --...
                        WHEN '2029-07-04' >= @EndDate THEN 204
                        WHEN '2029-09-03' >= @EndDate THEN 205
                        WHEN '2029-11-22' >= @EndDate THEN 206
                        WHEN '2029-11-23' >= @EndDate THEN 207
                        WHEN '2029-12-25' >= @EndDate THEN 208
                    END
                )
        SELECT
            WorkingDays = DATEDIFF(DAY, DATEADD(DAY, DATEDIFF(DAY, 0, @BegDate), 0), DATEADD(DAY, DATEDIFF(DAY, 0, @EndDate), 0)) - (w.weekend_days + (h.l - h.f))
        FROM
            cte_weekend_count w
            JOIN cte_holiday h
                ON 1 = 1;

    The net result...  No joins, no Cartesian product and and the following execution plan...



    My concern has now shifted. Now I'm concerned that the optimizer is underestimating the cost...
    The plan w/ the new function isn't far too off from the same table being run alone...  but... the execution times beg to differ...