您如何将“模糊日期”存储到数据库中?


125

这是我遇到过几次的问题。假设您有一条要存储到数据库表中的记录。该表具有一个名为“ date_created”的DateTime列。这项特别的记录创建于很久以前,您不确定确切的日期,但知道年份和月份。您只知道年份的其他记录。您知道日,月和年的其他记录。

您不能使用DateTime字段,因为“ 1978年5月”不是有效的日期。如果将其分成多列,则会失去查询能力。有没有其他人遇到过这个问题,如果是这样,您是如何处理的?

为了阐明我正在构建的系统,它是一个跟踪档案的系统。一些内容是很久以前制作的,我们所知道的只是“ 1978年5月”。我可以将其存储为1978年5月1日,但是只能以某种方式表示该日期仅对月份准确。这样,几年后,当我检索该存档时,当日期不匹配时,我不会感到困惑。

对我而言,将“ 1978年5月的未知日期”与“ 1978年5月1日”区分开是很重要的。另外,我也不想将未知数存储为0,例如“ 1978年5月0日”,因为大多数数据库系统都会将其作为无效的日期值来拒绝。


14
将“ 1978年5月的未知日期”与“ 1978年5月1日”区分开来是否很重要?

5
@MichaelT:是的,区别很重要。
nbv4 2013年


6
@aslum:大多数数据库系统都将其视为无效的日期值
nbv4 2013年

9
@JimmyHoffa-您从未遇到过模糊的日期场景,或者您需要比较日期的场景吗?在这两种情况下,常见的都是病史:您记得去年4月1日进行了阑尾切除术,但是1975年的某个时候进行了扁桃体切除术,而在某年的5月和6月发生了其他事情。如果您想知道某些医学事件是在其他医学突破之前还是之后发生的,该怎么办?这是在他们检查艾滋病毒的血液供应之前或之后发生的吗?
thursdaysgeek

Answers:


148

将所有日期存储在数据库的正常DATE字段中,并具有其他准确性字段,该字段实际的准确性是多少。

date_created DATE,
date_created_accuracy INTEGER, 

date_created_accuracy:1 =确切日期,2 =月,3 =年。

如果您的日期不明确(例如1980年5月),请在期间开始时(例如1980年5月1日)进行存储。或者,如果您的日期准确到年份(例如1980年),则将其存储为1月1日。1980年具有相应的准确度值。

这种方式可以轻松地以某种自然的方式进行查询,并且仍然可以知道日期的准确性。例如,这使您可以查询Jan 1st 1980和之间的日期Feb 28th 1981,并获得模糊的日期1980May 1980


1
您仍然必须根据我所看到的在此处计算日期结束时间,因此我认为在查询之间很难做到,因为您最多只能选择一个计算字段。
Wyatt Barnett

8
好的答案,真的很聪明。select * from mytable where date_created between "1980/1/1" and "1981/2/28" and date_created_accuracy <= 2;。天才。
Naftuli Kay 2013年

58
我鼓励您将日期准确性视为“天”。如果确切的日期是0,则可以使用更灵活的日期“夏天的某个时候”,该日期的准确度是6月1日的90天,而不是硬编码的特定日期范围。它还可以处理多年的准确性。

1
您可能应该将其作为答案提交,MichaelT
2013年

1
+1:此解决方案的另一个好处是,您可以根据date_created_accuracy字段的值添加显示逻辑。您可以在结果或用户界面中显示“ 1980年5月”,也可以仅显示“ 1980”,前提是该字段所显示的准确度很高。
Kyralessa 2015年

27

如果您不需要将此类数据用作常规日期时间信息,则可以使用任何简单的字符串格式。

但是,如果您需要保留所有功能,那么我可以想到两种解决方法,两种方法都需要存储在数据库中的其他信息:

  1. 创建min datemax date字段,它们对于“不完整”数据具有不同的值,但对于准确的日期而言将是一致的。
  2. 为每种不正确的日期创建类型(无_ 0,date_missing _1,month_missing _2,year_missing_4等等,因此可以将它们组合在一起)。type在记录中添加一个字段,并保留缺少的信息。

