从 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_DATA
由 start_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'
我有一个使用 "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_DATA
由 start_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'