【问题标题】:ADD_MONTHS function does not return the correct date in OracleADD_MONTHS 函数在 Oracle 中不返回正确的日期
【发布时间】:2011-07-17 23:28:53
【问题描述】:

查看以下查询的结果:

>> SELECT ADD_MONTHS(TO_DATE('30-MAR-11','DD-MON-RR'),-4) FROM DUAL;
30-NOV-10


>> SELECT ADD_MONTHS(TO_DATE('30-NOV-10','DD-MON-RR'),4) FROM DUAL;
31-MAR-11

如何在某个日期加上 4 个月时得到“30-MAR-11”?

请帮忙。

【问题讨论】:

  • 它可能可以通过一些额外的 (PL/)SQL 魔法来完成,但如何做到这一点也取决于 '30-OCT-10' 加上 4 个月的预期结果。那是 2 月 28 日 29 日还是 3 月 1 日?
  • 在添加“月”时,天数不同,没有单一的“正确方法”来添加它们。 Oracle 提供了 2 种方法:ADD_MONTHS 和区间算法。

标签: sql oracle date-arithmetic


【解决方案1】:

这里还有一个关于Oracle and Java的问题

说明

来自关于 add_months http://download-west.oracle.com/docs/cd/B19306_01/server.102/b14200/functions004.htm 的 Oracle 参考

如果 date 是该月的最后一天,或者如果生成的月份的天数少于 date 的天部分,则结果是生成月份的最后一天。否则,结果与日期具有相同的日期部分。

所以我猜你必须手动检查声明日期和结束日期才能更改函数的行为。或者也许通过添加天而不是月。 (但我在 ref 中没有找到 add_day 函数)

【讨论】:

  • 你找不到任何 add_days 函数可能是因为他们认为它是多余的,因为他们可以使用 +- 进行更简单的数学运算。
  • @Andriy:如果是这样,我该如何编写脚本以通用方式处理所有日期。不仅是从 11 月到 3 月的这 4 个月。
  • @Mohammed Saigh:请定义这种通用方式。到目前为止,我可以假设您希望在将 4 个月添加到 30-NOV-11 时保留日组件。但是如果你必须从31-MAR-11 中减去 4 个月呢?白天应该是什么?
【解决方案2】:

作为一种解决方法,我可能会使用这个算法:

  1. 使用ADD_MONTHS 计算目标日期TargetDate1
  2. 或者像这样计算目标日期TargetDate2

    1) 将ADD_MONTHS 应用于源日期月份的第一个月;
    2) 添加源日期与同月初的天数差。

  3. TargetDate1TargetDate2之间选择LEAST

因此,如果源日期的天数大于目标月份的天数,那么最终目标日期将包含不同的天数。在这种情况下,目标日期将是相应月份的最后一天。

我不太确定我对 Oracle 的 SQL 语法的了解,但基本上实现可能如下所示:

SELECT
  LEAST(
    ADD_MONTHS(SourceDate, Months),
    ADD_MONTHS(TRUNC(SourceDate, 'MONTH'), Months)
      + (SourceDate - TRUNC(SourceDate, 'MONTH'))
  ) AS TargetDate
FROM (
  SELECT
    TO_DATE('30-NOV-10', 'DD-MON-RR') AS SourceDate,
    4 AS Months
  FROM DUAL
)

以下是该方法如何工作的详细说明:

SourceDate = '30-NOV-10'
Months     = 4

TargetDate1 = ADD_MONTHS('30-NOV-10', 4) = '31-MAR-11'  /* unacceptable */
TargetDate2 = ADD_MONTHS('01-NOV-10', 4) + (30 - 1)
            = '01-MAR-11' + 29 = '30-MAR-11'            /* acceptable */
TargetDate  = LEAST('31-MAR-11', '30-MAR-11') = '30-MAR-11'

这里还有一些例子来展示不同的情况:

SourceDate | Months | TargetDate1 | TargetDate2 | TargetDate
-----------+--------+-------------+-------------+-----------
 29-NOV-10 |    4   |   29-MAR-11 |   29-MAR-11 |  29-MAR-11
 30-MAR-11 |   -4   |   30-NOV-10 |   30-NOV-10 |  30-NOV-10
 31-MAR-11 |   -4   |   30-NOV-10 |   01-DEC-10 |  30-NOV-10
 30-NOV-10 |    3   |   28-FEB-11 |   02-MAR-11 |  28-FEB-11

