【问题标题】:Storing and retrieving historical data using SQL / relational database使用 SQL / 关系数据库存储和检索历史数据
【发布时间】:2011-06-22 17:03:17
【问题描述】:

鉴于此表:

CREATE TABLE DeptPeopleHistory (
  DEPT_ID INTEGER,
  PERSON_ID INTEGER,
  START_DATE INTEGER,
  END_DATE INTEGER,
  UNIQUE(DEPT_ID, START_DATE, PERSON_ID), -- works as sorted index.
  UNIQUE(PERSON_ID, START_DATE),
  UNIQUE(PERSON_ID, END_DATE),
  CONSTRAINT (START_DATE < END_DATE)
);

我有两个需求。首先是让所有在给定日期在给定部门工作的人。目前我使用这个(语义正确)查询:

SELECT PERSON_ID FROM DeptPeopleHistory
WHERE
  DEPT_IT = :given_dept AND
  START_DATE <= :given_date AND :given_date < END_DATE

这对于小的历史表或查询最近的数据来说很快,但对于大的历史表和旧数据来说速度很慢,因为优化器只使用第一个索引并且没有很好的方法来处理 END_DATE。我尝试将 END_DATE 添加到第一个索引,但查询性能是相同的。我猜这是因为子过滤器 (DEPT_IT=:given_dept AND START_DATE

我的另一个需要是强制执行以下约束:一个人不能同时在两个部门工作,也不能在同一个部门工作两次。这意味着:

-- This must work for previously empty data:
INSERT INTO DeptPeopleHistory(DEPT_ID, PERSON_ID, START_DATE, END_DATE)
                      VALUES (1,       1,         20100501,   20100520);

-- This should cause constraint violation because the person already
-- works at dept 1 on days from 20100517 to 20100519:
INSERT INTO DeptPeopleHistory(DEPT_ID,   PERSON_ID, START_DATE, END_DATE)
                      VALUES (:any_dept, 1,         20100517,   20100523);

另一种指定此约束的方法是,对于给定的 PERSON_ID,START_DATE 必须是最小值或等于另一个记录的 END_DATE。

看看这两个需求,我们实际上需要一种有效的方法来处理不相交的范围。您是否知道通用 SQL 或某些特定数据库中的某些功能或构造无法满足这些需求?也许是一些“空间数据库”功能?

示例在 MySQL 中,但我需要适用于 Oracle、SQL Server 和 FireBird 的解决方案。这些解决方案不需要在所有此类数据库中都可移植。

【问题讨论】:

  • 首先,mySQL 缺乏时态数据库所需的约束,例如您的表缺少排序的主键,即允许同一个人 + 部门的重叠日期;更新表格时,即使您的 START_DATE &lt; END_DATE 也不会被检查。其次,(假设您正在寻找开源的东西),无论如何,postgreSQL 都有更好的时间支持:)

标签: mysql sql indexing constraints spatial


【解决方案1】:

作为起点,我推荐 Rick Snodgrass 所著的《用 SQL 开发面向时间的数据库应用程序》一书,地址为 a free PDF download。看起来您可以直接跳到第 5 章并通读第 6 章和第 7 章(但不要忽略后面章节中的替代方法)。

在实现方面,postgreSQL 目前总体上具有良好的时间支持和对可延迟约束的支持(这在 SQL 中至关重要!对于序列键等概念)。

请注意,时间数据库还有其他模型,例如Date Darwen Lorentzos.

【讨论】:

【解决方案2】:

您是否尝试过在 DEPT_ID 和 END_DATE 添加另一个索引?如果您使用的是 MySQL 5+,它可能能够进行索引合并并同时使用该索引和 DEPT_ID、START_DATE、PERSON_ID 索引。

至于您的第二个问题,我认为强制执行此类约束的唯一方法是通过应用程序逻辑或插入/更新触发器。

【讨论】:

    【解决方案3】:

    能不能把表DeptPeopleHistory的结构改成?:

    CREATE TABLE DeptPeopleHistoryDetail (
      DEPT_ID INTEGER,
      PERSON_ID INTEGER,
      WORK_DATE INTEGER,               --- why is that INT and not DATE by the way?
      UNIQUE(WORK_DATE, PERSON_ID)
    );
    

    优点:

    • 您不需要强制执行之前的任何 UNIQUE 约束,也不需要强制执行 START_DATE &lt; END_DATE 约束。
    • 第二个复杂约束也神奇地解决了。

    缺点:

    • 上一个示例中的(1, 1, 20100501, 20100520) 现在拆分为 20 行。我会说,这不是一个真正的问题。关系数据库旨在处理多行。
    • 要为部门中的人员查找START_DATEEND_DATE,必须运行查询。 (如果这太慢了,我怀疑,可以使用额外的表)

    哦,你的慢查询会写成:

    SELECT PERSON_ID FROM DeptPeopleHistoryDetail
    WHERE
      DEPT_IT = :given_dept AND
      WORK_DATE = :given_date 
    

    以您当前的DeptPeopleHistory 设计,您可以尝试以下查询的性能吗?

    SELECT H.PERSON_ID
    FROM DeptPeopleHistory H
      JOIN
        ( SELECT PERSON_ID
               , MAX(START_DATE) AS LATEST_START_DATE
          FROM DeptPeopleHistory
          WHERE
            DEPT_IT = :given_dept AND
            START_DATE <= :given_date
          GROUP BY
            PERSON_ID
        ) AS grp
        ON  H.DEPT_IT = :given_dept
        AND grp.PERSON_ID = H.PERSON_ID
        AND grp.LATEST_START_DATE = H.START_DATE
    WHERE 
       :given_date < H.END_DATE
    

    【讨论】:

    • 可能会在短期内工作,但我不认为它是可扩展的。考虑 5 年的 1000 名员工 - 180 万行 - 对于单个表来说太多了。此外,我们可能会将精度从天更改为分钟,这会将行数乘以 1440。顺便说一句,我使用 INT 而不是 DATE 只是为了获得可移植的文字并使示例独立于数据类型。
    • @fernacolo:180 万行 x 12 字节 = 21MB。不是很大。如果你有 30 年和 10 万名员工,是的,它会变得很大。
    • 如果您将精度提高到分钟,那也是不可能的。
    • @ypercube 如果在 :given_date 之前离开部门的人很少,则加入 GROUP BY 的查询会立即返回!非常有趣的逻辑!
    • @fernacolo:原始查询的问题是它必须检查两个范围条件,并且不能同时在START_DATEEND_DATE 上使用索引。此重构尝试在组子查询中使用PERSON_ID,START_DATE 的索引,然后在外部查询中使用END_DATE 的索引。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-01
    • 2021-09-19
    • 2023-04-06
    • 1970-01-01
    • 2011-04-21
    相关资源
    最近更新 更多