Мінливість (volatility) - обіцянка, яку функція дає планувальнику про свою поведінку. Від неї залежить, які оптимізації PostgreSQL може застосувати.
| Категорія | Обіцянка | Приклади |
|---|---|---|
IMMUTABLE |
для тих самих аргументів завжди той самий результат, нічого не читає з бази | lower(text), abs(), математика |
STABLE |
той самий результат у межах одного оператора, може читати базу | now(), функції, що залежать від налаштувань сесії |
VOLATILE (за замовчуванням) |
результат може змінюватися навіть між рядками, можливі побічні ефекти | random(), nextval(), clock_timestamp() |
Що від цього залежить:
1. Індекси за виразом дозволені лише для IMMUTABLE-функцій:
CREATE INDEX ON users (lower(email)); -- працює
CREATE INDEX ON events ((created_at::date)); -- помилка для timestamptz
ERROR: functions in index expression must be marked IMMUTABLE
Приведення timestamptz до date залежить від часового поясу сесії, тож результат не незмінний. Рішення - зафіксувати пояс: ((created_at AT TIME ZONE 'UTC')::date).
2. Обчислення один раз: STABLE чи IMMUTABLE функцію з константними аргументами в WHERE планувальник може обчислити один раз і використати індекс. VOLATILE - викликається для кожного рядка, і індекс за нею не використати.
3. Згенеровані колонки й умови партицій теж вимагають IMMUTABLE.
Пастка - неправдива позначка. Власну функцію можна оголосити IMMUTABLE, навіть якщо вона читає таблицю:
CREATE FUNCTION tax_rate(country text) RETURNS numeric
LANGUAGE sql IMMUTABLE -- неправда: читає таблицю
AS $$ SELECT rate FROM tax_rates WHERE code = country $$;
PostgreSQL повірить. Індекс за такою функцією зберігатиме значення на момент вставки, і після зміни tax_rates індекс поверне неправильні дані - тихо, без помилок. Кешовані плани підготовлених запитів теж можуть закріпити старий результат.
Правило: позначайте функцію найсуворішою категорією, яка справді правдива. Сумніви - STABLE чи VOLATILE. Вигода від неправдивого IMMUTABLE не варта пошкоджених даних.
Пов'язане - PARALLEL SAFE: чи можна виконувати функцію в паралельних воркерах. Власні функції за замовчуванням PARALLEL UNSAFE - це вимикає паралельні плани для запитів з ними.
У Laravel такі функції й індекси за виразами створюють у міграціях через DB::statement(), а запити мають використовувати точно той самий вираз, що в індексі (whereRaw('lower(email) = ?', [...])), інакше індекс не підхопиться.