SQLite 与 Oracle - 计算日期差异 - 小时

SQLite vs. Oracle - Calculating date differences - hours

我想知道是否有人看到过这个,是否有解决方案,或者我只是做错了什么。我正在尝试获取现在与数据库记录中的 "created date" 之间的小时差 - 不是试图获取总小时数,而是在你摆脱总天数后剩下的小时数,所以你可以输出一些东西是x 天 x 小时。

初始给定

让我们使用 12/6/2016 6:41 PM 中的 SYSDATE 或 "now"。

假设我有一个 Oracle table 和一个我们称之为 MyTable 的 SQLite table。在其中,我有一个 CREATED_DATE 字段,其中日期存储在本地时间:

CREATED_DATE
------------
1/20/2015 1:35:17 PM
6/9/2016 3:10:46 PM

两个table是相同的,除了它在Oracle中是DATE类型,但在SQLite中,你必须将日期存储为格式为字符串'yyyy-MM-dd HH:mm:ss'。但是每个 table 的值都是相同的。

我开始获取 "now" 和日期之间的总天数差异。我可以从小数天数中减去整数天数,得到我需要的小时数。

总天数 - Oracle

如果我在 Oracle 中这样做,给我总天数差异:
SELECT (SYSDATE - CREATED_DATE) FROM MyTable

第一个得到 686.211284...,第二个得到 180.144976...

总天数 - SQLite

如果我使用 SQLite 执行此操作以获得总天数差异,第一个非常接近,但第二个真的很差:
SELECT (julianday('now') - julianday(CREATED_DATE, 'utc')) FROM MyTable

第一个得到 686.212924....,第二个得到 180.188283...

问题

我在 SQLite 查询中添加了 'utc',因为我知道 julianday() 使用 GMT。否则时间大约是 6 小时。问题是他们现在有 1 小时的休息时间,但并非所有时间都如此。第一个结果给出正确的小时数差异:5,在这两种情况下:

.211284 x 24 = 5.07 hours
.212924 x 24 = 5.11 hours

当我降低这些值时,它会给出我需要的结果。

不过,对于第二个,我得到的是:

.144976 x 24 = 3.479 hours
.188283 x 24 = 4.519 hours

巨大的不同 - 整整一个小时!任何人都可以帮助解释为什么会这样,如果有办法修复 it/make 它准确吗?

获取时间

这是我用来获取小时数的代码。我已使用计算器 double-check 确认我使用 Oracle 时获得的小时数是正确的。为此,我使用:

SELECT FLOOR(((SYSDATE - CREATED_DATE)-(FLOOR(SYSDATE - CREATED_DATE)))*24) FROM MyTable

我目前正在尝试使用类似的设置获取 SQLite 中的小时数:

(((julianday('now') - julianday(CREATED_DATE, 'utc')) - 
CAST ((julianday('now') - julianday(CREATED_DATE, 'utc')) AS INTEGER))*24)

我现在暂时没有对 SQLite 结果进行 "flooring" 或整数转换。这两个查询基本上都是用总天数减去整数总天数得到小数余数(这是一天中代表小时的部分)并将其乘以 24。

这很有趣,因为我在整个小时内使用上面的相同查询减去整数小时的强制转换版本,将小数部分保留在分钟上,然后将其乘以 60,结果就出来了非常适合会议记录。

屏幕截图:并排比较

这是在 12/6/2016 7:20 PM 拍摄的,左侧是我的应用程序中显示的 SQLite,右侧是在 Oracle SQL 中完成的 Oracle 查询 开发人员:

据我了解你的问题,似乎 SQLite 的日光变化丢失了,或者准确地说,你需要在保存时指定这个变化(因为它是一个字符串而不是 true 日期字段/透明时间戳)。

当你生成(?如果你这样做)日期时,使其完整 UTC 时区显式而不是本地隐式:

SQLite Date / Time

Formats 2 through 10 may be optionally followed by a timezone indicator of the form "[+-]HH:MM" or just "Z". The date and time functions use UTC or "zulu" time internally, and so the "Z" suffix is a no-op. Any non-zero "HH:MM" suffix is subtracted from the indicated date and time in order to compute zulu time. For example, all of the following time strings are equivalent:

