从具有"生效日期"的表格构建日常视图

时间:2015-08-11 14:32:50

标签: mysql sql query-optimization

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

我的架构和示例数据:

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);

我已经创建了this Phing ticket来模拟问题。

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

2 个答案:

答案 0 :(得分:1)

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

首先,您需要将句点的结尾带到每条记录,这会将您的行从事件转换为句点:

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在子查询中找到下一个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);

答案 1 :(得分:1)

您需要非常小心这种类型的视图。编写一个擅长给出每个记录有效的所有单独日期的视图会很容易,但在询问哪个记录在某个特定日期有效时会很慢。

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

以下是合理的,在约会时返回有效的行。

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是一个真正的持久性物化表,而不是您使用的内联视图,使用它将极大地影响性能。

它还假定VALUE_DATAstart_date编入索引。


<强> 编辑:

我还发现你的价值表可能还有其他列。我们假设它是每人的值。也许是他们在任何特定日期的地址。

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

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块中。当您只从目标表中提取一个值时,这是有效的,但如果需要从目标表中提取多个值,则效果较差...

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'