如何存储日期范围(实际上是时间戳)

发布于 2024-07-06 15:12:13 字数 957 浏览 8 评论 0 原文

Java& Oracle 都有一个名为 Date 的时间戳类型。 开发人员倾向于将这些日期视为日历日期,我已经看到这会导致令人讨厌的一次性错误。

  1. 对于基本日期量,您可以简单地在输入时截掉时间部分,即降低精度。 但如果您使用日期范围(例如:9/29-9/30)执行此操作,则这两个值之间的差异为 1 天,而不是 2 天。此外,范围比较需要 1 ) 截断操作: start < trunc(now) <= end,或 2) 算术:start < 现在< (结束 + 24 小时)。 不可怕,但也不是

  2. 另一种方法是使用真实时间戳:9/29 00:00:00 - 10/1 00:00:00。 (午夜到午夜,因此不包括十月的任何部分)。 现在,持续时间本质上是正确的,并且范围比较更简单:start <= now start <= now start <= now start <= now start <= now start <= now start <= now start 结束。 对于内部处理来说当然更干净,但是结束日期确实需要在初始输入 (+1) 和输出 (-1) 时进行转换,假设用户级别的日历日期隐喻。

您如何处理项目的日期范围? 还有其他选择吗? 我特别感兴趣的是您如何在 Java 和 Oracle 方面处理这个问题。

Java & Oracle both have a timestamp type called Date. Developers tend to manipulate these as if they were calendar dates, which I've seen cause nasty one-off bugs.

  1. For a basic date quantity you can simply chop off the time portion upon input, i.e., reduce the precision. But if you do that with a date range, (e.g.: 9/29-9/30), the difference between these two values is 1 day, rather than 2. Also, range comparisons require either 1) a truncate operation: start < trunc(now) <= end, or 2) arithmetic: start < now < (end + 24hrs). Not horrible, but not DRY.

  2. An alternative is to use true timestamps: 9/29 00:00:00 - 10/1 00:00:00. (midnight-to-midnight, so does not include any part of Oct). Now durations are intrinsically correct, and range comparisons are simpler: start <= now < end. Certainly cleaner for internal processing, however end dates do need to be converted upon initial input (+1), and for output (-1), presuming a calendar date metaphor at the user level.

How do you handle date ranges on your project? Are there other alternatives? I am particularly interested in how you handle this on both the Java and the Oracle sides of the equation.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

爱*していゐ 2024-07-13 15:12:13

我们是这样做的。

  1. 使用时间戳。

  2. 使用半开区间进行比较:start <= now < end

忽略那些坚持认为 BETWEEN 在某种程度上对于成功的 SQL 至关重要的抱怨者。

有了这个一系列的日期范围就很容易审核。 9/30 到 10/1 的数据库值包含一天 (9/30)。 下一个间隔的开始必须等于前一个间隔的结束。 interval[n-1].end == Interval[n].start 规则对于审核来说很方便。

显示时,如果愿意,可以显示格式化的startend-1。 事实证明,你可以教育人们理解“结束”实际上是规则不再成立的第一天。 所以“9/30 to 10/1”的意思是“从9/30开始有效,从10/1开始不再有效”。

Here's how we do it.

  1. Use timestamps.

  2. Use Half-open intervals for comparison: start <= now < end.

Ignore the whiners who insist that BETWEEN is somehow essential to successful SQL.

With this a series of date ranges is really easy to audit. The database value for 9/30 to 10/1 encompass one day (9/30). The next interval's start must equal the previous interval's end. That interval[n-1].end == interval[n].start rule is handy for audit.

When you display, if you want, you can display the formatted start and end-1. Turns out, you can educate people to understand that the "end" is actually the first day the rule is no longer true. So "9/30 to 10/1" means "valid starting 9/30, no longer valid starting 10/1".

初相遇 2024-07-13 15:12:13

Oracle 具有 TIMESTAMP 数据类型。 它存储 DATE 数据类型的年、月和日,以及小时、分钟、秒和小数秒值。

这是一个 asktom 上的线程。 oracle.com 有关日期算术的信息。

Oracle has the TIMESTAMP datatype. It stores the year, month, and day of the DATE datatype, plus hour, minute, second and fractional second values.

Here is a thread on asktom.oracle.com about date arithmetic.

另类 2024-07-13 15:12:13

我赞同 S.Lott 的解释。 我们有一个产品套件,它广泛使用日期时间范围,这是我们处理此类范围的经验教训之一。 顺便说一句,如果结束日期不再是范围的一部分(IOW,半开区间),我们将其称为独占结束日期。 相反,如果它算作范围的一部分,则它是一个包含的结束日期,只有在没有时间部分的情况下才有意义。

用户通常期望包含日期范围的输入/输出。 无论如何,应尽快将用户输入转换为独占结束日期范围,并在必须向用户显示时尽可能晚地转换任何日期范围。

在数据库中,始终存储排他的结束日期范围。 如果存在具有包含结束日期范围的旧数据,请尽可能将它们迁移到数据库上,或者在读取数据时尽快转换为不包含结束日期范围。

I second what S.Lott explained. We have a product suite which makes extensive use of date time ranges and it has been one of our lessons-learned to work with ranges like that. By the way, we call the end date exclusive end date if it is not part of the range anymore (IOW, a half open interval). In contrast, it is an inclusive end date if it counts as part of the range which only makes sense if there is no time portion.

Users typically expect input/output of inclusive date ranges. At any rate, convert user input as soon as possible to exclusive end date ranges, and convert any date range as late as possible when it has to be shown to the user.

On the database, always store exclusive end date ranges. If there is legacy data with inclusive end date ranges, either migrate them on the DB if possible or convert to exclusive end date range as soon as possible when the data is read.

一身软味 2024-07-13 15:12:13

我使用 Oracle 的日期数据类型,并向开发人员介绍影响边界条件的时间分量问题。

数据库约束还可以防止在不应包含时间分量的列中意外指定时间分量,并且还告诉优化器所有值都没有时间分量。

例如,约束 CHECK (MY_DATE=TRUNC(MY_DATE)) 可防止将时间不是 00:00:00 的值放入 my_date 列中,并且还允许 Oracle 推断诸如 MY_DATE = TO_DATE(' 2008-09-12 15:00:00') 永远不会为 true,因此不会从表中返回任何行,因为它可以扩展为:

MY_DATE = TO_DATE('2008-09-12 15:00:00') AND
TO_DATE('2008-09-12 15:00:00') = TRUNC(TO_DATE('2008-09-12 15:00:00'))

这当然自动为 false。

尽管有时很容易将日期存储为数字(例如 20080915),但这可能会导致查询优化问题。 例如,20,071,231 和 20,070,101 之间有多少个合法值? 2007 年 12 月 31 日至 2008 年 1 月 1 日之间怎么样? 它还允许输入非法值,例如 20070100。

因此,如果您的日期没有时间部分,则定义范围变得很容易:

select ...
from   ...
where  my_date Between date '2008-01-01' and date '2008-01-05'

当有时间部分时,您可以执行以下操作之一:

select ...
from   ...
where  my_date >= date '2008-01-01' and
       my_date  < date '2008-01-06'

select ...
from   ...
where  my_date Between date '2008-01-01'
                   and date '2008-01-05'-(1/24/60/60)

注意 (1 /24/60/60) 而不是幻数。 在 Oracle 中,通过添加一天的定义小数部分来执行日期算术是很常见的……3/24 表示三个小时,27/24/60 表示 27 分钟。 这种类型的 Oracle 数学是精确的,不会出现舍入错误,因此:

select 27/24/60 from dual;

... 给出 0.01875,而不是 0.01874999999999 或其他值。

I use Oracle's date data type and educate developers on the issue of time components affecting boundary conditions.

A database constraint will also prevent the accidental specification of a time component in a column that should have none and also tells the optimizer that none of the values have a time component.