2013-10-07 08:23:19.120
2013-10-07T08:23:19.120Z
2013-10-07 04:23:19.120-04:00

老实说,utc 的 SQLite 在 "forget" 日光变化时并没有错,因为 UTC 不会移动(这是计时物理时间)。如果你告诉他一切都在同一个 UTC 时区,它只会做一个简单的减法,而不会给出关于你的日光的 ficus

你的 julianday('now') 不也拿 'UTC' 吗? (不好意思,我明天再看)

实际上您错过了一个重要信息:您认为哪个值是正确的?是否必须考虑夏令时?

Oracle 开始:

我假设列 CREATED_DATE 的数据类型是 DATESYSDATE returns 也是一个 DATE 值。 DATE 值没有任何时区(即夏令时设置)信息。

假设 现在 是 2016-12-06 06:00:00:

SELECT 
   TO_DATE('2016-12-06 06:00:00','YYYY-MM-DD HH24:MI:SS') 
   - TO_DATE('2016-06-09 06:00:00','YYYY-MM-DD HH24:MI:SS') 
FROM dual;

returns正好 180 天。

如果必须考虑夏令时,则必须使用数据类型 TIMESTAMP WITH TIME ZONE(或 TIMESTAMP WITH LOCAL TIME ZONE),请参阅此示例:

SELECT 
   TO_TIMESTAMP_TZ('2016-12-06 06:00:00 Europe/Zurich','YYYY-MM-DD HH24:MI:SS TZR') 
   - TO_TIMESTAMP_TZ('2016-06-09 06:00:00 Europe/Zurich','YYYY-MM-DD HH24:MI:SS TZR') 
FROM dual;

结果是 +180 01:00:00.000000,即 180 天零 1 小时。

这取决于你的要求,你必须使用哪一个。一般来说,我建议分别使用 TIMESTAMPTIMESTAMP WITH TIME ZONE 而不是 DATE,因为您可以简单地使用 EXTRACT(datetime) 来获取时间,而不必 fiddle 和 FLOOR 之类的东西:

 SELECT 
    EXTRACT(HOUR FROM SYSTIMESTAMP - CREATED_DATE) AS diff_hours 
FROM MyTable;

注意,LOCALTIMESTAMP returns 一个 TIMESTAMP 值,使用 SYSTIMESTAMP,resp。 CURRENT_TIMESTAMP 获取当前时间作为 TIMESTAMP WITH TIME ZONE 值。

现在考虑 SQLite:

更新

实际上 julianday('now') - julianday(CREATED_DATE, 'utc') 给出了正确的结果 - 或者我们称之为 "precise result"。它考虑了夏令时的变化。例如,'2016-10-31 00:00:00' - '2016-10-30 00:00:00'(欧洲时间)的差异是 25 小时 - 而不是 24 小时!

现在,您想在计算中忽略夏令时变化。对于 Oracle,这很简单,使用 DATETIMESTAMP 数据类型而不是 TIMESTAMP WITH TIME ZONE,然后就完成了。

SQLite 始终考虑时区和夏令时变化,您必须进行一些修改才能绕过它。 我有时间做了一些测试,并且找到了几种方法。

以下方法均适用于我的机器(夏令时设置的瑞士时间,+01:00 和 +02:00)。

  • julianday('now', 'localtime') - julianday(CREATED_DATE)
  • julianday(datetime('now', 'localtime')||'Z') - julianday(CREATED_DATE||'Z')

查看测试用例:

create table t (CREATED_DATE DATE);

insert into t values (datetime('2015-06-01 00:00:00'));
insert into t values (datetime('2015-12-01 00:00:00'));
insert into t values (datetime('2016-06-01 00:00:00'));
insert into t values (datetime('2016-12-01 00:00:00'));