最小和最大日期字段也是我的第一个想法。
Michael Itzoe 2013年

1
很久以前,我们不得不解决完全相同的问题。用户可以讲述过去任何时间发生的事件的故事,因此我们必须支持模糊日期。经过反复的研究,我们得出的解决方案与此处的superM建议最相似,其中日期存储为包含故事日期的最小和最大可能瞬间。报告日期时,可以从最小日期和最大日期之间的差异中提取准确性(即“此记录对月/年/日是准确的”)。无需存储第三字段以确保准确性。
Meetamit 2013年

4
min datemax date字段+1 。我认为这是最灵活,最精确且易于使用的解决方案。
2013年

1
起初我反对这个想法。但是我意识到这是最灵活的方法。
阿努拉格·卡利亚

这是自然的。您所描述的并不是模糊的日期,而是一个时间范围.....它有一个开始和一个结束。
Pieter B

20

实际上,这更多是需求定义,而不是技术问题,您需要关注的是“我们如何定义过去的日期”,技术解决方案将会出现。

我不得不处理类似以下情况的时间通常是:

  • 定义如何映射事物- 就像MichaelT建议的那样,确定将定义为“月/日”的任何内容定义为在所述月份的1号午夜。对于大多数用途而言,这通常已经足够好了-如果确切的日期是如此重要,那么您可能会在35年后记录下来,对吧?
  • 弄清楚是否需要跟踪-IE,创建日期稍有改动的记录是否需要标记说明?还是仅仅是用户培训问题,以便人们知道并可以采取相应的行动。

有时,您需要做一些事情,例如使日期变得模糊-例如,可能一个日期可能需要对1978年5月的任何查询做出响应。这是可行的-只需将您的create_date 2字段设为旧记录即可,将30天的传播时间视情况而定,新的天数将获得2个相同的值。


1
+1-我正在用双重约会方法来制定答案。您的答案首先到达这里。

2
+1,这很丑陋,并且为不需要它的新条目创建了很多无用的额外信息,但另一方面,它的确使查询变得比以前简单得多。一段时间以来,我们一直在使用类似的解决方案来解决相关问题。
Izkata

3
@Izkata-公平的观点,但是当您需要制作一个应该在一个月的时间范围内的单一观点时,您可以得到多大的优雅。当然,它比在某个地方即时计算查询的开始和结束要漂亮。
Wyatt Barnett

1
+1,因为它能够表示任意粒度而不会出现枚举值的爆炸式增长。
Dan Neely

18

指示日期是否正确的最简单方法是创建一个默认值为NULL的精度字段INT(1)

如果日期是正确的,则将日期时间存储在“ date_created”中,并保留精度为NULL

如果日期仅对月份准确,则将日期时间存储为月份的第一天,其准确度值为1

如果日期仅在1月1日的年份存储日期时间中准确,则准确度值为2

您可以使用不同的数字来保存不同的值,例如第一季度等


当您这样做时,查询就会变得非常繁琐。
Blrfl 2013年

3
这对于不属于干净月份边界的数据(例如“ Q2 1991”和“ Winter 1978-1979”)有困难。

1
OP需要某种方式来表示该日期仅对月份准确。
大卫·斯特拉坎

7
您在此处滥用NULL的含义。NULL表示“未知”,因此,如果日期正确,则精度不能为NULL。它可以是“ 1”。
Konerak 2013年

@Konerak语义上是。但是,由于大多数日期都是准确的,因此仅需要识别特殊情况,并在此处使用NULL作为默认值。
大卫·斯特拉坎

17

