【问题标题】:How to optimize query to compute row-dependent datetime relationships?如何优化查询以计算与行相关的日期时间关系?
【发布时间】:2019-05-29 05:36:01
【问题描述】:

假设我有一个简化模型,其中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 个事件。

我想出的天真的解决方案是有两个表,一个patient 和一个event 表。在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;

但是,随着添加更多类别/过滤器,此解决方案无法很好地扩展。对于 1,000 名患者和 45,000 个事件,我看到以下性能行为:

| number of inner joins | approx. query response |
| --------------------- | ---------------------- |
| 2                     | 100ms                  |
| 3                     | 500ms                  |
| 4                     | 2000ms                 |
| 5                     | 8000ms                 | 

说明:

有人对如何优化此查询/数据模型有任何建议吗?

额外信息:

  • 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.

【问题讨论】:

  • 感谢 @ErwinBrandstetter 的提醒,添加了 postgres 版本。
  • 您对事件表所做的操作称为 EAV 模型。它有利有弊。我个人喜欢它,但我知道它的极限是什么。 (少数)缺点之一是您正在尝试的性能问题。这里没有神奇的解决方案。完全改变模型是其中之一,为查询最多的事件创建物化视图是另一个。如果你用谷歌搜索 EAV 性能问题,你会发现很多想法。
  • @ThomasG:我建议的重写应该导致 3 次仅索引扫描(在理想的读取条件下),并且相比之下表现得像所说的“奇迹”。
  • @ErwinBrandstetter 您的解决方案很好(我赞成),但是它有其局限性,您知道 :) 您使用 3 个类别,但是 10 会发生什么? 3 只猫他有 500 毫秒,5 只他有 8000 毫秒。你的方法也会发生同样的情况,但希望不是相同的规模。
  • @ThomasG:当然,有限制。但这对于 任何 个级别应该很快,因为每个添加的级别都会减少下一步的行数。所以它应该在这方面很好地扩展——除非每个级别都没有选择性。如果早期步骤是有选择性的,这将很有帮助……也许我们会从 OP 那里得到反馈。

标签: sql postgresql performance data-modeling exists


【解决方案1】:

首先,如果通过 FK 约束强制执行参照完整性,您可以从查询中完全删除 patient 表:

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

请注意,准入中仍然可能存在重复项,但这可能是您的数据模型/查询设计中的主要问题,需要在 cmets 中进行说明。

如果您出于某种原因确实希望将同一患者的所有病例汇总在一起,有多种方法可以在初始步骤中让每位患者最早入院 - 并重复类似的方法每一个额外的步骤。对于您的情况可能最快(将患者表重新引入查询):

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

见:

您可以通过缩短冗长(且冗余)的类别名称来优化您的表格设计。使用查找表并仅存储 integer(或者甚至将 int2"char" 值作为 FK。)

为了获得最佳性能(这是至关重要的),请在 (parent_id, category, date DESC) 上设置一个多列索引,并确保所有三列都定义为 NOT NULL。索引表达式的顺序很重要。 DESC 在这里大部分是可选的。在您的情况下,Postgres 可以使用默认的ASC 排序顺序的索引几乎同样有效。

如果 VACUUM(最好以 autovacuum 的形式)可以跟上写操作,或者您一开始处于只读状态,那么您将很快得到 index-only scans

相关:


要实施您的额外时间框架(您的“高级用例”),请在第二个查询的基础上构建,因为我们必须考虑所有再次发生事件。

您真的应该有病例 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算术:

【讨论】:

  • 您的最终查询仍然需要count(distinct patient_id)s,以防止具有多个admissions 的患者被多次计数。除此之外,这是一个很好的答案,并得到了我的支持。
  • @Sentinel:好点。而且是真的。但这可能暗示数据模型/查询一开始就存在逻辑缺陷。因为同一患者只应接受同一手术一次。目前,对于以后入院的治疗(针对不同的手术)似乎会产生误报。缺少的是“案例 ID”或类似的东西,以将各种类别与适用的案例联系起来。
  • @ErwinBrandstetter 是的,我可能过于简单化了,但我确实想避免你提到的误报。我希望使用像date_truncate 这样的日期函数来支持像find patients that were given a medication after an operation but on the same day 这样的用例。理想情况下,我想支持find patients that were given a medication within x days of an operation 之类的东西。我会用这些额外的细节更新问题。
  • 感谢@ErwinBrandstetter 的回答,5 个类别的查询时间从~8000ms 减少到~100ms!随着更多过滤器/类别的添加,性能仍然略有下降,我将继续测试,但现在这对于我的用例来说已经足够了。
  • 顺便说一句:您的最终查询可以简化为单个 EXISTS( event e1 JOIN event e2 ...) 无需嵌套 EXISTS(e2),恕我直言。
【解决方案2】:

您可能会发现条件聚合可以满足您的需求。如果您的序列变得复杂,时间组件可能难以处理(见下文),但基本思想:

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

我怀疑您并不想找到这些记录。您可能需要一个中间级别,代表给定患者的某种“情节”。

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

【讨论】:

  • 听起来您正在解决与此评论相同的设计问题(几乎同时):stackoverflow.com/questions/54008579/…
  • 在我们的模型中,我不认为我们可以创建像情节这样的东西,即数据是在我们控制之外创建的。关于这个问题,我最初的想法是使用日期函数来启用诸如“查找在入院当天发生的药物”之类的事情。我试图简化问题,但也许我简化了太多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-26
  • 1970-01-01
相关资源
最近更新 更多