select datetime('now', 'localtime') as now, 
    created_date, 
    julianday('now') - julianday(CREATED_DATE, 'utc') as wrong_delta_days,
    strftime('%j %H:%M:%S', datetime('0000-01-01T00:00:00', '+'||(julianday('now') - julianday(CREATED_DATE, 'utc'))||' day', '-1 day')) as wrong_delta,    

    strftime('%j %H:%M:%S', datetime('0000-01-01T00:00:00', '+'||(julianday('now', 'localtime') - julianday(CREATED_DATE))||' day', '-1 day')) as delta_1, 
    strftime('%j %H:%M:%S',
       datetime('now', 'localtime', 
          '-'||strftime('%Y', CREATED_DATE)||' year', 
          '-'||strftime('%j', CREATED_DATE)||' day', 
          '-'||strftime('%H', CREATED_DATE)||' hour', 
          '-'||strftime('%M', CREATED_DATE)||' minute', 
          '-'||strftime('%S', CREATED_DATE)||' second'
         )) as delta_2,
    strftime('%j %H:%M:%S', datetime('0000-01-01T00:00:00', '+'||(julianday(datetime('now', 'localtime')||'Z') - julianday(CREATED_DATE||'Z'))||' day', '-1 day')) as delta_3
from t;


now                 | CREATED_DATE        | wrong_delta_days | wrong_delta  | delta_1      | delta_2      | delta_3
2016-12-08 08:34:08 | 2015-06-01 00:00:00 | 556.398711088113 | 190 09:34:08 | 190 08:34:08 | 190 08:34:08 | 190 08:34:08
2016-12-08 08:34:08 | 2015-12-01 00:00:00 | 373.357044421136 | 007 08:34:08 | 007 08:34:08 | 007 08:34:08 | 007 08:34:08
2016-12-08 08:34:08 | 2016-06-01 00:00:00 | 190.398711088113 | 190 09:34:08 | 190 08:34:08 | 190 08:34:08 | 190 08:34:08
2016-12-08 08:34:08 | 2016-12-01 00:00:00 | 7.35704442113638 | 007 08:34:08 | 007 08:34:08 | 007 08:34:08 | 007 08:34:08

我使用 strftime('%j %H:%M:%S', datetime('0000-01-01T00:00:00', ..., '-1 day')) 只是为了格式化目的,它不适合跨越超过 1 年的增量。

我知道这根本不是执行此操作的理想方法,但我在提取 SQLite 值的 C# 应用程序中使用补偿器函数自行解决了这个问题。在我写完这篇文章后,我实际上想到我可以在 C# 中重新完成日期减法并覆盖我的 Age 字段!但是由于我只需要修改给定条件的小时数(和天数,如果小时数为 0 并且是 DST 日期),我只是使用了这个。

所以我有一个 class,它将根据我提供给 class 中的 public 静态字符串变量和我调用的另一个函数的查询提供结果数据表。然后,我调用 BindTable() 将 table 绑定到我的 WPF 应用程序中的 ListView 以显示信息。

我通过调用 ADOClass.adoDataTable 拉入了我的 DataTable。获得数据表后,我只需遍历行,将创建日期存储为变量,将年龄字符串(我使用该函数根据需要更新)存储为变量。如果创建日期满足 .IsDaylightSavingTime() 条件,则必须减去一个小时。如果说年龄是 0 小时(和一些奇数分钟),我们必须将日期设置为一天,并将小时设置为 23。

