SQL це не C++, і не JavaScript. Тому обчислення логічних виразів відбувається інакше, і ось це — зовсім не те саме:
WHERE fncondX() AND fncondY()
= fncondX() && fncondY()
У процесі оптимізації плану виконання запиту PostgreSQL
Тому якщо керувати пріоритетом таки хочеться, треба структурно зробити ці умови нерівними за допомогою умовних
Дані та робота з ними - основа
#0: RTFM
стартовий
Коли порядок обчислення є важливим, його можна зафіксувати за допомогою конструкції
CASE
. Наприклад, такий спосіб уникнути поділу на нуль у реченніWHERE
ненадійний:SELECT ... WHERE x > 0 AND y/x > 1.5;
Безпечний варіант:
SELECT ... WHERE CASE WHEN x > 0 THEN y/x > 1.5 ELSE false END;
Конструкція, що застосовується так
CASE
захищає вираз від оптимізації, тому використовувати її потрібно лише за необхідності.
#1: умова у тригері
BEGIN
IF cond(NEW.fld) AND EXISTS(SELECT ...) THEN
...
END IF;
RETURN NEW;
END;
Начебто все виглядає добре, але... Ніхто не обіцяє, що вкладений SELECT
не виконуватиметься при помилковості першої умови. Виправити за допомогою вкладених IF
:
BEGIN
IF cond(NEW.fld) THEN
IF EXISTS(SELECT ...) THEN
...
END IF;
END IF;
RETURN NEW;
END;
Тепер подивимося уважно — все тіло тригерної функції виявилося «загорнуто» у IF
. А це означає, що нам ніщо не заважає винести цю умову з процедури за допомогою WHEN
-Умови
BEGIN
IF EXISTS(SELECT ...) THEN
...
END IF;
RETURN NEW;
END;
...
CREATE TRIGGER ...
WHEN cond(NEW.fld);
Такий підхід дозволяє гарантовано заощадити ресурси сервера за помилковості умови.
#2: OR/AND-ланцюжок
SELECT ... WHERE EXISTS(... A) OR EXISTS(... B)
В іншому випадку можна отримати, що обидва EXISTS
будуть «істинними», але обидва і виконаються.
Але якщо ми точно знаємо, що один із них буває «істинним» набагато частіше (або «хибним» — для AND
-ланцюжка) - чи не можна якось «підвищити його пріоритет», щоб другий не виконувався зайвий раз?
Виявляється, можна — алгоритмічно підхід близький до статті
Давайте просто «засунемо під CASE» обидві ці умови:
SELECT ...
WHERE
CASE
WHEN EXISTS(... A) THEN TRUE
WHEN EXISTS(... B) THEN TRUE
END
У цьому випадку ми не визначали ELSE
-значення, тобто у разі помилковості обох умов CASE
поверне NULL
що трактується як FALSE
в WHERE
-Умови.
Даний приклад можна скомбінувати й інакше – на смак та колір:
SELECT ...
WHERE
CASE
WHEN NOT EXISTS(... A) THEN EXISTS(... B)
ELSE TRUE
END
#3: як [не] треба писати умови
На розбір причин дивного спрацювання цього тригера ми витратили два дні — давайте подивимося, чому.
Вихідник:
IF( NEW."Документ_" is null or NEW."Документ_" = (select '"Комплект"'::regclass::oid) or NEW."Документ_" = (select to_regclass('"ДокументПоЗарплате"')::oid)
AND ( OLD."ДокументНашаОрганизация" <> NEW."ДокументНашаОрганизация"
OR OLD."Удален" <> NEW."Удален"
OR OLD."Дата" <> NEW."Дата"
OR OLD."Время" <> NEW."Время"
OR OLD."ЛицоСоздал" <> NEW."ЛицоСоздал" ) ) THEN ...
Проблема №1: нерівність не враховує NULL
Уявімо, що все OLD
-поля мали значення NULL
. Що вийде?
SELECT NULL <> 1 OR NULL <> 2;
-- NULL
А з погляду відпрацювання умови NULL
еквівалентний FALSE
, як згадувалося вище.
Рішення: використовуйте оператор IS DISTINCT FROM
ROW
-оператора, порівнюючи відразу цілі записи:
SELECT (NULL, NULL) IS DISTINCT FROM (1, 2);
-- TRUE
Проблема №2: різна реалізація однакового функціоналу
порівняємо:
NEW."Документ_" = (select '"Комплект"'::regclass::oid)
NEW."Документ_" = (select to_regclass('"ДокументПоЗарплате"')::oid)
Навіщо тут зайві вкладені SELECT
? А функція to_regclass
? А по-різному чому?..
Виправимо:
NEW."Документ_" = '"Комплект"'::regclass::oid
NEW."Документ_" = '"ДокументПоЗарплате"'::regclass::oid
Проблема №3: пріоритет bool-операцій
Відформатуємо вихідний код:
{... IS NULL} OR
{... Комплект} OR
{... ДокументПоЗарплате} AND
( {... неравенства} )
Упс… За фактом, вийшло, що у разі істинності будь-якої з двох перших умов, вся умова цілком звертається до TRUE
без урахування нерівностей. А це зовсім не те, що ми хотіли.
Виправимо:
(
{... IS NULL} OR
{... Комплект} OR
{... ДокументПоЗарплате}
) AND
( {... неравенства} )
Проблема №4 (маленька): складна OR-умова для одного поля
Власне, проблеми у №3 у нас виникли рівно тому, що умов було три. Але замість них можна обійтися одним, за допомогою механізму coalesce ... IN
:
coalesce(NEW."Документ_"::text, '') IN ('', '"Комплект"', '"ДокументПоЗарплате"')
Так ми й NULL
«спіймаємо», і складних OR
зі дужками городити не доведеться.
Разом
Зафіксуємо те, що в нас вийшло:
IF (
coalesce(NEW."Документ_"::text, '') IN ('', '"Комплект"', '"ДокументПоЗарплате"') AND
(
OLD."ДокументНашаОрганизация"
, OLD."Удален"
, OLD."Дата"
, OLD."Время"
, OLD."ЛицоСоздал"
) IS DISTINCT FROM (
NEW."ДокументНашаОрганизация"
, NEW."Удален"
, NEW."Дата"
, NEW."Время"
, NEW."ЛицоСоздал"
)
) THEN ...
А якщо врахувати, що ця тригерна функція може застосовуватися тільки в UPDATE
-тригері через наявність OLD/NEW
за умови верхнього рівня, то цю умову можна взагалі винести в WHEN
-Умова, як було показано в #1 ...
Джерело: habr.com