【问题标题】:If I add 360 months to begin_date, How can I accurately obtain end_date using a lookup dimension table?如果我将 360 个月添加到 begin_date,如何使用查找维表准确获取 end_date?
【发布时间】:2012-07-04 03:04:41
【问题描述】:

编辑:begin_date 和 end_date 是任何表中的类型 DATE 列。

我有以下维度表,它提供了从 1980 年到 2500 年每个月的总天数:

CREATE TABLE total_days
    (
from_date     DATE,
to_date       DATE,
days_in_month SMALLINT
    );

from_date   to_date     days_in_month
1980-01-01  1980-01-31  31
1980-02-01  1980-02-29  29
...
2500-11-01  2500-11-30  30
2500-12-01  2500-12-31  31

如果我要向 begin_date 添加 360 个月,我应该如何构造 SQL 查询以获得准确的 end_date?.. 我是否需要以任何方式更改维度表才能实现我的目标?

编辑:必须在不使用任何本机 SQL 日期算术函数的情况下执行日期算术。必须通过在维度表中查找 begin_date 来完成。

【问题讨论】:

  • 参数是什么?日期和360 months?
  • 是的,将 360 个月添加到 begin_date 并使用事实表确定 end_date。 begin_date 是任何其他表中的列。

标签: sql database date informix date-arithmetic


【解决方案1】:

我猜你有你的理由 - 这是一个非常简单的 hack - 假设事实表每个月都有一行 - 添加一个表示月份编号的新列,从 1 开始并按时间顺序自动递增,从每年开始。

SELECT B.* 
FROM SO_total_days2 A
INNER JOIN SO_total_days2 B ON B.monthnumber = A.monthnumber + 360
WHERE A.from_date =  '2010-01-01'

from_date   to_date     days_in_month monthnumber
1980-01-01  1980-01-31  31    1
1980-02-01  1980-02-29  29    2
1980-03-03  1980-03-31  31    3
...
1981-01-01  1981-01-31  31    13
1981-12-01  1981-12-31  31    24
...
1985-01-01  1985-01-31  31    49
1985-12-01  1985-12-31  31    60

【讨论】:

  • 我的想法完全正确!如果日期算术被禁止,则只需将其替换为不同类型的算术(在这种情况下为整数)。
  • 原因?.. 1)原生SQL月份函数不准确,有些甚至产生无效日期。 2)本机功能不可移植。 3)事实表允许定义规则来控制结果。 4)事实表的性能更快?
  • @Frank:事实表不可能比日期算术更快。最新的 Informix 服务器(11.70.xC5 或更高版本)中的 DATETIME 算法进行了修订,因此如果您将 N 个月添加到日期,它将生成该月的最后一天,而不是无效日期。代价是,如果你这样做,例如,DATE('2012-01-31') + 1 UNITS MONTH 然后减去1 UNITS MONTH,你会得到2012-01-29。也就是说,加 N 个月并从结果中减去 N 个月(或先减后加)不一定是恒等运算。 ADD_MONTHS() 函数也适用于早期版本。
  • 我不会开始认为我可以提出比已经存在 x 年、由 x 个用户测试并改进/改进的功能更快/更好的东西。话虽如此,我不必处理多个 RDBM。我在日期函数中看到的问题来自于 progr 的误用/理解,而不是函数 - 所以请加入多数据库供应商。我们已经看到人们从 ORACLE 迁移到 SQL,并且日期函数可能是一个转换/大学习曲线,其行为与开发人员的预期不同。在我们看来是对是错,问题是如何在没有他们的情况下解决任务。
  • @Frank,是的 ADD_MONTHS() 支持 DATE 和 DATETIME 类型。
【解决方案2】:

如果我要与数据库无关,我会稍微更改一下事实表:

CREATE TABLE total_days
(
  year          INT,
  month         TINYINT,
  from_date     DATE,
  to_date       DATE,
  days_in_month SMALLINT
);

year  month  from_date   to_date     days_in_month
------------------------------------------------
1980    1    1980-01-01  1980-01-31   31
1980    2    1980-02-01  1980-02-29   29
...
2500   11    2500-11-01  2500-11-30   30
2500   12    2500-12-01  2500-12-31   31