private void BindTable()
{            
    DataTable dt = ADOClass.adoDataTable;

    if (!Oracle_DAL.ConnTest()) // if no Oracle connection, SQLite is running and it needs DST correction
    {
        int oldHours = 0;
        int newHours = 0;
        int oldDays = 0;
        int newDays = 0;
        string ageCell = String.Empty;
        string hoursString = String.Empty;
        string daysString = String.Empty;
        string crDate = String.Empty;

        if (dt != null && dt.Rows != null)
        {
            if (dt.Rows.Count > 0)
            {
                foreach (DataRow dr in dt.Rows)
                {
                    crDate = dr["CREATED_DATE"] != null ? dr["CREATED_DATE"].ToString() : String.Empty;
                    if (!String.IsNullOrEmpty(crDate))
                    {
                        DateTime createdDate = DateTime.Parse(crDate);
                        if (createdDate.IsDaylightSavingTime())
                        {
                            ageCell = dr["AGE"] != null ? dr["AGE"].ToString() : String.Empty;
                            if (!String.IsNullOrEmpty(ageCell))
                            {
                                hoursString = ageCell.Split(',')[3];
                                hoursString = hoursString.TrimStart(' ');
                                oldHours = int.Parse(hoursString.Split(' ')[0]);

                                if (oldHours == 0)
                                {
                                    newHours = 23;
                                    daysString = ageCell.Split(',')[2];
                                    daysString = daysString.TrimStart(' ');
                                    oldDays = int.Parse(daysString.Split(' ')[0]);
                                    oldDays--;
                                    newDays = oldDays;
                                    ageCell = ageCell.Replace(daysString, newDays.ToString() + " days");
                                    dr["AGE"] = ageCell;
                                }
                                else
                                {
                                    oldHours--;
                                    newHours = oldHours;                                    
                                }
                                dr["AGE"] = ageCell.Replace(hoursString, newHours.ToString() + " hours");
                            }
                        }
                    }
                }                        
            }
        }
    }

    lstData.DataContext = dt;  // binds to my ListView's grid
}

(注意: 后来我意识到,因为我在非夏令时测试了这个函数,这是为了解决夏令时日期的问题,当它变成又是夏令时,我相信事情会改变。我想我最终不得不在非夏令时日期的结果中增加一个小时才能使其正常工作,在那个时候。所以如果 DateTime.Now 符合.IsDaylightSavingTime() 以便知道该怎么做。我可能会在那个时候重新访问这个post。)

万一有人好奇..... 这是我在 Oracle 中 运行 的完整查询:

SELECT CREATED_DATE, (SYSDATE - CREATED_DATE) AS TOTALDAYS, 
FLOOR(ABS(MONTHS_BETWEEN(CREATED_DATE, SYSDATE)) / 12) || ' years, '  
|| (FLOOR(ABS(MONTHS_BETWEEN(CREATED_DATE, SYSDATE))) - 
   (FLOOR(ABS(MONTHS_BETWEEN(CREATED_DATE, SYSDATE)) / 12)) * 12) || ' months, '  
-- we take total days - years(as days) - months(as days) to get remaining days
|| FLOOR((SYSDATE - CREATED_DATE) -      -- total days
   (FLOOR((SYSDATE - CREATED_DATE)/365)*12)*(365/12) -      -- years, as days
   -- this is total months - years (as months), to get number of months, 
   -- then multiplied by 30.416667 to get months as days (and remove it from total days)
   FLOOR(FLOOR(((SYSDATE - CREATED_DATE)/365)*12 - (FLOOR((SYSDATE - CREATED_DATE)/365)*12)) * (365/12)))  
|| ' days, '   
-- Here, we can just get the remainder decimal from total days minus 
-- floored total days and multiply by 24       
|| FLOOR(
     ((SYSDATE - CREATED_DATE)-(FLOOR(SYSDATE - CREATED_DATE)))*24
   )
|| ' hours, ' 
-- Minutes just use the unfloored hours equation minus floored hours, 
-- then multiply by 60
|| ROUND(
       (
         (
           ((SYSDATE - CREATED_DATE)-(FLOOR(SYSDATE - CREATED_DATE)))*24
         ) - 
         FLOOR((((SYSDATE - CREATED_DATE)-(FLOOR(SYSDATE - CREATED_DATE)))*24))
       )*60
    )
