1

我有一个在 SQL 2008 R2 服务器上运行的 SQL 查询。它在每月的 1 日和 16 日运行。该查询包含以下用于设置 StartDate 和 EndDate 参数的逻辑:

DECLARE     @RunDateTime datetime
DECLARE     @RunDate datetime
DECLARE     @StartDate datetime
DECLARE     @EndDate datetime
DECLARE     @Month int
DECLARE     @Year int
DECLARE     @strStartDate varchar(10)

SET     @RunDateTime = GetDate()
SET     @RunDate = CAST(@RunDateTime AS DATE)

IF                  DATEPART(d, @RunDate) = 16
    BEGIN       
                    SET @StartDate =  DATEADD(d, -15, @RunDate)
                    SET @EndDate = @RunDate
    END
ELSE
    BEGIN
                    IF      Month(@RunDate) = 1
                            SET @Month = 12
                    ELSE    
                            SET @Month = Month(@RunDate) - 1

                    IF      Month(@RunDate) = 1
                            SET @Year = Year(@RunDate) - 1
                    ELSE
                            SET @Year = Year(@RunDate)

                    SET @strStartDate = CAST(@Year AS VARCHAR) + CAST(@Month AS VARCHAR) + '16'
                    SET @StartDate = CONVERT(datetime, @strStartDate)
                    SET @EndDate = @RunDate
    END

如果查询在 16 日运行,我们希望日期范围为每月 1 日到 15 日。如果查询在 1 日运行,我们希望日期范围是从 16 日到上个月的月底。

由于工作中的需求不断变化,我们被告知需要找到一种方法来做到这一点,而无需使用任何日期到字符串的转换。这可能吗,有没有人知道如何做到这一点?我很快就在这里脱离了我的深度。

4

4 回答 4

2

当我看到您之前的问题时,我知道有一种更简单的方法可以解决这个问题。这个怎么样:

DECLARE     @StartDate date;
DECLARE     @EndDate date;
DECLARE     @RunDateTime datetime = getdate() - 2;

if DAY(@RunDateTime) = 1
begin
    set @StartDate = dateadd(day, 15, DATEADD(MONTH, -1, @RunDateTime));
    set @EndDate = DATEADD(day, -1, @RunDateTime);
end
else if day(@RunDateTime) = 16
begin
    set @StartDate = dateadd(day, -DAY(@RunDateTime) + 1, @RunDateTime);
    set @EndDate = dateadd(day, -DAY(@RunDateTime) + 15, @RunDateTime);
end
else -- do your error processing here
于 2013-01-03T19:00:52.403 回答
2

这将符合您的规范。它不关心您在哪一天运行它,并且如果 EndDate 不在确切的边界上,它会将其调整为当前日期。我无法与您给定的代码完全比较,因为它在某些日期(例如20120201)有错误。

DECLARE
   @StartDate datetime,
   @EndDate datetime;

SELECT
   @StartDate = Convert(date, M.Anchor + X.S),
   @EndDate = Convert(date,
      M.Anchor + Day(GetDate() - 1)
      - CASE WHEN Day(GetDate()) IN (1, 16) THEN 1 ELSE 0 END
   )
FROM
   (VALUES (1, 15, 0), (16, 31, 15)) X (F, T, S)
   CROSS APPLY (
      SELECT DateAdd(month, DateDiff(month, 0, GetDate() - 1), 0)
   ) M (Anchor)
WHERE Day(GetDate() - 1) BETWEEN X.F AND X.T;

我针对一年的日期测试了这段代码,我相信一切正常。

通过在这个 SqlFiddle中检查一年的计算结果,自己验证它的正确性。

基本上:从昨天的日期计算每月的第一天。使用该日期,确定从月初的偏移量以计算开始日期(如果 1 日 - 15 日,使用 0;如果 16 日 - 31 日,使用 15)。最后,从月初 + 计算的日期计算结束日期,对 1 日和 16 日进行特殊调整以使用前一天的结束期间。

实际上,始终指定到周期结束(1 - 16 或 1 - 28/29/30/31)而不是一直持续到这一天并没有什么害处,但这不是你所要求的。

