从 table 和 "effective date" 构建每日视图

Building a daily view from a table with an "effective date"

我有一个使用 "start dates" 或有效日期的 table。 table 中的值从开始日期开始生效,直到它被同一 table 中具有较晚开始日期的另一个条目覆盖。

我的架构和示例数据:

CREATE TABLE VALUE_DATA (
    `start_date` DATE,
    `value` FLOAT
);

INSERT INTO VALUE_DATA (start_date, value) VALUES
('2015-01-01', 10),
('2015-01-03', 20),
('2015-01-08', 30),
('2015-01-09', 15);

生成所需结果的查询:

SELECT date, value
FROM(
    SELECT date, MAX(start_date) as max_start
    FROM (
        select curdate() - INTERVAL (ones.digit + (10 * tens.digit) + (100 * hundreds.digit)) DAY as date
        from (select 0 as digit union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as ones
        cross join (select 0 as digit union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as tens
        cross join (select 0 as digit union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as hundreds
    ) DATE_TABLE
    LEFT JOIN VALUE_DATA ON (DATE_TABLE.date >= VALUE_DATA.start_date)
    WHERE DATE_TABLE.date between '2015-01-01' and '2015-01-10'
    GROUP BY date
) START_DATES LEFT JOIN VALUE_DATA ON (START_DATES.max_start = VALUE_DATA.start_date);

我创建了一个 SQL Fiddle 来模拟这个问题。

虽然 SQL Fiddle 有效(给出正确的结果),但我不认为这是最好的方法。我不得不使用的查询有点复杂。我最终想为此 table 创建一个视图,其中包含每天的正确值,无论它是否在开始日期(如 Fiddle 产生的输出)加入这个 table 更容易。显然,我想确保这个视图尽可能快。所以我的问题是,如何改进(优化)此查询以用于此类视图?

我会分两步来解决这个问题。

首先,您需要为每条记录添加句点结束时间,这会将您的行从事件变为句点:

SELECT 
  v1.start_date, 
  v2.start_date as next_start_date, 
  v1.value 
FROM 
  VALUE_DATA v1 LEFT JOIN 
  VALUE_DATA v2 ON 
    v1.start_date < v2.start_date AND 
    NOT EXISTS 
     (SELECT * FROM VALUE_DATA 
      WHERE start_date > v1.start_date and start_date < v2.start_date)

现在您需要将 date dimension 添加到您的架构中。

一旦你有了日期维度,就很容易将它加入到以前的查询中:

SELECT 
  d.date, v1.value 
FROM 
  VALUE_DATA v1 LEFT JOIN 
  VALUE_DATA v2 ON 
    v1.start_date < v2.start_date AND 
    NOT EXISTS 
     (SELECT * FROM VALUE_DATA 
      WHERE start_date > v1.start_date and start_date < v2.start_date)
  INNER JOIN DATE_DIMENSION d ON 
      d.date >= v1.start_date AND d.date < COALESCE(v2.start_date, CURDATE()) 

以下查询在 MySQL 中可能会更快,它有点派生自前两个查询,只是不使用 JOIN,而是在 a 中找到下一个 start_date子查询:

SELECT 
  d.date, v1.value 
FROM 
  VALUE_DATA v1 
  INNER JOIN DATE_DIMENSION d ON 
      d.date >= v1.start_date AND 
      d.date < (SELECT COALESCE(MIN(v.start_date), CURDATE()) 
                FROM VALUE_DATA v 
                WHERE v.start_date > v1.start_date);

你需要非常小心这种观点。编写一个擅长给出每条记录有效的所有单独日期的视图很容易,但在询问哪个记录在一个特定日期有效时速度很慢。

(因为回答第二个问题需要先回答第一个问题的每个日期,然后丢弃失败的。)

以下是合理的在获取日期并返回在该日期有效的行。

CREATE VIEW DAILY_VALUE_DATA AS (
    SELECT
        DATE_TABLE.date,
        VALUE_TABLE.value
    FROM
        DATE_TABLE
    LEFT JOIN
        VALUE_DATA
            ON  VALUE_DATA.start_date = (SELECT MAX(lookup.start_date)
                                           FROM VALUE_DATA lookup
                                          WHERE lookup.start_date <= DATE_TABLE.date
                                        )
);

SELECT * FROM DAILY_VALUE_DATA WHERE date = '2015-08-11'

注意:这假设 DateTable 是一个真正的持久物化 table,而不是您使用的内联视图,使用它会大大降低性能。

它还假定 VALUE_DATAstart_date 索引。


编辑:

我还发现您的值 table 可能会有其他列。假设这是一个值 per person。也许他们在任何给定日期的地址。

要扩展上面的查询,您还需要加入 person table...

CREATE VIEW DAILY_VALUE_DATA AS (
    SELECT
        PERSON.id   AS person_id,
        DATE_TABLE.date,
        VALUE_TABLE.value
    FROM
        PERSON
    INNER JOIN
        DATE_TABLE
            ON  DATE_TABLE.date >=          PERSON.date_of_birth
            AND DATE_TABLE.date <  COALESCE(PERSON.date_of_death, CURDATE() + 1)
    LEFT JOIN
        VALUE_DATA
            ON  VALUE_DATA.start_date = (SELECT MAX(lookup.start_date)
                                           FROM VALUE_DATA lookup
                                          WHERE lookup.start_date <= DATE_TABLE.date
                                            AND lookup.person_id   = PERSON.id
                                        )
);

SELECT * FROM DAILY_VALUE_DATA WHERE person_id = 1 AND date = '2015-08-11'


编辑:

LEFT JOIN 的另一种替代方法是将相关的子查询嵌入到 SELECT 块中。当您只有一个值可以从目标 table 中提取时,这是有效的,但如果您需要从目标 table...

中提取多个值,则效果会降低
CREATE VIEW DAILY_VALUE_DATA AS (
    SELECT
        PERSON.id   AS person_id,
        DATE_TABLE.date,
        (SELECT VALUE_DATA.value
           FROM VALUE_DATA
          WHERE VALUE_DATA.start_date <= DATE_TABLE.date
            AND VALUE_DATA.person_id   = PERSON.id
       ORDER BY VALUE_DATA.start_date DESC
          LIMIT 1
        )   AS value
    FROM
        PERSON
    INNER JOIN
        DATE_TABLE
            ON  DATE_TABLE.date >=          PERSON.date_of_birth
            AND DATE_TABLE.date <  COALESCE(PERSON.date_of_death, CURDATE() + 1)
);

SELECT * FROM DAILY_VALUE_DATA WHERE person_id = 1 AND date = '2015-08-11'