过去,我已经将准确的日期存储为开始日期和结束日期。2012年5月21日这一天将表示为开始= 2012年5月21日上午12点,结束= 2012年5月22日上午12点。2012年将表示为开始= 2012年1月1日上午12点,结束= 2013年1月1日12上午。

我不确定是否会推荐这种方法。当向用户显示信息时,您需要正确地检测到日期范围恰好覆盖了一天,以便显示“ may 25”而不是两个过度指定的端点(这意味着要处理夏令时等等)。

但是,当您不打算翻译成人类时,使用端点进行编程要比使用中心+准确性容易得多。您最终不会遇到很多情况。很好


实际上,如果范围始终存储为UTC,那么确定如何显示范围并不需要那么棘手。作为UTC时间戳,每天,每周,每月,每年-甚至季节和季度-都将有两个恒定的,全局的,不同的且易于确定的数字,分别代表该时期的开始和结束。逻辑简单地变成了一些if语句,以查看两个日期是否在某种类型的周期的开始和结束。不需要复杂的数学或时区内容:)
2013年

@Supr确定特定秒是否在特定人类时期的边界上,这本身就是一个难题。尤其是从长远来看,随着地球自转速度减慢,人类对当地时间的定义也将发生不小的变化。
Craig Gidney 2013年

14

为什么不存储两个日期。

Created_After和Created_Before。实际的语义是“在...上或之后创建的”和“在...上或之前创建的”

因此,如果您知道确切的日期,那么Created_After和Created_Before将是同一日期。

如果您知道这是2000年5月的第一周,则Created_After ='2000-05-01'和Created_Before ='2000-05-07'。

如果您只知道1999年5月,则值将为'1999-05-01'和'1999-05-30'。

如果它是“ 42的夏天”,则值将是“ 1942-06-01”和“ 1942-08-31”。

使用普通的SQL可以很容易地查询该模式,对于非技术用户来说也很容易遵循。

例如,查找可能在2001年5月创建的所有文档:

SELECT * FROM DOCTAB WHERE Created_After < '2001-05-31' And Created_Before > 2001-05-01;

相反,要查找肯定在2001年5月创建的所有文档:

SELECT * FROM DOCTAB WHERE Created_After > '2001-05-01' And Created_Before < 2001-05-31;

1
我认为这是最优雅的解决方案。
Pieter B

这与superM和Strilanc的答案相同。+1不过是为了更清楚地说明并显示查询的简单程度。
2013年

9

ISO 8601日期时间格式带有持续时间定义,例如

2012-01-01P1M (阅读:2012年1月1日,期限:1个月)应为“ 2012年1月”。

我会用它来存储数据。为此,您可能需要一个String类型的数据库字段。如何对此进行明智的搜索是另一回事。


+1表示创意,但-1表示不使用日期字段(出于搜索和/或查找原因)
user151019 2013年

取决于数据库。但是,这可能是扩展的基础,但问题是:如果您搜索的是此结果集中的文档,那么在这种情况下,是否所有文档都比1月12日更新?这不是小事。在这里,问题是如何存储模糊日期。
Matthias Ronge 2013年

3

通常,我仍然将它们存储为日期,以使一般查询的业务仍然可能,即使准确性稍差一些。

如果重要的是要知道我过去的精度,可以将精度“窗口”存储为+/-十进制或查找(日,月,年等)。在其他情况下(而不是窗口中),我只是将原始日期值存储为字符串,然后将我可以转换为日期时间的格式转换为给定示例,可能是1978-05-01 00:00:00和“ May 1978”。


3

如果将其分成多列,则会失去查询能力。

谁说的?这是您的工作:

  1. 具有3列,分别是Day,Month,Year,int类型,以及第四列TheDate of DateTime类型。
  2. 如果TheDate保留为空,但有一个触发器使用3列Day,Month,Year来构建TheDate,但是Day,Month,Year字段中的一个或多个具有值。
  3. 当提供TheDate时,有一个触发器可填充Day,Month,Year字段,但没有提供。