回答您关于查询中发生的事情的问题。

  1. 派生表是行集返回查询,通常以 a 开头,SELECT括在括号中并给出别名。例如:

    SELECT X.FullName
    FROM
       (
          SELECT FullName = FirstName + ' ' + LastName
          FROM dbo.User
          WHERE ID = 123
       ) AS X
    

    在这里,我们派生X了包含其中查询结果的表。我喜欢跳过这一AS部分,因为对我来说它只会增加混乱。请注意,这只是一个简单的示例——派生表通常比这更复杂,并且在各种情况下都很有用。我在dbo.User桌子上没有使用别名,但最好这样做。但重要的是要注意派生表中的所有列引用都是从派生表中解析的。不使用外面的桌子。

  2. VALUES是一种从查询中提供的值创建行集的方法。您可能熟悉的正常语法是INSERT dbo.Table VALUES (1, 'a');. 在 SQL Server 2008 中,这被扩展为同时允许多行,如INSERT dbo.Table VALUES (1, 'a'), (2, 'b');. 我还允许这种特殊的多行表示法代替SELECT派生表中的查询。例如:

    SELECT X.*
    FROM (VALUES (1, 'a'), (2, 'b')) X
    -- Alias X is for a derived table of 2 rows & 2 unnamed columns
    
  3. 派生表的列名通常是从查询中发现的。看到在#1 中,X.FullName实际上是指包含来自 table 的多个列的表达式dbo.User。表达式在查询中被显式地赋予了一个新别名。如果您没有提供别名,则会收到错误消息,因为表达式没有内在名称。事实上,前一点的查询不会按原样运行,因为派生表X没有列名!但是,有一种语法可以在派生表之外提供显式列别名:

    SELECT X.ID, X.Name
    FROM (VALUES (1, 'a'), (2, 'b')) X (ID, Name)
    

    我更喜欢第二种语法,因为它确实有助于明确其意图,它有助于将表达式与其名称分开,而且通常对我来说,在派生表中列别名更好。很多时候,我从其他地方复制和粘贴派生表中的内容,而且不必每次都拼凑列名是很好的。别名F,TS是给我FromTo, 和Start. 我本可以让它们成为更长的名字,但我选择不这样做。

  4. 使用SELECT Alias = <Expression>只是SELECT <Expression> AS Alias. 我更喜欢这种=方法,因为这样别名可靠地位于一个易于扫描的列的左侧,而不是可变长度表达式的末尾。由于能够通过添加一个字符来更改变量名称,或者UPDATE轻松地将查询转换为语句,因此这种语法也有好处。

  5. 您可以更改任何查询以将值分配给变量,而不是仅通过@Variable =在每个列表达式前面添加来返回行集。需要注意的一个问题是,如果查询返回多行,虽然您仍然可以使用该@Variable =语法,但服务器仍将完成所有实现所有行的工作,并且您的变量将仅具有值) 从一些,一个,行。它可能是第一行,也可能是最后一行,但即使您认为返回的行一致,您也应该始终假设它是随机行。如果您需要特定行,则提供 WHERE 子句或带有 的TOP语句ORDER BY以强制使用特定行的值。

  6. CROSS APPLY只是一个派生表,它具有允许具有“外部引用”的特殊属性,也就是说,它可以在括号内使用之前引入的表中的列值。它在没有返回行时限制行(就像INNER JOINed 派生表一样),或者OUTER APPLY不限制行(就像OUTER JOINed 派生表一样)。它不需要ON子句,因为所有过滤都是通过WHERE子句完成的。优化器非常善于理解查询的意图,并且最终不会APPLY为每个外部行运行一次——它几乎总是能够智能地获取数据,就好像它只是一个常规连接一样。我在这里使用它作为一种廉价的方法来获取我可以多次使用的查询中的计算值。我也可以做到DECLARE @MonthDate = <same expression>并改用它,但我的某些部分喜欢在我不需要时不声明变量(因为 @MonthDate 变量仅用于单个查询)。

PS我想指出我在您的示例代码中看到的一件事(如果您允许的话)。考虑这一部分:

IF   Month(@RunDate) = 1
     SET @Month = 12
ELSE    
     SET @Month = Month(@RunDate) - 1

IF   Month(@RunDate) = 1
     SET @Year = Year(@RunDate) - 1
ELSE
     SET @Year = Year(@RunDate)