For example, the constraint CHECK (MY_DATE=TRUNC(MY_DATE)) prevents a value with a time other than 00:00:00 being placed into the my_date column, and also allows Oracle to infer that a predicate such as MY_DATE = TO_DATE('2008-09-12 15:00:00') will never be true, and hence no rows will be returned from the table because it can be expanded to:

MY_DATE = TO_DATE('2008-09-12 15:00:00') AND
TO_DATE('2008-09-12 15:00:00') = TRUNC(TO_DATE('2008-09-12 15:00:00'))

This is automatically false of course.

Although it is sometimes tempting to store dates as numbers such as 20080915 this can cause query optimization problems. For example, how many legal values are there between 20,071,231 and 20,070,101? How about between the dates 31-Dec-2007 abnd 01-Jan-2008? It also allows illegal values to be entered, such as 20070100.

So, if you have dates without time components then defining a range becomes easy:

select ...
from   ...
where  my_date Between date '2008-01-01' and date '2008-01-05'

When there is a time component you can do one of the following:

select ...
from   ...
where  my_date >= date '2008-01-01' and
       my_date  < date '2008-01-06'

or

select ...
from   ...
where  my_date Between date '2008-01-01'
                   and date '2008-01-05'-(1/24/60/60)

Note the use of (1/24/60/60) instead of a magic number. It's pretty common in Oracle to perform date arithmetic by adding defined fractions of a day ... 3/24 for three hours, 27/24/60 for 27 minutes. Oracle math of this type is exact and doesn't suffer rounding errors, so:

select 27/24/60 from dual;

... gives 0.01875, not 0.01874999999999 or whatever.

黑色毁心梦 2024-07-13 15:12:13

我还没有看到发布的间隔数据类型。

Oracle 还提供适合您具体场景的数据类型。 Oracle 中也有 INTERVAL YEAR TO MONTH 和 INTERVAL DAY TO SECOND 数据类型。

来自 10gR2 文档。

INTERVAL YEAR TO MONTH 存储一个周期
使用 YEAR 和 MONTH 的时间
日期时间字段。 该数据类型是
对于表示差异很有用
仅当两个日期时间值之间
年和月的值是
意义重大。

间隔年 [(year_ precision)] TO

其中year_ precision 是
YEAR 日期时间字段中的数字。 这
year_ precision的默认值为2。

INTERVAL DAY TO SECOND 数据类型

INTERVAL DAY TO SECOND 存储一个句点
以天、小时为单位的时间,
分钟和秒。 该数据类型是
对于表示精确的有用
两个日期时间之间的差异
值。

按如下方式指定此数据类型:

间隔天 [(day_ precision)] 至
第二
[(fractional_seconds_ precision)]

哪里

day_ precision 是位数
在 DAY 日期时间字段中。 公认
值为 0 到 9。默认值为 2。

fractional_seconds_ precision 是
小数的位数
第二个日期时间字段的一部分。
接受的值为 0 到 9。
默认值为 6。

你有很大的灵活性
当将间隔值指定为
文字。 请参阅“间隔
文字”以获取详细信息
将间隔值指定为文字。
另请参阅“日期时间和间隔
示例”的示例使用
间隔。

I don't see the Interval datatypes posted yet.

Oracle also has datatypes for your exact scenario. There are INTERVAL YEAR TO MONTH and INTERVAL DAY TO SECOND datatypes in Oracle as well.

From the 10gR2 docs.

INTERVAL YEAR TO MONTH stores a period
of time using the YEAR and MONTH
datetime fields. This datatype is
useful for representing the difference
between two datetime values when only
the year and month values are
significant.

INTERVAL YEAR [(year_precision)] TO
MONTH

where year_precision is the number of
digits in the YEAR datetime field. The
default value of year_precision is 2.

INTERVAL DAY TO SECOND Datatype

INTERVAL DAY TO SECOND stores a period
of time in terms of days, hours,
minutes, and seconds. This datatype is
useful for representing the precise
difference between two datetime
values.

Specify this datatype as follows:

INTERVAL DAY [(day_precision)] TO
SECOND
[(fractional_seconds_precision)]

where

day_precision is the number of digits
in the DAY datetime field. Accepted
values are 0 to 9. The default is 2.

fractional_seconds_precision is the
number of digits in the fractional
part of the SECOND datetime field.
Accepted values are 0 to 9. The
default is 6.

You have a great deal of flexibility
when specifying interval values as
literals. Please refer to "Interval
Literals" for detailed information on
specify interval values as literals.
Also see "Datetime and Interval
Examples" for an example using
intervals.

花海 2024-07-13 15:12:13

根据我的经验,主要有四种方法:

1)将日期转换为纪元整数(自 1970 年 1 月 1 日以来的秒数)并将其作为整数存储在数据库中。

2)将日期转换为YYYYMMDDHHMMSS整数,并将其作为整数存储在数据库中。

3) 将其存储为日期

4) 将其存储为字符串

我一直坚持使用 1 和 2,因为它使您能够对日期执行快速而简单的算术,而不依赖于底层数据库功能。

Based on my experiences, there are four main ways to do it:

1) Convert the date to an epoch integer (seconds since 1st Jan 1970) and store it in the database as an integer.

2) Convert the date to a YYYYMMDDHHMMSS integer and store it in the database as an integer.

3) Store it as a date

4) Store it as a string

I've always stuck with 1 and 2, because it enables you to perform quick and simple arithmetic with the date and not rely on the underlying database functionality.

池木 2024-07-13 15:12:13

根据您的第一句话,您偶然发现了 Java 的一个隐藏“功能”(即错误):java.util.Date 应该是不可变的,但事实并非如此。 (Java 7 承诺通过新的日期/时间 API 来解决此问题。)几乎每个企业应用程序都依赖于各种 时间模式,并且在某些时候您将需要对日期和时间进行算术运算。

理想情况下,您可以使用 Google 日历使用的 Joda 时间。 如果你做不到这一点,我猜想一个 API 由一个围绕 java.util.Date 的包装器组成,其计算方法类似于 Grails/Rails,以及一系列包装器(即有序的对指示一个时间段的开始和结束)就足够了。

在我当前的项目(HR 计时应用程序)中,我们尝试将 Oracle 和 Java 的所有日期标准化为同一时区。 幸运的是,我们的本地化要求很轻(= 1 个时区就足够了)。 当持久对象不需要比一天更精细的精度时,我们使用午夜的时间戳。 我会更进一步,坚持将额外的毫秒丢弃到持久对象可以容忍的最粗粒度(这将使您的处理更简单)。

Based upon your first sentence, you're stumbling upon one of the hidden "features" (i.e. bugs) of Java: java.util.Date should have been immutable but it ain't. (Java 7 promises to fix this with a new date/time API.) Almost every enterprise app counts on various temporal patterns, and at some point you will need to do arithmetic on date and time.

Ideally, you could use Joda time, which is used by Google Calendar. If you can't do this, I guess an API that consists of a wrapper around java.util.Date with computational methods similar to Grails/Rails, and of a range of your wrapper (i.e. an ordered pair indicating the start and end of a time period) will be sufficient.

On my current project (an HR timekeeping application) we try to normalize all our Dates to the same timezone for both Oracle and Java. Fortunately, our localization requirements are lightweight (= 1 timezone is enough). When a persistent object doesn't need finer precision than a day, we use the timestamp as of midnight. I would go further and insist upon throwing away the extra milli-seconds to the coarsest granularity that a persistent object can tolerate (it will make your processing simpler).

伪装你 2024-07-13 15:12:13

通过将 getTime() 的结果存储为长整数,所有日期都可以明确存储为 GMT 时间戳(即没有时区或夏令时问题)。

如果数据库查询中需要进行日、周、月等操作,并且当查询性能至关重要时,可以将时间戳(标准化为比毫秒更高的粒度)链接到包含当天列的日期细分表、周、月等值,以便在查询中不必使用昂贵的日期/时间函数。

All dates can be unambiguously stored as GMT timestamps (i.e. no timezone or daylight saving headaches) by storing the result of getTime() as a long integer.

