- PVSM.RU - https://www.pvsm.ru -

SQL HowTo: разные варианты работы с EAV

Соблазн использовать модель EAV [1] (Entity-Attribute-Value) при организации структуры БД весьма велик, особенно когда предметная область заранее плохо известна (или разработчик просто не хочет в нее углубляться). Это ведь так удобно - создать "универсальный" способ описания характеристик объектов, который больше не потребует доработок базы ни при появлении новых типов объектов, ни при возникновении новых атрибутов...

Однако, за любую универсальность приходится платить сложностью и производительностью запросов - так что json[b] может оказаться более эффективной заменой [2]. Но если уж такая модификация невозможна - давайте попробуем выжать максимум производительности из доставшегося нам legacy на самом простом примере.

SQL HowTo: разные варианты работы с EAV - 1

Ограничимся работой с единственной таблицей значений:

CREATE TABLE tst_eav AS
SELECT
  (random() * 1e4)::integer e -- 10k объектов
, (random() * 1e2)::integer a -- 100 характеристик
, (random() * 1e2)::integer v -- 100 вариантов значений
FROM
  generate_series(1, 1e6);    -- 1M записей о значениях

Попробуем найти такие объекты e, для которых одновременно существуют записи с (a, v) = (1, 1) и (a, v) = (2, 2) - это типичный вариант множественного фильтра в любом интернет-магазине: "смартфоны с экраном 6" и памятью 64GB".

JOIN

Самым первым вариантом решения, пришедшим в голову разработчика уровня "я уже освоил SQL!", наверняка, будет соединение:

SELECT
  e
FROM
  tst_eav r1
JOIN
  tst_eav r2
    USING(e)
WHERE
  (r1.a, r1.v) = (1, 1) AND
  (r2.a, r2.v) = (2, 2);

Очевидно, для этого нам понадобится, как минимум, индекс по (a, v):

CREATE INDEX eav_idx1 ON tst_eav(a, v);

Посмотрим, что у нас получится в плане [3]:

JOIN'им два набора строк
JOIN'им два набора строк

Сначала отбор по одной паре значений и сортировка по e, потом - по второй паре и сортировка, а потом уже - слияние двух отсортированных наборов.

Этот вариант станет для нас отправной точкой: 432мкс + 207 buffers.

Неплохо для отбора из миллиона записей, но можно лучше!

INTERSECT

Ведь в предыдущем запросе мы искали вовсе не соединение, а пересечение множеств - так давайте его и попробуем использовать:

  SELECT
    e
  FROM
    tst_eav
  WHERE
    (a, v) = (1, 1)
INTERSECT
  SELECT
    e
  FROM
    tst_eav
  WHERE
    (a, v) = (2, 2);
INTERSECT'им наборы
INTERSECT'им наборы

А в плане [4] теперь все получше - читаем ровно столько же, зато не пришлось тратить время на две сортировки: 301мск + 207 buffers.

GROUP BY

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

SELECT
  e
FROM
  tst_eav
WHERE
  (a, v) IN ((1, 1), (2, 2))
GROUP BY
  e
HAVING
  count(*) = 2; -- присутствуют оба условия
GROUP BY сразу пары условий
GROUP BY сразу пары условий

Сэкономили "копеечку" [5]: 296мкс + 202 buffers.

Конечно, тут мы пошли на допущение, что каждая пара (a, v) внутри одного объекта заведомо уникальна. Потому что если это не так, запрос станет существенно сложнее:

SELECT
  e
FROM
  tst_eav T
WHERE
  (a, v) IN ((1, 1), (2, 2))
GROUP BY
  e
HAVING
  array_length(array_agg(DISTINCT T), 1) = 2; -- оба уникальных условия

INCLUDE

Но всегда терзает мысль - может быть, можно сделать запрос еще быстрее?.. Оказывается, в нашем случае - можно!

Заметим, что львиная доля времени уходит на Bitmap Heap Scan - то есть вычитку страниц таблицы ради получения значения e, ведь его нет в нашем индексе, иначе мы могли бы обойтись Index Only Scan.

Но ведь еще с PostgreSQL 11 есть способ добавить неключевые поля в индекс:

CREATE INDEX eav_idx2 ON tst_eav(a, v) INCLUDE(e);
INCLUDE избавляет от Bitmap Heap Scan
INCLUDE избавляет от Bitmap Heap Scan

И вот теперь наш план [6] для INTERSECT-варианта: 121мкс + 9 buffers.

А ведь чем меньше страниц данных (buffers) читается, тем меньше шансов сходить за ними на диск и потерять в скорости.


Напоминаю, что анализировать планы запросов и бороться за их производительность удобнее всего с помощью визуализаций на explain.tensor.ru [7].

Автор: Боровиков Кирилл

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/postgresql/373456

Ссылки в тексте:

[1] модель EAV: https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model

[2] json[b] может оказаться более эффективной заменой: https://www.pvsm.ru/ru/post/475178/

[3] в плане: https://explain.tensor.ru/archive/explain/350c3340f570e9febe8d32328ccc21b0:0:2022-03-29

[4] в плане: https://explain.tensor.ru/archive/explain/f5f2dcba277313700b9893caa5a28e80:0:2022-03-29

[5] "копеечку": https://explain.tensor.ru/archive/explain/ee95b99bd140c0ed2e682009d42a0d5c:0:2022-03-29

[6] наш план: https://explain.tensor.ru/archive/explain/ce8ebb925456212d09f9d1e6039b78a8:0:2022-03-29

[7] explain.tensor.ru: https://explain.tensor.ru/

[8] Источник: https://habr.com/ru/post/657895/?utm_source=habrahabr&utm_medium=rss&utm_campaign=657895