Функция PostgreSQL не выполняется при вызове из CTE

14

Просто надеюсь подтвердить мои наблюдения и получить объяснение того, почему это происходит.

У меня есть функция, определенная как:

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?

Энди
источник

Ответы:

11

Это своего рода ожидаемое поведение. 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 *качестве вашего примера, чтобы все, что вызывает этот запрос, знали, были ли обновления и каковы были изменения в таблице.

ypercubeᵀᴹ
источник
4

Это ожидаемое, документированное поведение.

Том Лейн объясняет это здесь.

Документировано в руководстве здесь:

Операторы изменения данных в WITHвыполняются ровно один раз и всегда до завершения , независимо от того, считывает ли первичный запрос все (или вообще любые) их выходные данные. Обратите внимание, что это отличается от правила для SELECTin 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. Я добавил некоторые разъяснения выше. Это ответственность пользователя, если он (и) добавляет код в функцию, которая изменяет данные за кулисами.
Эрвин Брандштеттер