因此,如果我进行如下插入: insert into thistable (Day, Month, Year) values (-1, 2, 2012);那么TheDate将成为2/1/2013,但由于Day(日期)字段中的-1,所以我知道它确实是2/2012中一个不确定的日期。

如果我insert into thistable (TheDate) values ('2/5/2012');将Day设置为5,将把Month设置为2,将Year设置为2012,并且因为它们都不是-1,所以我知道这是确切的日期。

我不会失去查询能力,因为插入/更新触发器可确保我的3个字段(日,月,年)始终在TheDate中生成可以查询的DateTime值。


3

另一种选择是将日期存储为形式的整数YYYYMMDD

  • 您只知道1951年:另存为 19510000
  • 您知道月份和年份是1951年3月:存储为 19510300
  • 您知道完整日期是1951年3月14日:存储为 19510314
  • 一个完全未知的日期:另存为 0

好处

您可以将模糊日期存储在一个字段中,而不是两个日期字段或其他许多答案建议的日期和准确性中。

查询仍然很容易:

  • 1951年的所有记录- SELECT * FROM table WHERE thedate>=19510000 and thedate<19520000
  • 1951年3月的所有记录- SELECT * FROM table where thedate>=19510300 and thedate<19510400
  • 1951年3月14日的所有记录- SELECT * FROM table where thedate=19510314

笔记

  • 您的GUI需要一个GetDateString(int fuzzyDate)非常容易实现的。
  • 使用int格式可以轻松进行排序。您应该知道未知的日期将首先出现。您可以通过使用99“填充”(而不是00月份或日期)来扭转这种情况。

您如何表示“ 1941-1942年冬季”的模糊日期?可能是1941年12月或1942

1
您的问题与一般解决方案有关。原始问题并未将此列为问题。根据发布的问题,有时会知道完整的日期,有时只知道年份和月份,有时甚至是年份。没有提到模糊日期范围的问题。我同意,如果您需要解决此问题,则需要两个日期(尽管将范围存储为两个“模糊日期整数”可能比存储两个“硬”日期更具灵活性)。
里克2014年

1

ISO 8601还指定了“模糊日期”的语法。2012年2月12日下午3点将是“ 2012-02-12T15”,而2012年2月可能只是“ 2012-02”。使用标准字典分类法可以很好地扩展此范围:

$ (echo "2013-03"; echo "2013-03"; echo "2012-02-12T15"; echo "2012-02"; echo "2011") | sort
2011
2012
2012-02
2012-02-12T15
2013-03

0

这是我的看法:

从模糊日期转到日期时间对象(将适合数据库)

import datetime
import iso8601

def fuzzy_to_datetime(fuzzy):
    flen = len(fuzzy)
    if flen == 4 and fuzzy.isdigit():
        dt = datetime.datetime(year=int(fuzzy), month=1, day=1, microsecond=111111)

    elif flen == 7:
        y, m = fuzzy.split('-')
        dt = datetime.datetime(year=int(y), month=int(m), day=1, microsecond=222222)

    elif flen == 10:
        y, m, d = fuzzy.split('-')
        dt = datetime.datetime(year=int(y), month=int(m), day=int(d), microsecond=333333)

    elif flen >= 19:
        dt = iso8601.parse_date(fuzzy)

    else:
        raise ValueError("Unable to parse fuzzy date: %s" % fuzzy)

    return dt

然后是一个获取日期时间对象并将其移回模糊日期的函数。

def datetime_to_fuzzy(dt):
    ms = str(dt.microsecond)
    flag1 = ms == '111111'
    flag2 = ms == '222222'
    flag3 = ms == '333333'

    is_first = dt.day == 1
    is_jan1 = dt.month == 1 and is_first

    if flag1 and is_jan1:
        return str(dt.year)

    if flag2 and is_first:
        return dt.strftime("%Y-%m")

    if flag3:
        return dt.strftime("%Y-%m-%d")

    return dt.isoformat()

