如何优化查询以计算行相关的日期时间关系?

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 模型有什么建议吗?

额外信息:

高级用例:

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这个。

相关:


要实施您的额外时间范围(您的 "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 byhaving 应该具有您想要的一致的性能特征(尽管对于简单的查询可能会更慢)。这种方法(或任何方法)的诀窍是当给定患者有多个类别时会发生什么。

例如,这个或你的方法会发现:

admission --> operation --> admission --> medication

我怀疑你真的不想找到这些记录。您可能需要一个中间级别,代表给定患者的某种 "episode"。

如果是这种情况,您应该问 另一个 问题,并提供更清晰的数据示例、您可能想问的问题以及匹配和不匹配的案例条件。