然后你可以使用类似的东西:

SELECT td.* 
FROM 
      total_days AS td 
   CROSS JOIN
      ( SELECT year, month
        FROM total_days 
        WHERE from_date <= @StartingDate 
          AND @StartingDate <= to_date
      ) AS st
   CROSS JOIN
      ( SELECT 360 AS add_months ) AS param
WHERE td.year = st.year + ( st.month -1 + add_months ) / 12
  AND td.month = 1 + ( st.month - 1 + add_months ) % 12 )
;

或更简单(但更难优化效率:

WHERE 12 * td.year + td.month = 
      12 * st.year + st.month + add_months

【讨论】:

  • 你在正确的轨道上!.. 将 year_month 作为一列会更好,例如198001?.. 所以你是说添加年和月列作为索引方法来确定 begin_date 属于哪个年/月,然后添加该特定年/月的 360 个月?
  • 是的,我不希望在一个列中包含月份和年份。它会使计算更加复杂
  • 如果您想要一列,请将 (12*year + month) 添加为硬列或计算(持久)列,以便您对其进行索引。
  • 好的,谢谢!.. 我觉得这是一种更好的方法,可以更准确地到达 end_date,而不是使用内置 SQL 函数,我不确定不同的数据库供应商如何实现他们的逻辑。它还使代码更便携!.. 另一种选择是将每个月定义为 30.4375 天(365.25 天/12 个月)并将结果四舍五入到最接近的日期,但这可能不够准确!
【解决方案3】:

这就是我想象你的“事实表”的样子:

declare @dt datetime
    set @dt = '7-1-2012'

;
with date_table as  (
        select  @dt as [Start Date],
                dateadd(d,-1,dateadd(mm,1,@dt)) as [End Date],
                datepart(d,dateadd(d,-1,dateadd(mm,1,@dt))) as [Days]

        union ALL

        select  dateadd(mm, 1, [Start Date]) as  [Start Date],
                dateadd(d,-1,dateadd(mm,1,dateadd(mm, 1, [Start Date]))) as [End Date], 
                datepart(d,dateadd(d,-1,dateadd(mm,1,dateadd(mm, 1, [Start Date])))) as [Days]

        from    date_table
        where   dateadd(mm, 1, [Start Date]) <= dateadd(m,500,@dt))

select  [Start Date], [End Date], [Days]
into    #temp
from    date_table

option (MAXRECURSION 0)

这是选择日期。 (注意这些语句中没有包含 DATEADD 或 DATEPART)

select  finish.[Start Date], finish.[End Date], finish.[Days]
from    (select rownum
         from   (select [Start Date], [End Date], [Days], row_number() over (order by [Start Date]) as rownum
                    from    #temp) as x
         where  x.[Start Date] = '2012-07-01 00:00:00.000' ) as start 

        join    (select [Start Date], [End Date], [Days], 
                        row_number() over (order by [Start Date]) as rownum
                 from   #temp) as finish
            on finish.rownum = start.rownum + 360

我在下面阅读了您的 cmets...如果您想总结日子或其他事情,您可以这样做:(因此从 2012 年 7 月 1 日开始,持续 360 个月... date_diff_days结果将是 360 个月的总天数...使用我制作的 #temp 表...我假设它与您的事实表相似...我得到了 10957 天)

select  sum(dayscount.[Days]) as date_diff_days
from    (select rownum
         from   (select [Start Date], [End Date], [Days], row_number() over (order by [Start Date]) as rownum
                    from    #temp) as x
         where  x.[Start Date] = '2012-07-01 00:00:00.000' ) as start 

        join    (select [Start Date], [End Date], [Days], 
                        row_number() over (order by [Start Date]) as rownum
                 from   #temp) as finish
            on finish.rownum = start.rownum + 360

        join (select    [Start Date], [End Date], [Days], 
                        row_number() over (order by [Start Date]) as rownum
                 from   #temp) as dayscount
            on dayscount.rownum >= start.rownum and 
                dayscount.rownum < finish.rownum 

【讨论】:

  • 不,必须在不使用任何本机 SQL 日期算术函数(如您在示例中使用的 DATEADD 或 DATEPART)的情况下完成。必须使用我在问题中描述的事实表来完成。
  • 我没有在实际的选择语句中使用 DATEADD 或 DATEPART。它们仅用于我构建与您的“事实表”等效的临时表的地方。看到 CTE 之外的两个 select 语句了吗?
【解决方案4】:

为什么是事实表?大多数数据库对日期/时间操作都有本机支持。在 MS SQL Server 中,您可以使用 DATEADD 执行此操作。

我看到您用“informix”标记了您的问题,但没有在您的问题中指定任何版本详细信息。 Here's an ADD_MONTHS function from IBM Informix 11.50.

【讨论】:

  • 一些数据库不提供有效日期,如 ADD_MONTHS() 或 MONTHS_BETWEEN()。我宁愿使用事实表,也不愿依赖任何内置的 SQL 日期算术函数。这样一来,DBs 版本就无关紧要了。
  • 嗯,对于初学者来说,添加月数是一个模棱两可的操作。 “一个月”有多长?如您的事实表所示,任何给定的月份都可能不同。如果您从 1 月 31 日开始并在 2 月结束,该怎么办?你应该回到28号吗?如果是闰年呢?有不同的内涵,所以这就是人们回归内置函数的原因。至少在那里,你有一些标准化。如果你要自己做,你必须考虑所有这些事情,而且它会很快变得复杂,并可能导致意想不到的错误。
  • 这就是为什么我使用一个包含从 1980 年到 2500 年每个月的总天数的事实查找表。由于月份的天数是可变的,我将我的日期算术基于添加 360 个月中的总天数,具体取决于 begin_date 是什么。
  • 然后对您的事实表进行查询,以将 >= 您的开始日期的前 x 行中的天数相加,并将结果添加到您的开始日期。
【解决方案5】:

你可以使用interval

mysql> SELECT '2008-12-31 23:59:59' + INTERVAL 1 month;
+------------------------------------------+
| '2008-12-31 23:59:59' + INTERVAL 1 month |
+------------------------------------------+
| 2009-01-31 23:59:59                      |
+------------------------------------------+
1 row in set (0.00 sec)


mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2012-07-03 12:27:46 |
+---------------------+
1 row in set (0.00 sec)

mysql> SELECT now() + INTERVAL 30 month;
+---------------------------+
| now() + INTERVAL 30 month |
+---------------------------+
| 2015-01-03 12:27:49       |
+---------------------------+
1 row in set (0.00 sec)

编辑:

mysql> SELECT STR_TO_DATE('01,5,2013','%d,%m,%Y') + interval 30 month;
+---------------------------------------------------------+
| STR_TO_DATE('01,5,2013','%d,%m,%Y') + interval 30 month |
+---------------------------------------------------------+
| 2015-11-01                                              |
+---------------------------------------------------------+
1 row in set (0.00 sec)

编辑 2:

mysql> show create table tiempo;
+--------+------------------------------------------------------------------------------------------------+
| Table  | Create Table                                                                                   |
+--------+------------------------------------------------------------------------------------------------+
| tiempo | CREATE TABLE `tiempo` (
  `fecha` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+--------+------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select fecha + interval 20 month from tiempo;
+---------------------------+
| fecha + interval 20 month |
+---------------------------+
| NULL                      |
| 2001-10-02 02:02:02       |
+---------------------------+
2 rows in set (0.00 sec)

【讨论】:

  • 无法使用INTERVAL,数据为DATE类型,无法转换为DATETIME。
【解决方案6】:

如果您的起始日期与列出的完全相同,并且您总是添加月份,您可以使用这个吗:

SELECT max (to_date)
  FROM (SELECT ROW_NUMBER () OVER (ORDER BY from_date) AS Row,
                from_date,
                to_date,
                days_in_month
         FROM total_days
        WHERE from_date > '1/1/1982'
      GROUP BY from_date, to_date, days_in_month) MyDates
 WHERE Row <= 360

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-04-29
    • 2020-10-14
    • 2020-07-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-23
    相关资源
    最近更新 更多