然后进行单元测试。我有没有想念任何案件?

if __name__ == '__main__':
    assert fuzzy_to_datetime('2001').isoformat() == '2001-01-01T00:00:00.111111'
    assert fuzzy_to_datetime('1981-05').isoformat() == '1981-05-01T00:00:00.222222'
    assert fuzzy_to_datetime('2012-02-04').isoformat() == '2012-02-04T00:00:00.333333'
    assert fuzzy_to_datetime('2010-11-11T03:12:03Z').isoformat() == '2010-11-11T03:12:03+00:00'

    exact = datetime.datetime(year=2001, month=1, day=1, microsecond=231)
    assert datetime_to_fuzzy(exact) == exact.isoformat()

    assert datetime_to_fuzzy(datetime.datetime(year=2001, month=1, day=1, microsecond=111111)) == '2001'
    assert datetime_to_fuzzy(datetime.datetime(year=2001, month=3, day=1, microsecond=222222)) == '2001-03'
    assert datetime_to_fuzzy(datetime.datetime(year=2001, month=6, day=6, microsecond=333333)) == '2001-06-06'

    assert datetime_to_fuzzy(fuzzy_to_datetime('2002')) == '2002'
    assert datetime_to_fuzzy(fuzzy_to_datetime('2002-05')) == '2002-05'
    assert datetime_to_fuzzy(fuzzy_to_datetime('2002-02-13')) == '2002-02-13'
    assert datetime_to_fuzzy(fuzzy_to_datetime('2010-11-11T03:12:03.293856+00:00')) == '2010-11-11T03:12:03.293856+00:00'

在一个极端的情况下,一个事件恰好发生在2001-01-01T00:00:00.333333系统上,但是系统将其解释为仅是“ 2001”,但这似乎非常不可能。


0

我在一家出版公司工作,该公司处理很多旧书,而我们常常无法获得确切的日期。对于给定的日期条目,我们通常有两个字段:date和一个大约布尔值:

date date
dateCirca enum('Y', 'N')

我们使用日期字段来指示某个事件的日期,或者在我们不知道真实日期的情况下使用“足够接近”的日期。如果我们不知道真实的日期,则将该dateCirca字段标记为,Y并给出足够接近的日期,该日期标记为“ 1st”,例如

1st March, 2013  // We don't know the day of the month
1st January, 2013  // We don't know the month/day of the year
1st January, 2000  // We don't know the month/day/year, we only know the century

0

总览

有许多可能的表示形式,也就是数据库模式,用于存储模糊日期时间(甚至只是模糊日期):

  1. 日期时间和指示其精度或准确性的代码
  2. 日期时间和间隔,其中有几种表示间隔的可能性:
    1. 将所有间隔表示为某个固定单位的整数(或其他数字)数量,例如天,分钟,纳秒。
    2. 将间隔表示为整数(或其他数字)数量和表示其单位的代码。
  3. 开始和结束日期时间
  4. 概率分布:
    1. 指定特定族中特定分布的参数的小数或浮点数量,例如正态分布的均值和标准差。
    2. 概率分布函数,例如作为(查找)代码(可能带有特定值的参数),或作为具有足够表达力的语言,格式或表示形式的表达式。

[1],[2]和[3]都是(隐含)一致的间隔,即一组(相等)可能的时间点。

[4]是最富表现力的,即允许任何可能(或至少任意长)的书面语言句子或短语时。但这也是最难处理的。在极限情况下,将需要人类级别的AI处理任意值。实际上,可能值的范围将需要严格限制,并且对于许多操作(例如排序,搜索),可能更希望使用替代的“结构化”值。

[5]可能是(某种程度上)最通用的紧凑表示形式。

均匀间隔

均匀间隔是表示一组(可能的)日期时间值的最简单的紧凑方法。

