Функція PostgreSQL не виконується при виклику зсередини CTE


16

Просто сподіваюся підтвердити своє спостереження і отримати пояснення, чому це відбувається.

У мене функція визначена як:

CREATE OR REPLACE FUNCTION "public"."__post_users_id_coin" ("coins" integer, "userid" integer) RETURNS TABLE (id integer) AS '
UPDATE
users
SET
coin = coin + coins
WHERE
userid = users.id
RETURNING
users.id' LANGUAGE "sql" COST 100 ROWS 1000
VOLATILE
RETURNS NULL ON NULL INPUT
SECURITY INVOKER

Коли я викликаю цю функцію з CTE, вона виконує команду SQL, але не запускає функцію, наприклад:

WITH test AS
(SELECT * FROM __post_users_id_coin(10, 1))

SELECT
1 -- Select 1 but update not performed

З іншого боку, якщо я викликаю функцію з CTE, а потім вибираю результат CTE (або викликаю функцію безпосередньо без CTE), він виконує команду SQL і робить функцію, наприклад:

WITH test AS
(SELECT * FROM __post_users_id_coin(10, 1))

SELECT
*
FROM
test -- Select result and update performed

або

SELECT * FROM __post_users_id_coin(10,1)

Оскільки я не дуже переймаюся результатом функції (просто потрібно, щоб виконати оновлення), чи є якийсь спосіб змусити це працювати, не вибираючи результат CTE?

Відповіді:


12

Це така очікувана поведінка. CTE матеріалізовані, але є виняток.

Якщо CTE не посилається на батьківський запит, він взагалі не матеріалізується. Ви можете спробувати це, наприклад, і це буде нормально:

WITH not_executed AS (SELECT 1/0),
     executed AS (SELECT 1)
SELECT * FROM executed ;

Код скопійований із коментаря у публікації до блогу Крейга Рінгера:
CTE PostgreSQL - це огорожі для оптимізації .


Перш ніж спробувати цей і кілька подібних запитів, я подумав, що виняток був: "коли CTE не посилається на батьківський запит або в інший CTE і не посилається на інший CTE". Отже, якщо ви хотіли, щоб CTE був виконаний, але результати, які не відображаються в результаті запиту, я подумав, що це буде обхідним способом (посилаючись на нього в іншому CTE).

Але, на жаль, це не працює, як я очікував:

WITH test AS
    (SELECT * FROM __post_users_id_coin(10, 1)),
  execute_test AS 
    (TABLE test)
SELECT 1 ;     -- no, it doesn't do the update

і тому моє "правило виключення" не є правильним. Коли на CTE посилається інший CTE, і жоден з них не посилається на батьківський запит, ситуація є складнішою, і я не впевнений, що саме відбувається і коли CTE будуть матеріалізовані. Я також не можу знайти жодної посилання на подібні випадки в документації.


Я не бачу кращого рішення, ніж використовувати те, що ви вже запропонували:

SELECT * FROM __post_users_id_coin(10, 1) ;

або:

WITH test AS
    (SELECT * FROM __post_users_id_coin(10, 1))
SELECT *
FROM test ;

Якщо функція оновлює кілька рядків, і ви отримуєте багато рядків (з 1) в результаті, ви можете об'єднатись, щоб отримати один рядок:

SELECT MAX(1) AS result FROM __post_users_id_coin(10, 1) ;

але я вважаю за краще, щоб результати функції, яка виконує оновлення, поверталися з SELECT *вашим прикладом, тому незалежно від того, що викликає цей запит, відомо, чи були оновлення та які зміни були в таблиці.



5

Це очікувана, задокументована поведінка.

Том Лейн пояснює це тут.

Документовано в посібнику тут:

Дані , що модифікують заяви в WITHвиконуються рівно один раз, і завжди до кінця , незалежно від того , первинний запит зчитує всі (або дійсно будь-який) своєї продукції. Зауважте, що це відрізняється від правила для SELECTв WITH: як зазначено в попередньому розділі, виконання a SELECTздійснюється лише настільки, наскільки первинний запит вимагає його виведення .

Сміливий акцент мій. «Дані модифікують» є INSERT, UPDATEі DELETEзапити. (На відміну від SELECT.). Посібник ще раз:

Ви можете використовувати оператори даних модифікують ( INSERT, UPDATEабо DELETE) в WITH.

Правильна функція

CREATE OR REPLACE FUNCTION public.__post_users_id_coin (_coins integer, _userid integer)
  RETURNS TABLE (id integer) AS
$func$
UPDATE users u
SET    coin = u.coin + _coins  -- see below
WHERE  u.id = _userid
RETURNING u.id
$func$ LANGUAGE sql COST 100 ROWS 1000 STRICT;

Я скинув за замовчуванням (шум) пропозиції і STRICTє коротким синонімомRETURNS NULL ON NULL INPUT .

Переконайтеся, що імена параметрів не суперечать іменам стовпців. Я прикидався _, але це лише мої особисті переваги.

Якщо coinможе бути NULLя пропоную:

SET    coin = CASE WHEN coin IS NULL THEN _coins ELSE coin + _coins END

Якщо users.idце первинний ключ, то ні це RETURNS TABLEне ROWs 1000має жодного сенсу. Оновити / повернути можна лише один рядок Але це все поруч із головним моментом.

Правильний дзвінок

Немає сенсу використовувати RETURNINGзастереження та повертати значення зі своєї функції, якщо ви все одно будете ігнорувати повернені значення у виклику. Також немає сенсу розкладати повернуті рядки, SELECT * FROM ...якщо ви все одно проігноруєте їх.

Просто поверніть скалярну константу ( RETURNING 1), визначте функцію як RETURNS int(або RETURNINGвзагалі опустіть і зробіть її RETURNS void) та зателефонуйте їйSELECT my_function(...)

Рішення

Оскільки ви ...

не дуже хвилює результат

.. просто SELECTпостійна форма CTE. Він гарантовано буде виконаний до тих пір, поки на нього посилається зовні SELECT(прямо чи опосередковано).

WITH test AS (SELECT __post_users_id_coin(10, 1))
SELECT 1 FROM test;

Якщо у вас є функція повернення набору і все одно не хвилюєтесь про вихід:

WITH test AS (SELECT * FROM __post_users_id_coin(10, 1))
SELECT 1 FROM test LIMIT 1;

Не потрібно повертати більше 1 ряду. Функція все ще називається.

Нарешті, незрозуміло, для чого вам потрібен CTE. Напевно, просто доказ концепції.

Тісно пов'язані:

Відповідна відповідь на SO:

І врахуйте:


Страхітливий, великий шанувальник і мав честь мати свою відповідь і Ервін. Я використовую CTE, коли я роблю INSERTраніше, ніж в UPDATEрамках тієї ж функції обгортання - транзакцій немає.
Енді,

Приємно. Просто водно: це testв WITH test AS (SELECT * FROM __post_users_id_coin(10, 1)) SELECT ... LIMIT 1;вважається зміною КТРА чи ні?
ypercubeᵀᴹ

@ ypercubeᵀᴹ: A SELECTне "змінює дані" згідно термінології CTE. Я додав трохи уточнень вище. Користувач несе відповідальність, якщо він додає код до функції, яка модифікує дані за шторами.
Ервін Брандстеттер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.