|| ' minutes'  
AS AGE FROM MyTable`

这是我从我的应用程序中对 SQLite 的最终完整查询:

    private static readonly string mainqueryCommandTextSQLite = "SELECT " + 
        "CREATED_DATE, " +
        " (julianday('now') - julianday(CREATED_DATE, 'utc')) AS TOTALDAYS, " +
        //            " (((julianday('now') - julianday(CREATED_DATE))/365)*12) || ' total months, ' || " +
        //            " ((CAST ((julianday('now') - julianday(CREATED_DATE))/365 AS INTEGER))*12) || ' years as months, ' || " +

        // Provide years, months
        " CAST ((julianday('now') - julianday(CREATED_DATE, 'utc'))/365 AS INTEGER) || ' years, ' || " +
        " CAST (((((julianday('now') - julianday(CREATED_DATE, 'utc'))/365)*12) - (CAST ((julianday('now') - julianday(CREATED_DATE, 'utc'))/365 AS INTEGER)*12)) AS INTEGER)  || ' months, ' " +

        // Provide days
        "|| ((CAST ((julianday('now') - julianday(CREATED_DATE, 'utc')) AS INTEGER) - " +  // total number of days
        " (CAST ((julianday('now') - julianday(CREATED_DATE, 'utc'))/365 AS INTEGER)*365) ) -" + // years in days  
        " CAST((30.41667 * ((CAST ((((julianday('now') - julianday(CREATED_DATE, 'utc'))/365)*12) AS INTEGER)) - ((CAST ((julianday('now') - julianday(CREATED_DATE, 'utc')) / 365 AS INTEGER)) * 12))) AS INTEGER)) " + // days of remaining months using total months - months from # of floored years * (365/12)
        " || ' days, ' " +

        // BUG:  These next two do not get accurate hours during DST months (March - Nov)
        // This gives hours
        "|| CAST ((((julianday('now') - julianday(CREATED_DATE, 'utc')) - " +
        " CAST ((julianday('now') - julianday(CREATED_DATE, 'utc')) AS INTEGER))*24) AS INTEGER) " +

        // This gives hours.minutes
        //"|| (((julianday('now') - julianday(CREATED_DATE, 'utc')) - CAST ((julianday('now') - julianday(CREATED_DATE, 'utc')) AS INTEGER))*24) " +

        // This gives days.hours, but taking the decimal and multiplying by 24 to get actual hours 
        // gives an incorrect result
        //"|| ((" +
        //        "(0.0 + strftime('%S', 'now', 'localtime') " +
        //        "+ 60*strftime('%M', 'now', 'localtime') " +
        //        "+ 24*60*strftime('%H', 'now', 'localtime') " +
        //        "+ 24*60*60*strftime('%j', 'now', 'localtime')) - " +
        //        "(strftime('%S', CREATED_DATE) " +
        //        "+ 60*strftime('%M', CREATED_DATE) " +
        //        "+ 24*60*strftime('%H', CREATED_DATE) " +
        //        "+ 24*60*60*strftime('%j', CREATED_DATE)) " +
        //    ")/60/60/24) " +
        "|| ' hours, ' " +

        // Provide minutes
        "|| CAST (ROUND(((((julianday('now') - julianday(CREATED_DATE, 'utc')) - CAST ((julianday('now') - julianday(CREATED_DATE, 'utc')) AS INTEGER))*24) - " +
        "(CAST((((julianday('now') - julianday(CREATED_DATE, 'utc')) - CAST((julianday('now') - julianday(CREATED_DATE, 'utc')) AS INTEGER)) * 24) AS INTEGER)))*60) AS INTEGER)" +
        "|| ' minutes' " +
        " AS AGE FROM MyTable";

以及新的屏幕截图,显示匹配的所有内容(总天数除外,我可以通过从我的 C# 函数中减去 1/24 来更改并以相同的方式更新 DST 日期):

更新

由于 Wernfried 在 SQLite 中发现了 2 个查询否定了对这个函数的需求,我将接受关于如何真正解决这个问题的答案:

对于 Oracle -

  • SELECT (SYSDATE - CREATED_DATE) FROM MyTable

或使用 to_date 格式语法对于获取 days.hours 和进行转换很有帮助。取小数部分并乘以 24 对小时有好处,并且与 DST 无关,就像我想要的那样。请参阅上面的完整查询,我将其格式化为年、月、日、小时和分钟。

对于 SQLite -

正如 Wernfried 发现的那样,以下任一方法都有效:

julianday('now', 'localtime') - julianday(CREATED_DATE)

julianday(datetime('now', 'localtime')||'Z') - julianday(CREATED_DATE||'Z')

这避免了我上面的函数的需要。

如果您使用:

julianday('now') - julianday(CREATED_DATE, 'utc')

就像我在上面的早期代码中所做的那样,那么您将需要我的 DST 补偿器函数,在上面更远的地方。