如何生成具有开始月份日期和结束月份日期的日历表

问题描述 投票:1回答:2

我需要在具有乞讨月份日期和结束月份日期的两个日期之间生成日历表。如果它比今天更大,那么它应该在当前日期停止。

应该是这样的:

enter image description here

如您所见,Eomonth列的最后一个值具有今天的日期(不是月末)

谢谢

sql-server tsql sql-server-2012
2个回答
3
投票

如果您没有日历表,则可以使用临时计数表

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

返回

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

2
投票

只是为了建立John的出色答案......我对他的解决方案做了一个改变:

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

现在执行计划:

enter image description here

我们是如何删除那种?通过杠杆,我称之为virtual index。 ROW_NUMBER返回有序的数字流。这就是为什么你可以将名为RN的列定义为RN = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))并添加一个不会导致排序的ORDER BY RN语句。似乎所有窗口排名函数都执行此操作(RANK,DENSE_RANK,NTILE和ROW_NUMBER)。

考虑到这一点,让我们检查一下利用dbo.RangeAB的解决方案,这个函数可以充分利用虚拟索引的强大功能。

-- 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

返回:

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

如果你检查执行计划,尽管我订购了,你也不会看到排序。但是Descending Sorts怎么样? ROW_NUMBER虚拟索引不处理DESCending排序?如果将上述查询更改为ORDER BY r.RN DESC,您将在执行计划中看到一种排序。要改变它,我们只需将r.RN的引用更改为r.OP. r.OP是ROW_NUMBER的相反数字。让我们比较这两个返回相同结果的查询。我在这里做的是回归最近五个月:

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;

并执行计划:

enter image description here

利用我称之为有限相反数字我可以使用ROW_NUMBER(RN)相反数字(OP)我可以按相反顺序返回数字,同时仍按RN按升序排序。使用OP,dbo.rangeAB的有限对数列,您可以充分利用虚拟索引,而不仅仅用于ASCending排序。

我使用的函数的DDL如下。

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;

请注意,您必须调整我使用的自定义架构(例如,更改为DBO)

© www.soinside.com 2019 - 2024. All rights reserved.