对于[1],日期时间值的部分被忽略,即,对应于比所指示的精度或准确性更精细的单位的部分;否则,它等于[2],并且精度/精度代码等于具有相同单位的间隔(隐含的数量为1)。

[2]和[3]在表达上是等效的。[1]的表达力严格不及任何一种,因为存在无法用[1]表示的有效间隔。等于跨越日期边界的12小时间隔的模糊日期时间。

用户[1]比其他任何表示形式都更容易输入,并且通常应该(至少稍微)减少键入次数。如果可以使用各种文本表示形式输入日期时间,例如“ 2013”​​,“ 2014-3”,“ 2015-5-2”,“ 7/30/2016 11p”,“ 2016-07-31 18:15” ,也可以从输入中自动推断出精度或准确性。

[1]的准确度或精确度也最容易转换为要传达给用户的表格,例如“ 2015-5月准确度”到“ 2015年5月”,而“ 2015年5月13日2p,上下13.5天” (请注意,后者无论如何都不能用[1]表示)。

弦乐

实际上,字符串值将需要转换为其他表示形式以进行查询,排序或以其他方式比较多个值。因此,尽管任何书面自然(人类)语言都比[1],[2],[3]或[5]更具表现力,但是我们还没有办法处理超出标准文本表示形式或格式的内容。鉴于这种情况,这可能是最有用的表现本身

这种表示的一个优点是,在实践中,值应按原样呈现给用户,并且不需要转换就易于理解。

概率分布

概率分布概括了统一间隔表示[1],[2],[3],并且(可以说)等效于(一般)字符串表示[4]。

概率分布相对于字符串的优势之一是前者是明确的。

[5-1]适用于(大部分)符合现有分布的值,例如,从已知(或认为)测量符合特定分布的设备的日期时间值。

[5-2]可能是紧凑地表示任意“模糊日期时间”值的最佳(某种程度上)实用的方法。当然,所使用的特定概率分布的可计算性很重要,并且在查询,排序或比较不同的值时肯定要解决一些有趣的(也许是不可能的)问题,但是其中很多可能已经为人所知或已解决。数学和统计文献,因此,这绝对是一种极为笼统和明确的表述。



-2

在您的情况下,您只需要年,月和日。年份和月份是必填项,日期是可选的。我会用这样的东西:

year smallint not null,
month smallint not null,
day smallint

另外,您仍然可以非常有效地使用索引。该(微小=减,queires得到一点更“复杂”(长)。


1
但这意味着,如果模糊性也吞噬了月度部分,则此方法将失败。
Anurag Kalia

1
@AnuragKalia-因此使month字段为空。没有任何理由以后无法重新配置。
JeffO 2013年

那只是一个例子。解决方案必须足够通用以适应将来的问题。如果您指定的范围是2013年3月15日至2013年3月22日,则此方法无效。上面的最小-最大答案是最通用的答案。
阿努拉格·卡利亚(

1
您是否在OP帖子中找到了这样的要求,或者仅仅是您的幻想?
Danubian Sailor

将月份设置为可空值使您可以指定日期,但不能指定月份。也没有道理。什么时候1978-??-31
MSalters

-2

我将简单地存储正常日期的确切时间,并使模糊日期的时间部分通用,例如00:00:00。然后,我将所有模糊日期设为该月的1号。

查询时,您

  1. 检查时间也等于00:00:00的日期范围(模糊)
  2. 检查日期范围,其中时间不等于00:00:00(真实)
  3. 检查日期范围,但忽略时间部分(合计)

有比这更好的解决方案,但是我个人讨厌元数据(关于我的数据的数据)。它只是有一段时间后会失控的习惯。


2
这将如何处理具有时间00:00:00的真实日期?
gnat 2013年

从理论上讲,可以在该时间添加一个实际日期,但不会发生。我见过具有数百万行的表,但没有一个表的datetime值的时间为00:00:00。实用主义胜于常规。
肯帕奇船长
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.