【讨论】:

    【解决方案3】:

    你可以使用区间算法得到你想要的结果

    SQL> select date '2011-03-30' - interval '4' month
      2    from dual;
    
    DATE'2011
    ---------
    30-NOV-10
    
    SQL> ed
    Wrote file afiedt.buf
    
      1  select date '2010-11-30' + interval '4' month
      2*   from dual
    SQL> /
    
    DATE'2010
    ---------
    30-MAR-11
    

    但是请注意,如果您使用的日期不是每个月都存在的,那么区间算术就会存在缺陷

    SQL> ed
    Wrote file afiedt.buf
    
      1  select date '2011-03-31' + interval '1' month
      2*   from dual
    SQL> /
    select date '2011-03-31' + interval '1' month
                             *
    ERROR at line 1:
    ORA-01839: date not valid for month specified
    

    【讨论】:

      【解决方案4】:

      这样的事情怎么样:

      SELECT
          LEAST(
              ADD_MONTHS(TO_DATE('30-MAR-11','DD-MON-RR'),-4),
              ADD_MONTHS(TO_DATE('30-MAR-11','DD-MON-RR')-1,-4)+1
          )
      FROM
          DUAL
      ;
      

      结果:1​​0 年 11 月 30 日

      SELECT
          LEAST(
              ADD_MONTHS(TO_DATE('30-NOV-10','DD-MON-RR'),4),
              ADD_MONTHS(TO_DATE('30-NOV-10','DD-MON-RR')-1,4)+1
          )
      FROM
          DUAL
      ;
      

      结果:1​​1 年 3 月 30 日

      【讨论】:

      • 与我的相似,但不完全是一种改进——而是一种稍微不同的方法(比我的更好——对我来说,无论如何)。
      【解决方案5】:

      add_months 函数返回一个日期加上 n 个月。

      由于 11 月 30 日是该月的最后一天,因此添加 4 个月将导致日期为 4 个月的结束日期。这是预期的行为。如果日期不会更改,解决方法是在返回新日期后减去一天

      SQL> SELECT ADD_MONTHS(TO_DATE('30-NOV-10','DD-MON-RR'),4) -1 from dual;
      
      ADD_MONTH
      ---------
      30-MAR-11
      

      【讨论】:

        【解决方案6】:
        SELECT TO_DATE('30-NOV-10','DD-MON-RR') + 
               (
                ADD_MONTHS(TRUNC(TO_DATE('30-NOV-10','DD-MON-RR'),'MM'),4) - 
                TRUNC(TO_DATE('30-NOV-10','DD-MON-RR'),'MM')
               ) RESULT
          FROM DUAL;
        

        括号中的这一部分:

        ADD_MONTHS(TRUNC(TO_DATE('30-NOV-10','DD-MON-RR'),'MM'),4) - TRUNC(TO_DATE('30-NOV-10','DD-MON-RR'),'MM')
        

        提供您输入的日期与 4 个月后之间的天数。因此,将此天数添加到您给出的日期会得到 4 个月后的确切日期。

        参考:http://www.dba-oracle.com/t_test_data_date_generation_sql.htm

        【讨论】:

        • 不错。请记住,如果您使用您的方法将 3 个月添加到 '30-NOV-10',您将得到 '02-MAR-11' 作为结果。
        【解决方案7】:

        简单的解决方案:

        ADD_MONTHS(date - 1, x) + 1
        

        【讨论】:

        • 这看起来像是对tekla's suggestion 的简化,但似乎只针对目标月份的天数多于源月份的情况进行了调整。当它相反时将无法按预期工作。
        【解决方案8】:

        这是诀窍:

        select add_months(to_date('20160228', 'YYYYMMDD')-1, 1)+1 from dual;
        

        享受吧!

        【讨论】:

          【解决方案9】:

          我们已经针对这个问题找到了更简单的(在我们的理解中)解决方案 - 从原始日期和 add_month 结果日期中取最少的天数,如下所示:

          TRUNC(ADD_MONTHS(input_date,1),'MM') + LEAST(TO_CHAR(input_date, 'DD'), TO_CHAR(ADD_MONTHS(input_date,1), 'DD')) - 1 
          

          此处的其他一些示例并非在每个日期都有效,低于我们的测试结果:

          WITH DATES as (
              SELECT TO_DATE('2020-01-31', 'YYYY-MM-DD HH24:MI:SS') as input_date,
                     '2020-02-29' as expected_date
              FROM dual
              UNION ALL
              SELECT TO_DATE('2020-02-28', 'YYYY-MM-DD HH24:MI:SS'),
                     '2020-03-28'
              FROM dual
              UNION ALL
              SELECT TO_DATE('2020-09-30', 'YYYY-MM-DD HH24:MI:SS'),
                     '2020-10-30'
              FROM dual
              UNION ALL
              SELECT TO_DATE('2020-09-01', 'YYYY-MM-DD HH24:MI:SS'),
                     '2020-10-01'
              FROM dual
              UNION ALL
              SELECT TO_DATE('2019-01-30', 'YYYY-MM-DD HH24:MI:SS'),
                     '2019-02-28'
              FROM dual
              UNION ALL
              SELECT TO_DATE('2020-02-29', 'YYYY-MM-DD HH24:MI:SS'),
                     '2020-03-29'
              FROM dual
              UNION ALL
              SELECT TO_DATE('2020-09-29', 'YYYY-MM-DD HH24:MI:SS'),
                     '2020-10-29'
              FROM dual
              UNION ALL
              SELECT TO_DATE('2020-03-01', 'YYYY-MM-DD HH24:MI:SS'),
                      '2020-04-01'
              FROM dual
          ),
          methods as (
          SELECT
              input_date,
              expected_date,
              ADD_MONTHS(input_date,1) as standard_way,
              add_months(input_date-1, 1)+1 as wrong_way,
              TO_DATE(LEAST(TO_CHAR(input_date, 'DD'), TO_CHAR(ADD_MONTHS(input_date,1), 'DD')) || '-' || TO_CHAR(ADD_MONTHS(input_date,1), 'MM-YYYY'), 'DD-MM-YYYY') as good_way,
              TRUNC(ADD_MONTHS(input_date,1),'MM') + LEAST(TO_CHAR(input_date, 'DD'), TO_CHAR(ADD_MONTHS(input_date,1), 'DD')) - 1 as better_way
          FROM
               DATES
          )
          SELECT
              input_date,
              expected_date,
              standard_way,
              CASE WHEN TO_CHAR(standard_way,'YYYY-MM-DD') = expected_date THEN 'OK' ELSE 'NOK' END as standard_way_ok,
              wrong_way,
              CASE WHEN TO_CHAR(wrong_way,'YYYY-MM-DD') = expected_date THEN 'OK' ELSE 'NOK' END as wrong_way_ok,
              good_way,
              CASE WHEN TO_CHAR(good_way,'YYYY-MM-DD') = expected_date THEN 'OK' ELSE 'NOK' END as good_way_ok,
              better_way,
             CASE WHEN TO_CHAR(better_way,'YYYY-MM-DD') = expected_date THEN 'OK' ELSE 'NOK' END as better_way_ok
          FROM
              methods
          ;
          

          【讨论】:

            【解决方案10】:
                CREATE OR REPLACE FUNCTION My_Add_Month(
                  STARTDATE           DATE,
                  MONTHS_TO_ADD      NUMBER
                )
                    RETURN DATE
                IS
                    MY_ADD_MONTH_RESULT DATE;
                BEGIN
            
                    SELECT ORACLES_ADD_MONTH_RESULT + NET_DAYS_TO_ADJUST INTO MY_ADD_MONTH_RESULT FROM
                    (
                        SELECT T.*,CASE WHEN SUBSTRACT_DAYS > ADD_DAYS THEN ADD_DAYS - SUBSTRACT_DAYS ELSE 0 END AS NET_DAYS_TO_ADJUST FROM
                        (
                            SELECT T.*,EXTRACT(DAY FROM ORACLES_ADD_MONTH_RESULT) AS SUBSTRACT_DAYS FROM
                            (
                                SELECT ADD_MONTHS(STARTDATE,MONTHS_TO_ADD) AS ORACLES_ADD_MONTH_RESULT,EXTRACT(DAY FROM STARTDATE) AS ADD_DAYS FROM DUAL
                            )T
                        )T
                    )T;
                    RETURN TRUNC(MY_ADD_MONTH_RESULT);
                END My_Add_Month;
                /
            
                --test & verification of logic & function both
                SELECT T.*,ORACLES_ADD_MONTH_RESULT + NET_DAYS_TO_ADJUST AS MY_ADD_MONTH_RESULT,
                My_Add_Month(STARTDATE,MONTHS_TO_ADD) MY_ADD_MONTH_FUNCTION_RESULT
                FROM
                (
                    SELECT T.*,CASE WHEN SUBSTRACT_DAYS > ADD_DAYS THEN ADD_DAYS - SUBSTRACT_DAYS ELSE 0 END AS NET_DAYS_TO_ADJUST FROM
                    (
                        SELECT T.*,EXTRACT(DAY FROM ORACLES_ADD_MONTH_RESULT) AS SUBSTRACT_DAYS FROM
                        (
                            SELECT T.*,ADD_MONTHS(STARTDATE,MONTHS_TO_ADD) AS ORACLES_ADD_MONTH_RESULT,EXTRACT(DAY FROM STARTDATE) AS ADD_DAYS FROM
                            (
                                SELECT TO_DATE('28/02/2014','DD/MM/YYYY') AS STARTDATE, 1 AS MONTHS_TO_ADD FROM DUAL
                            )T
                        )T
                    )T
                )T;        
            

            查询结果

            开始日期2014 年 2 月 28 日

            MONTHS_TO_ADD 1

            ORACLES_ADD_MONTH_RESULT 2014 年 3 月 31 日

            ADD_DAYS 28

            SUBSTRACT_DAYS 31

            NET_DAYS_TO_ADJUST -3

            MY_ADD_MONTH_RESULT 2014 年 3 月 28 日

            MY_ADD_MONTH_FUNCTION_RESULT 2014 年 3 月 28 日

            【讨论】:

            • 您在这个答案中只有代码。稍微解释一下以获得更好的答案。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2021-04-06
            • 1970-01-01
            • 2015-12-14
            • 2020-02-16
            • 1970-01-01
            • 1970-01-01
            • 2017-11-03
            相关资源
            最近更新 更多