如何优化查询以计算行相关的日期时间关系?
How to optimize query to compute row-dependent datetime relationships?
假设我有一个简化模型,其中 patient
可以有零个或多个 events
。一个事件有一个 category
和一个 date
。我想支持以下问题:
Find all patients that were given a medication after an operation and
the operation happened after an admission.
其中用药、手术、入院都是事件类别。大约有 100 个可能的类别。
我预计会有 1000 名患者,每个患者每个类别大约有 10 个事件。
我想到的天真的解决方案是有两个 table,一个 patient
和一个 event
table。在 event.category
上创建索引,然后使用内部联接查询,例如:
SELECT COUNT(DISTINCT(patient.id)) FROM patient
INNER JOIN event AS medication
ON medication.patient_id = patient.id
AND medication.category = 'medication'
INNER JOIN event AS operation
ON operation.patient_id = patient.id
AND operation.category = 'operation'
INNER JOIN event AS admission
ON admission.patient_id = patient.id
AND admission.category = 'admission'
WHERE medication.date > operation.date
AND operation.date > admission.date;
但是,随着添加更多 categories/filters,此解决方案无法很好地扩展。对于 1,000 名患者和 45,000 个事件,我看到以下性能行为:
| number of inner joins | approx. query response |
| --------------------- | ---------------------- |
| 2 | 100ms |
| 3 | 500ms |
| 4 | 2000ms |
| 5 | 8000ms |
解释:
有人对如何优化这个 query/data 模型有什么建议吗?
额外信息:
- Postgres 10.6
- 在 Explain 输出中,
project_result
等同于简化模型中的 patient
。
高级用例:
Find all patients that were given a medication within 30 days after an
operation and the operation happened within 7 days after an admission.
首先,如果通过 FK 约束强制执行参照完整性,您可以完全从查询中删除 patient
table:
SELECT COUNT(DISTINCT patient) -- still not optimal
FROM event a
JOIN event o USING (patient_id)
JOIN event m USING (patient_id)
WHERE a.category = 'admission'
AND o.category = 'operation'
AND m.category = 'medication'
AND m.date > o.date
AND o.date > a.date;
接下来,通过使用 EXISTS
半连接来摆脱行的重复乘法和 DISTINCT
以对抗外部 SELECT
中的乘法:
SELECT COUNT(*)
FROM event a
WHERE EXISTS (
SELECT FROM event o
WHERE o.patient_id = a.patient_id
AND o.category = 'operation'
AND o.date > a.date
AND EXISTS (
SELECT FROM event m
WHERE m.patient_id = a.patient_id
AND m.category = 'medication'
AND m.date > o.date
)
)
AND a.category = 'admission';
请注意,准入中仍可能存在 重复项 ,但这可能是您的数据模型/查询设计中的主要问题,需要在评论中进行说明。
如果您出于某种原因确实想将同一患者的所有病例集中在一起,有多种方法可以在初始步骤中让每个患者最早入院 - 并且对每个额外的步骤重复类似的方法。对于您的情况可能最快(将患者 table 重新引入查询):
SELECT count(*)
FROM patient p
CROSS JOIN LATERAL ( -- get earliest admission
SELECT e.date
FROM event e
WHERE e.patient_id = p.id
AND e.category = 'admission'
ORDER BY e.date
LIMIT 1
) a
CROSS JOIN LATERAL ( -- get earliest operation after that
SELECT e.date
FROM event e
WHERE e.patient_id = p.id
AND e.category = 'operation'
AND e.date > a.date
ORDER BY e.date
LIMIT 1
) o
WHERE EXISTS ( -- the *last* step can still be a plain EXISTS
SELECT FROM event m
WHERE m.patient_id = p.id
AND m.category = 'medication'
AND m.date > o.date
);
参见:
- Select first row in each GROUP BY group?
- Optimize GROUP BY query to retrieve latest record per user
您可以通过缩短冗长(和冗余)的类别名称来优化您的 table 设计。使用查找 table 并仅存储 integer
(甚至 int2
或 "char"
值作为 FK。)
为了获得最佳性能(这很重要)在 (parent_id, category, date DESC)
上有一个 多列索引 并确保所有三列都已定义 NOT NULL
。索引表达式的顺序很重要。 DESC
在这里主要是可选的。 Postgres 可以使用具有默认 ASC
排序顺序的索引,几乎与您的情况一样有效。
如果VACUUM
(最好是autovacuum的形式)可以跟上写操作或者你有一个只读的情况开始,你会很快index-only scans这个。
相关:
- Optimizing queries on a range of timestamps (two columns)
- Select Items that has one item but not the other
要实施您的额外时间范围(您的 "advanced use case"),请基于第二个查询,因为我们必须考虑所有 个事件。
你真的应该有病例 ID 或更明确的东西,以将手术与入院和药物与手术等联系起来。 (可能只是引用事件的 id
!)单独的日期/时间戳很容易出错。
SELECT COUNT(*) -- to count cases
-- COUNT(DISTINCT patient_id) -- to count patients
FROM event a
WHERE EXISTS (
SELECT FROM event o
WHERE o.patient_id = a.patient_id
AND o.category = 'operation'
AND o.date >= a.date -- or ">"
AND o.date < a.date + 7 -- based on data type "date"!
AND EXISTS (
SELECT FROM event m
WHERE m.patient_id = a.patient_id
AND m.category = 'medication'
AND m.date >= o.date -- or ">"
AND m.date < o.date + 30 -- syntax for timestamp is different
)
)
AND a.category = 'admission';
关于date
/timestamp
算术:
- How to get the end of a day?
您可能会发现条件聚合可以满足您的需求。如果你的序列变得复杂,时间部分可能很难处理(见下文),但基本思想:
select e.patient_id
from events e
group by e.patient_id
having (max(date) filter (where e.category = 'medication') >
min(e.date) filter (where e.category = 'operation')
) and
(min(date) filter (where e.category = 'operation') >
min(e.date) filter (where e.category = 'admission'
);
这可以推广到更多类别。
使用 group by
和 having
应该具有您想要的一致的性能特征(尽管对于简单的查询可能会更慢)。这种方法(或任何方法)的诀窍是当给定患者有多个类别时会发生什么。
例如,这个或你的方法会发现:
admission --> operation --> admission --> medication
我怀疑你真的不想找到这些记录。您可能需要一个中间级别,代表给定患者的某种 "episode"。
如果是这种情况,您应该问 另一个 问题,并提供更清晰的数据示例、您可能想问的问题以及匹配和不匹配的案例条件。
假设我有一个简化模型,其中 patient
可以有零个或多个 events
。一个事件有一个 category
和一个 date
。我想支持以下问题:
Find all patients that were given a medication after an operation and
the operation happened after an admission.
其中用药、手术、入院都是事件类别。大约有 100 个可能的类别。
我预计会有 1000 名患者,每个患者每个类别大约有 10 个事件。
我想到的天真的解决方案是有两个 table,一个 patient
和一个 event
table。在 event.category
上创建索引,然后使用内部联接查询,例如:
SELECT COUNT(DISTINCT(patient.id)) FROM patient
INNER JOIN event AS medication
ON medication.patient_id = patient.id
AND medication.category = 'medication'
INNER JOIN event AS operation
ON operation.patient_id = patient.id
AND operation.category = 'operation'
INNER JOIN event AS admission
ON admission.patient_id = patient.id
AND admission.category = 'admission'
WHERE medication.date > operation.date
AND operation.date > admission.date;
但是,随着添加更多 categories/filters,此解决方案无法很好地扩展。对于 1,000 名患者和 45,000 个事件,我看到以下性能行为:
| number of inner joins | approx. query response |
| --------------------- | ---------------------- |
| 2 | 100ms |
| 3 | 500ms |
| 4 | 2000ms |
| 5 | 8000ms |
解释:
有人对如何优化这个 query/data 模型有什么建议吗?
额外信息:
- Postgres 10.6
- 在 Explain 输出中,
project_result
等同于简化模型中的patient
。
高级用例:
Find all patients that were given a medication within 30 days after an
operation and the operation happened within 7 days after an admission.
首先,如果通过 FK 约束强制执行参照完整性,您可以完全从查询中删除 patient
table:
SELECT COUNT(DISTINCT patient) -- still not optimal
FROM event a
JOIN event o USING (patient_id)
JOIN event m USING (patient_id)
WHERE a.category = 'admission'
AND o.category = 'operation'
AND m.category = 'medication'
AND m.date > o.date
AND o.date > a.date;
接下来,通过使用 EXISTS
半连接来摆脱行的重复乘法和 DISTINCT
以对抗外部 SELECT
中的乘法:
SELECT COUNT(*)
FROM event a
WHERE EXISTS (
SELECT FROM event o
WHERE o.patient_id = a.patient_id
AND o.category = 'operation'
AND o.date > a.date
AND EXISTS (
SELECT FROM event m
WHERE m.patient_id = a.patient_id
AND m.category = 'medication'
AND m.date > o.date
)
)
AND a.category = 'admission';
请注意,准入中仍可能存在 重复项 ,但这可能是您的数据模型/查询设计中的主要问题,需要在评论中进行说明。
如果您出于某种原因确实想将同一患者的所有病例集中在一起,有多种方法可以在初始步骤中让每个患者最早入院 - 并且对每个额外的步骤重复类似的方法。对于您的情况可能最快(将患者 table 重新引入查询):
SELECT count(*)
FROM patient p
CROSS JOIN LATERAL ( -- get earliest admission
SELECT e.date
FROM event e
WHERE e.patient_id = p.id
AND e.category = 'admission'
ORDER BY e.date
LIMIT 1
) a
CROSS JOIN LATERAL ( -- get earliest operation after that
SELECT e.date
FROM event e
WHERE e.patient_id = p.id
AND e.category = 'operation'
AND e.date > a.date
ORDER BY e.date
LIMIT 1
) o
WHERE EXISTS ( -- the *last* step can still be a plain EXISTS
SELECT FROM event m
WHERE m.patient_id = p.id
AND m.category = 'medication'
AND m.date > o.date
);
参见:
- Select first row in each GROUP BY group?
- Optimize GROUP BY query to retrieve latest record per user
您可以通过缩短冗长(和冗余)的类别名称来优化您的 table 设计。使用查找 table 并仅存储 integer
(甚至 int2
或 "char"
值作为 FK。)
为了获得最佳性能(这很重要)在 (parent_id, category, date DESC)
上有一个 多列索引 并确保所有三列都已定义 NOT NULL
。索引表达式的顺序很重要。 DESC
在这里主要是可选的。 Postgres 可以使用具有默认 ASC
排序顺序的索引,几乎与您的情况一样有效。
如果VACUUM
(最好是autovacuum的形式)可以跟上写操作或者你有一个只读的情况开始,你会很快index-only scans这个。
相关:
- Optimizing queries on a range of timestamps (two columns)
- Select Items that has one item but not the other
要实施您的额外时间范围(您的 "advanced use case"),请基于第二个查询,因为我们必须考虑所有 个事件。
你真的应该有病例 ID 或更明确的东西,以将手术与入院和药物与手术等联系起来。 (可能只是引用事件的 id
!)单独的日期/时间戳很容易出错。
SELECT COUNT(*) -- to count cases
-- COUNT(DISTINCT patient_id) -- to count patients
FROM event a
WHERE EXISTS (
SELECT FROM event o
WHERE o.patient_id = a.patient_id
AND o.category = 'operation'
AND o.date >= a.date -- or ">"
AND o.date < a.date + 7 -- based on data type "date"!
AND EXISTS (
SELECT FROM event m
WHERE m.patient_id = a.patient_id
AND m.category = 'medication'
AND m.date >= o.date -- or ">"
AND m.date < o.date + 30 -- syntax for timestamp is different
)
)
AND a.category = 'admission';
关于date
/timestamp
算术:
- How to get the end of a day?
您可能会发现条件聚合可以满足您的需求。如果你的序列变得复杂,时间部分可能很难处理(见下文),但基本思想:
select e.patient_id
from events e
group by e.patient_id
having (max(date) filter (where e.category = 'medication') >
min(e.date) filter (where e.category = 'operation')
) and
(min(date) filter (where e.category = 'operation') >
min(e.date) filter (where e.category = 'admission'
);
这可以推广到更多类别。
使用 group by
和 having
应该具有您想要的一致的性能特征(尽管对于简单的查询可能会更慢)。这种方法(或任何方法)的诀窍是当给定患者有多个类别时会发生什么。
例如,这个或你的方法会发现:
admission --> operation --> admission --> medication
我怀疑你真的不想找到这些记录。您可能需要一个中间级别,代表给定患者的某种 "episode"。
如果是这种情况,您应该问 另一个 问题,并提供更清晰的数据示例、您可能想问的问题以及匹配和不匹配的案例条件。