PostgreSQL Antipatterns: вредни JOIN и OR

Бойте се от операции, buffers, които носят...
На примера на един малък заявка ще разгледаме някои универсални подходи за оптимизация на заявки в PostgreSQL. Да ги ползвате или не – решавате вие, но е добре да знаете за тях.

В някои последващи версии на PG ситуацията може да се промени с "умния" плановик, но за 9.4/9.6 тя изглежда горе-долу еднакво, както примерите тук.

Ще взема напълно реална заявка:

SELECT
  TRUE
FROM
  "Документ" d
INNER JOIN
  "ДокументРасширение" doc_ex
    USING("@Документ")
INNER JOIN
  "ТипДокумента" t_doc ON
    t_doc."@ТипДокумента" = d."ТипДокумента"
WHERE
  (d."Лицо3" = 19091 or d."Сотрудник" = 19091) AND
  d."$Черновик" IS NULL AND
  d."Удален" IS NOT TRUE AND
  doc_ex."Состояние"[1] IS TRUE AND
  t_doc."ТипДокумента" = 'ПланРабот'
LIMIT 1;

за имената на таблиците и полетатаКъм "руските" названия на полетата и таблиците може да се подхожда по различен начин, но това е въпрос на вкус. Тъй като в "Тензор" нямаме разработчици и чужденци, а PostgreSQL ни позволява да даваме имена дори на йероглифи, стига те да са в кавички, предпочитаме да именуваме обектите точно и ясно, за да не възникват неясноти.
Нека да погледнем получен план:
PostgreSQL Antipatterns: вредни JOIN и OR
Както предполагахме, намерихме всички 30 записа. Но отне 60% от времето — защото направихме и 30 търсения по индекса. А можем ли да направим по-малко?

144ms и почти 53K buffers — тоест повече от 400MB данни! И ще имаме късмет, ако всички те се окажат в кеша в момента на нашата заявка, иначе тя ще стане многократно по-дълга при четенето от диска.

Алгоритъмът е всичко!

За да оптимизираме каквато и да е заявка, първо трябва да разберем какво точно трябва тя да прави.
Оставяме за момента разработката на самата структура на БД и се споразумяваме, че можем относително "евтино" да пренапишем заявката и/или да наложим на базата някои нужни ни индекси..

И така, заявката:
— проверява съществуването на някой документ
— в нужното ни състояние и определен тип
— където автор или изпълнител е нужният ни служител.

JOIN + LIMIT 1

Достатъчно често на разработчика му е по-лесно да напише заявка, където първо се извършва свързване на голям брой таблици, а след това от всичкото това множество остава един-единствен запис. Но по-лесно за разработчика не значи непременно по-ефективно за БД.
В нашия случай таблиците бяха само 3 — а какъв ефект...

Нека първо да се отървем от свързването с таблицата "ТипДокумента", а заедно да подскажем на базата, че записът с типа е уникален (ние знаем това, а плановикът все още не подозира):

С T КАК (
  ВЫБРАТЬ
    "@ТипДокумента"
  ИЗ
    "ТипДокумента"
  ГДЕ
    "ТипДокумента" = 'ПланРабот'
  ЛИМИТ 1
)
...
ГДЕ
  d."ТипДокумента" = (ТАБЛИЦА T)
...

Да, если таблица/CTE состоит из единственного поля и единственной записи, то в PG можно написать даже так, вместо

d."ТипДокумента" = (ВЫБРАТЬ "@ТипДокумента" ИЗ T ЛИМИТ 1)

«Ленивые» вычисления в запросах PostgreSQL

BitmapOr против UNION

В некоторых случаях Bitmap Heap Scan будет стоить нам очень дорого — например, в нашей ситуации, когда достаточно много записей подпадает под требуемое условие. Мы получили это из-за OR-условия, превратившегося в BitmapOr-операцию в плане.
Вернемся к исходной задаче — необходимо найти запись, соответствующую любому из условий — то есть незачем искать все 59K записей по обоим условиям. Есть способ отработать одно условие, а ко второму перейти только когда по первому ничего не нашлось. Нам поможет такая конструкция:

(
  ВЫБРАТЬ
    ...
  ЛИМИТ 1
)
UNION ALL
(
  ВЫБРАТЬ
    ...
  ЛИМИТ 1
)
ЛИМИТ 1

«Внешний» ЛИМИТ 1 гарантирует, что поиск завершится при нахождении первой же записи. И если она найдется уже в первом блоке, выполнение второго осуществляться не будет (never executed в плане).

«Прячем под CASE» сложные условия

В исходном запросе есть крайне неудобный момент — проверка состояния по связанной таблице «ДокументРасширение». Независимо от истинности остальных условий в выражении (например, d.«Удален» IS NOT TRUE), это соединение выполняется всегда и «стоит ресурсов». Больше или меньше их будет потрачено — зависит от объема этой таблицы.
Но можно модифицировать запрос так, чтобы поиск связанной записи происходил бы только тогда, когда это действительно необходимо:

ВЫБРАТЬ
  ...
ИЗ
  "Документ" d
ГДЕ
  ... /*index cond*/ И
  CASE
    КОГДА "$Черновик" IS NULL И "Удален" IS NOT TRUE ТО ЕСТЬ (
      ВЫБРАТЬ
        "Состояние"[1] IS TRUE
      ИЗ
        "ДокументРасширение"
      ГДЕ
        "@Документ" = d."@Документ"
    )
  КОНЕЦ

Раз из связываемой таблицы нам не нужно для результата ни одно из полей, то мы имеем возможность превратить JOIN в условие по подзапросу.
Оставим индексируемые поля «за скобками» CASE, простые условия от записи вносим в блок WHEN — и теперь «тяжелый» запрос выполняется только при переходе в THEN.

Моя фамилия «Итого»

Собираем результирующий запрос со всеми описанными выше механизмами:

С T КАТО (
  ИЗ
    "ТипДокумента"
  КЪДЕ
    "ТипДокумента" = 'ПланРабот'
)
  (
    ИЗ
      "Документ" d
    КЪДЕ
      ("Лицо3", "ТипДокумента") = (19091, (ТАБЛИЦА T)) И
      СЛУЧАЙ
        КОГАТО "$Черновик" Е NULL И "Удален" НЕ Е ИСТИНА ТОГАВА (
          ИЗ
            "ДокументРасширение"
          КЪДЕ
            "@Документ" = d."@Документ"
        )
      КРАЙ
    ОГРАНИЧЕНИЕ 1
  )
СЪЮЗ ВСИЧКИ
  (
    ИЗ
      "Документ" d
    КЪДЕ
      ("ТипДокумента", "Сотрудник") = ((ТАБЛИЦА T), 19091) И
      СЛУЧАЙ
        КОГАТО "$Черновик" Е NULL И "Удален" НЕ Е ИСТИНА ТОГАВА (
          ИЗ
            "ДокументРасширение"
          КЪДЕ
            "@Документ" = d."@Документ"
        )
      КРАЙ
    ОГРАНИЧЕНИЕ 1
  )
ОГРАНИЧЕНИЕ 1;

Настройваме [под] индексите

Опитният поглед забеляза, че индексируемите условия в подблоковете UNION леко се различават — това е, защото вече имаме подходящи индекси на таблицата. А ако ги нямаше — щеше да си струва да се създадат: Документ(Лицо3, ТипДокумента) и Документ(ТипДокумента, Сотрудник).
за реда на полетата в ROW-условиятаОт гледна точка на планировача, разбира се, може да се напише и (A, B) = (constA, constB), и (B, A) = (constB, constA). Но при записване в реда на полетата в индекса, такава заявка е просто по-удобна за отстраняване на проблеми след това.
Какво следва?
PostgreSQL Antipatterns: вредни JOIN и OR
Както предполагахме, намерихме всички 30 записа. Но отне 60% от времето — защото направихме и 30 търсения по индекса. А можем ли да направим по-малко?

За съжаление, нямаха късмет и в първия блок на UNION не беше намерено нищо, затова вторият все пак премина в изпълнение. Но дори и при това — само 0.037ms и 11 buffers!
Ускорихме заявката и намалихме „прокачването“ на данни в паметта хиляди пъти, като използвахме съвсем прости методи — добър резултат при малко копипейст. 🙂

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster