Как оптимизировать запрос для вычисления зависимых от строки отношений даты и времени? - PullRequest
0 голосов
/ 02 января 2019

Скажем, у меня есть упрощенная модель, в которой patient может иметь ноль или более events. Событие имеет category и date. Я хочу поддержать такие вопросы, как:

Find all patients that were given a medication after an operation and 
the operation happened after an admission. 

Там, где лекарства, операции и вход являются все типы категорий событий. Есть ~ 100 возможных категорий.

Я ожидаю тысячи пациентов, и у каждого пациента есть ~ 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;

Однако это решение плохо масштабируется, так как добавлено больше категорий / фильтров. С 1000 пациентов и 45 000 событий я вижу следующее поведение производительности:

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

Объясните: explain

У кого-нибудь есть предложения по оптимизации этой модели запроса / данных?

Дополнительная информация:

  • Postgres 10,6
  • В выводе Объяснения 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.

Ответы [ 2 ]

0 голосов
/ 02 января 2019

Вы можете обнаружить, что условное агрегирование выполняет то, что вы хотите. Компонент времени может быть трудным для обработки (см. Ниже), если ваши последовательности усложняются, но основная идея:

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

Я подозреваю, что вы на самом деле не хотите найти эти записи. Вам, вероятно, нужен промежуточный уровень, представляющий своего рода «эпизод» для данного пациента.

Если это так, вам следует задать другой вопрос с более ясными примерами как данных, так и вопросов, которые вы, возможно, захотите задать, и случаев, которые соответствуют и не соответствуют условиям.

0 голосов
/ 02 января 2019

Во-первых, если ссылочная целостность обеспечивается с помощью ограничений 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;

Затем избавьтесь от повторного умножения строк и * 1005.* чтобы противостоять этому во внешнем SELECT с помощью EXISTS полусоединений вместо:

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

Обратите внимание, что все еще может быть дубликаты в допуске , но это, вероятно, главная проблема в вашей модели данных / дизайне запроса, и потребуется пояснение, как описано в комментариях.

Если вы действительно хотите объединить все случаивместе с одним и тем же пациентом по какой-то причине существуют различные способы получить самую раннюю госпитализацию для каждого пациента на начальном этапе - и повторять аналогичный подход для каждого дополнительного этапа.Вероятно, самый быстрый для вашего случая (повторное представление таблицы пациентов в запросе):

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 (предпочтительно в форме автоочистки) может идти в ногу с операциями записи или у вас есть только для чтенияДля начала вы получите очень быстрое сканирование только по индексу из этого.

Связанный:


Для реализации дополнительных временных рамок (ваш «расширенный вариант использования» ), основываться на втором запросе, так как мы должны снова рассмотреть все событий.

У вас действительно должны быть идентификаторы случаев или что-то более определенное, чтобы связать операцию с поступлением и лечениемоперация и т. д., где это уместно.(Это может быть просто 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 арифметика:

...