In cases where day, week, month, etc. manipulations are needed in database queries, and when query performance is paramount, the timestamps (normalized to a higher granularity than milliseconds) can be linked to a date breakdown table that has columns for the day, week, month, etc. values so that costly date/time functions don't have to be used in queries.

秋日私语 2024-07-13 15:12:13

艾伦是对的——乔达时间很棒。 java.util.Date 和 Calendar 真是太可惜了。

如果您需要时间戳,请使用带有时间的 Oracle 日期类型,请使用某种后缀(如 _tmst)命名该列。 当您将数据读入java时,将其放入joda时间DateTime对象中。 为了确保时区正确,请考虑 oracle 中存在特定的数据类型,它们将存储带有时区的时间戳。 或者您可以在表中创建另一列来存储时区 ID。 时区 ID 的值应该是时区的标准全名 ID,请参阅 http://java.sun.com/j2se/1.4.2/docs/api/java/util/TimeZone.html#getTimeZone%28java.lang.String% 29.. 如果您对 TZ dta 使用另一列,那么当您将数据读入 java 时,请使用 DateTime 对象,但使用 .withZoneRetainFields 设置时区。

如果您只需要日期数据(无时间戳),则使用数据库中不带时间的日期类型。 再次命名好。 在本例中,使用 jodatime 中的 DateMidnight 对象。

底线:利用数据库的类型系统和您正在使用的语言。 学习它们并获得具有表现力的 API 和语言语法来处理您的问题的好处。

Alan is right- Joda time is great. java.util.Date and Calendar are just a shame.

If you need timestamps use the oracle date type with the time, name the column with some kind of suffix like _tmst. When you read the data into java get it into a joda time DateTime object. to make sure the timezone is right consider that there are specific data types in oracle that will store the timestamps with the timezone. Or you can create another column in the table to store the timezone ID. Values for the timezone ID should be standard full name ID for Timezones see http://java.sun.com/j2se/1.4.2/docs/api/java/util/TimeZone.html#getTimeZone%28java.lang.String%29 . If you use another column for the TZ dta then when you read the data into java use DateTime object but set the timezone on the DateTime object using the .withZoneRetainFields to set the timezone.

If you only need the date data (no timestamp) then use the date type in the database with no time. again name it well. in this case use DateMidnight object from jodatime.

bottom line: leverage the type system of the database and the language you are using. Learn them and reap the benefits of having expressive api and language syntax to deal with your problem.

﹎☆浅夏丿初晴 2024-07-13 15:12:13

更新:Joda-Time 项目现在处于维护模式。 其团队建议迁移到 Java 内置的 java.time 类。

Joda-Time

Joda-Time 提供 3 个类来表示时间跨度:Interval、Duration 和 period。

ISO 8601 标准指定如何格式化表示 Duration间隔。 Joda-Time 解析并生成此类字符串。

时区是一个重要的考虑因素。 您的数据库应以 UTC 格式存储其日期时间值。 但您的业务逻辑可能需要考虑时区。 “一天”的开始取决于时区。 顺便说一句,请使用正确的时区名称而不是 3 或 4 个字母的代码。

S.Lott 的正确答案明智地建议使用半开放逻辑,因为这通常最适合日期时间工作。 时间跨度的开始是包含,而结束是排除。 Joda-Time 在其方法中使用半开放逻辑。

定义一周大于或等于第 1 天且小于第 8 天的图表

DateTimeZone timeZone_NewYork = DateTimeZone.forID( "America/New_York" );
DateTime start = new DateTime( 2014, 9, 29, 15, 16, 17, timeZone_NewYork );
DateTime stop = new DateTime( 2014, 9, 30, 1, 2, 3, timeZone_NewYork );

int daysBetween = Days.daysBetween( start, stop ).getDays();

Period period = new Period( start, stop );

Interval interval = new Interval( start, stop );
Interval intervalWholeDays = new Interval( start.withTimeAtStartOfDay(), stop.plusDays( 1 ).withTimeAtStartOfDay() );

DateTime lateNight29th = new DateTime( 2014, 9, 29, 23, 0, 0, timeZone_NewYork );
boolean containsLateNight29th = interval.contains( lateNight29th );

