Почему строки, вставленные в CTE, не могут быть обновлены в одном выражении?

13

В PostgreSQL 9.5 приведена простая таблица, созданная с помощью:

create table tbl (
    id serial primary key,
    val integer
);

Я запускаю SQL, чтобы ВСТАВИТЬ значение, а затем ОБНОВИТЬ его в том же выражении:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

Результатом является то, что ОБНОВЛЕНИЕ игнорируется:

testdb=> select * from tbl;
┌────┬─────┐
 id  val 
├────┼─────┤
  1    1 
└────┴─────┘

Почему это? Является ли это ограничение частью стандарта SQL (то есть присутствует в других базах данных) или что-то специфическое для PostgreSQL, которое может быть исправлено в будущем? В документации к запросам WITH говорится, что несколько UPDATE не поддерживаются, но не упоминаются INSERT и UPDATE.

Джефф Тернер
источник

Ответы:

15

Все утверждения в CTE происходят практически одновременно. Т.е. они основаны на одном и том же снимке базы данных.

Команда UPDATEвидит то же состояние базовой таблицы, что и INSERT, что означает, что строка с val = 1еще не существует. Руководство разъясняет здесь:

Все операторы выполняются с одним и тем же снимком (см. Главу 13 ), поэтому они не могут «видеть» влияние друг друга на целевые таблицы.

Каждое утверждение может видеть, что возвращается другим CTE в RETURNINGпредложении. Но базовые таблицы выглядят для них одинаково.

Вам понадобятся два утверждения (в одной транзакции) для того, что вы пытаетесь сделать. Для INSERTначала приведенный пример должен быть просто единичным , но это может быть связано с упрощенным примером.

Эрвин Брандштеттер
источник
15

Это решение о реализации. Это описано в документации Postgres, WITHЗапросы (общие выражения таблиц) . Есть два параграфа, связанные с проблемой.

Во-первых, причина наблюдаемого поведения:

Подвыражения в WITHвыполняются одновременно друг с другом и с основным запросом . Следовательно, при использовании операторов, изменяющих данные WITH, порядок, в котором на самом деле происходят указанные обновления, непредсказуем. Все операторы выполняются с одним и тем же снимком (см. Главу 13), поэтому они не могут «видеть» влияние друг друга на целевые таблицы. Это смягчает последствия непредсказуемости фактического порядка обновлений строк и означает, что RETURNINGданные - это единственный способ сообщать об изменениях между различными WITHпод-утверждениями и основным запросом. Примером этого является то, что в ...

После того, как я опубликовал предложение в pgsql-docs , Марко Тииккая объяснил (что соответствует ответу Эрвина):

Случаи вставки-обновления и вставки-удаления не работают, поскольку UPDATE и DELETE не имеют возможности видеть строки INSERTed, поскольку их моментальный снимок был сделан до того, как произошла INSERT. В этих двух случаях нет ничего непредсказуемого.

Таким образом, причина, по которой ваше утверждение не обновляется, может быть объяснена в первом абзаце выше (о «снимках»). Когда вы изменяете CTE, происходит то, что все они и основной запрос выполняются и «видят» тот же моментальный снимок данных (таблиц), каким они были непосредственно перед выполнением оператора. CTE могут передавать информацию о том, что они вставили / обновили / удалили друг другу и в основной запрос, используя RETURNINGпредложение, но они не могут видеть изменения в таблицах напрямую. Итак, давайте посмотрим, что происходит в вашем утверждении:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

У нас есть 2 части, CTE ( newval):

-- newval
     INSERT INTO tbl(val) VALUES (1) RETURNING id

и основной запрос:

-- main 
UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id

Поток выполнения выглядит примерно так:

           initial data: tbl
                id  val 
                 (empty)
               /         \
              /           \
             /             \
    newval:                 \
       tbl (after newval)    \
           id  val           \
            1    1           |
                              |
    newval: returns           |
           id                 |
            1                 |
               \              |
                \             |
                 \            |
                    main query

В результате, когда основной запрос объединяет tbl(как видно на снимке) с newvalтаблицей, он соединяет пустую таблицу с таблицей из 1 строки. Очевидно, он обновляет 0 строк. Таким образом, заявление так и не пришло, чтобы изменить вновь вставленную строку, и это то, что вы видите.

Решение в вашем случае, это либо переписать оператор, чтобы вставить правильные значения в первую очередь, либо использовать 2 оператора. Один, который вставляет и второй, чтобы обновить.


Существуют и другие, похожие ситуации, например, если в операторе есть a INSERTи затем a DELETEв тех же строках. Удаление не удастся по тем же причинам.

Некоторые другие случаи с update-update и update-delete и их поведением объясняются в следующем абзаце на той же странице документов.

Попытка обновить одну и ту же строку дважды в одном операторе не поддерживается. Происходит только одна из модификаций, но не легко (а иногда и невозможно) надежно предсказать, какая именно. Это также относится к удалению строки, которая уже была обновлена ​​в том же операторе: выполняется только обновление. Поэтому, как правило, не следует пытаться модифицировать одну строку дважды в одном выражении. В частности, избегайте написания вложенных операторов WITH, которые могут повлиять на те же строки, измененные основным оператором или дочерним вложенным оператором. Последствия такого заявления не будут предсказуемыми.

И в ответе Марко Тииккая:

Случаи update-update и update-delete явно не вызваны одной и той же базовой деталью реализации (как случаи insert-update и insert-delete).
Случай обновления-обновления не работает, потому что он внутренне выглядит как проблема Хэллоуина, и Postgres не может знать, какие кортежи можно было бы обновить дважды, а какие могли бы снова вызвать проблему Хэллоуина.

Таким образом, причина одна и та же (как реализованы модифицирующие CTE и как каждый CTE видит один и тот же снимок), но детали отличаются в этих двух случаях, поскольку они более сложны, и результаты могут быть непредсказуемыми в случае обновления-обновления.

В случае вставки-обновления (как в вашем случае) и аналогичной вставки-удаления результаты предсказуемы. Происходит только вставка, так как вторая операция (обновление или удаление) не может видеть и влиять на вновь вставленные строки.


Тем не менее, предлагаемое решение одинаково для всех случаев, которые пытаются изменить одни и те же строки более одного раза: не делайте этого. Либо напишите операторы, которые изменяют каждую строку один раз, либо используйте отдельные (2 или более) операторов.

ypercubeᵀᴹ
источник