所有这些排列都非常适合尝试传达 IF / ELSE 块的意图,但在我的专业意见中,您应该使用BEGINand END。编写此代码的人选择以这种方式布置条件的一个原因是缩进样式(对我而言)有点过分。每个BEGIN最终都有 2 个缩进深度,一个用于IF(将其视为一个缩进块),一个用于BEGIN .. END. 但是这种对没有块的单语句 IF 的附加会导致几个问题。

  1. 上面的表达式没有正确传达意图,并迫使审阅者评估为什么某事被做了两次,实际上这两个条件是相同的。对于初学者,该块应重写为:

    IF   Month(@RunDate) = 1
         BEGIN
            SET @Month = 12;
            SET @Year = Year(@RunDate) - 1;
         END
    ELSE
         BEGIN
            SET @Month = Month(@RunDate) - 1;
            SET @Year = Year(@RunDate);
         END;
    

    现在可以明智地理解和审查这一点。

  2. 一旦您开始添加多个条件或嵌套级别,您就会陷入大麻烦并发现几乎无法调试。如果某个有进取心的开发人员认识到这两个条件是相同的,并且将它们的内容结合起来最有意义,那么会编写以下代码:

    IF   Month(@RunDate) = 1
         SET @Month = 12;
         SET @Year = Year(@RunDate) - 1;
    ELSE
         SET @Month = Month(@RunDate) - 1;
         SET @Year = Year(@RunDate);
    

    看起来不错,但隐藏了第二个SET语句不属于IF它之前的事实。现在,它不会正确编译,因为它ELSE是孤立的。但如果改变是这样的:

    IF   Month(@RunDate) = 1
         SET @Month = 12;
         IF @SpecialFlag = 1
            SET @Year = Year(@RunDate) - 1;
    ELSE
         SET @Month = Month(@RunDate) - 1;
         IF @SpecialFlag = 1
            SET @Year = Year(@RunDate);
    

    现在我们有一个可怕的混乱!这将正确解析,但与开发人员预期的结果相去甚远:ELSE块是@SpecialFlag条件的一部分!由于缩进,它在代码中看起来肯定不像它。

因此,尽管我知道代码格式约定可能会因坚定的信念而受到青睐,并且组织和人员可能会极大地抵制更改,但我想建议您将获得一些好处,如果 1)您在所有块中使用BEGIN和,并且2)为了减轻由于双缩进所有内容的麻烦而引起的斗争,您重新制定您的块缩进练习,如下所示:ENDIF

  • 代替IFELSE开始一个没有结束的块,并在更深的层次上BEGIN进行匹配;END

  • 制作IFELSE开始以 , 结尾的块END,并放在BEGIN行尾。

这将是:

IF Month(@RunDate) = 1 BEGIN
   SET @Month = 12;
   SET @Year = Year(@RunDate) - 1;
END
ELSE BEGIN
   SET @Month = Month(@RunDate) - 1;
   SET @Year = Year(@RunDate);
END;

我认识到这对于习惯于不同缩进风格的人来说看起来很奇怪——所有的改变一开始都很难。但我相信,只要稍加练习,它就会在你身上生长,变得不那么痛苦。只是训练眼睛去配合END其他东西而已BEGIN。最终,您意识到您甚至都不在乎寻找,因为它不会为以orBEGIN开头的行添加任何额外的含义。最后,您不必做疯狂的、劳动密集型的缩进模式(包括在后面放置多个空格并让所有内容对齐),因为缩进很有效,而且您只需为每个块做一个级别而不是许多。而且你永远不会有代码脱离IFELSEIFELSEIF如上所示意外阻塞。

100% 明确:使用这种样式,如果BEGIN/END块中只有一个语句,则无需重新排列任何内容以添加第二个语句,并且代码不会中断。

最后,请注意我在自己的示例中添加了分号,原因很简单,在 SQL Server 中它们有一天会被需要,我希望我的所有生产代码都可以继续工作,而不需要一个巨大而痛苦的分号项目!这也带来了明确指示您的块何时停止的好处(坦率地说,我不知道在END前面的立即数之后是否需要分号 - 如果是ELSE这样,我至少会有一个分号项目而不是巨大的)。

于 2013-01-03T19:58:12.470 回答
1

试试这个(SQL FIDDLE);

declare @startdate date, @enddate date, @runDate date
select @runDate = '20130216'

select @startdate = case datepart(day,@runDate) when 16 
                         then dateadd(day,-15, @runDate)
                         else dateadd(day,14, dateadd(month,-1,@runDate))
                    end,
       @enddate =  dateadd(day,-1,@runDate)

select @startdate, @enddate 
--Results for when @runDate = '20130216'
2013-02-01  2013-02-15

--Results for when @runDate = '20130201'
2013-01-15  2013-01-31
于 2013-01-03T19:23:10.600 回答
0

像这样的东西可能会起作用:

where
    Run_Date between
    (
        case
            when datepart(dd, getdate()) = 1 then dateadd(mm, -1, getdate())
            else dateadd(dd, -15, getdate())
        end
    )
    and
    (
        case
            when datepart(dd, getdate()) = 1 then dateadd(dd, -1, getdate())
            else dateadd(dd, -1, getdate())
        end
    )

您的数字可能会有所不同

于 2013-01-03T18:55:32.317 回答