转储到控制台...

System.out.println( "start: " + start );
System.out.println( "stop: " + stop );
System.out.println( "daysBetween: " + daysBetween );
System.out.println( "period: " + period ); // Uses format: PnYnMnDTnHnMnS
System.out.println( "interval: " + interval );
System.out.println( "intervalWholeDays: " + intervalWholeDays );
System.out.println( "lateNight29th: " + lateNight29th );
System.out.println( "containsLateNight29th: " + containsLateNight29th );

运行时...

start: 2014-09-29T15:16:17.000-04:00
stop: 2014-09-30T01:02:03.000-04:00
daysBetween: 0
period: PT9H45M46S
interval: 2014-09-29T15:16:17.000-04:00/2014-09-30T01:02:03.000-04:00
intervalWholeDays: 2014-09-29T00:00:00.000-04:00/2014-10-01T00:00:00.000-04:00
lateNight29th: 2014-09-29T23:00:00.000-04:00
containsLateNight29th: true

UPDATE: The Joda-Time project is now in maintenance mode. Its team advises migration to the java.time classes built into Java.

Joda-Time

Joda-Time offers 3 classes for representing a span of time: Interval, Duration, and Period.

The ISO 8601 standard specifies how to format strings representing a Duration and an Interval. Joda-Time both parses and generates such strings.

Time zone is a crucial consideration. Your database should be storing its date-time values in UTC. But your business logic may need to consider time zones. The beginning of a "day" depends on time zone. By the way, use proper time zone names rather than 3 or 4 letter codes.

The correct answer by S.Lott wisely advises to use Half-Open logic, as that usually works best for date-time work. The beginning of a span of time is inclusive while the ending is exclusive. Joda-Time uses half-open logic in its methods.

diagram defining a week as greater than or equal to Day 1 and less than Day 8

DateTimeZone timeZone_NewYork = DateTimeZone.forID( "America/New_York" );
DateTime start = new DateTime( 2014, 9, 29, 15, 16, 17, timeZone_NewYork );
DateTime stop = new DateTime( 2014, 9, 30, 1, 2, 3, timeZone_NewYork );

int daysBetween = Days.daysBetween( start, stop ).getDays();

Period period = new Period( start, stop );

Interval interval = new Interval( start, stop );
Interval intervalWholeDays = new Interval( start.withTimeAtStartOfDay(), stop.plusDays( 1 ).withTimeAtStartOfDay() );

DateTime lateNight29th = new DateTime( 2014, 9, 29, 23, 0, 0, timeZone_NewYork );
boolean containsLateNight29th = interval.contains( lateNight29th );

Dump to console…

System.out.println( "start: " + start );
System.out.println( "stop: " + stop );
System.out.println( "daysBetween: " + daysBetween );
System.out.println( "period: " + period ); // Uses format: PnYnMnDTnHnMnS
System.out.println( "interval: " + interval );
System.out.println( "intervalWholeDays: " + intervalWholeDays );
System.out.println( "lateNight29th: " + lateNight29th );
System.out.println( "containsLateNight29th: " + containsLateNight29th );

When run…

start: 2014-09-29T15:16:17.000-04:00
stop: 2014-09-30T01:02:03.000-04:00
daysBetween: 0
period: PT9H45M46S
interval: 2014-09-29T15:16:17.000-04:00/2014-09-30T01:02:03.000-04:00
intervalWholeDays: 2014-09-29T00:00:00.000-04:00/2014-10-01T00:00:00.000-04:00
lateNight29th: 2014-09-29T23:00:00.000-04:00
containsLateNight29th: true
野稚 2024-07-13 15:12:13

我以毫秒为单位存储所有日期。 我根本不使用时间戳/日期时间字段。

所以,我必须尽可能地操纵它。 这意味着我在 sql 查询中不使用“before”、“after”、“now”关键字。

Im storing all dates in milliseconds. I do not use timestamps/datetime fields at all.

So, i have to manipulate it as longs. It means i do not use 'before', 'after', 'now' keywords in my sql queries.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文