Чому рядки, вставлені в 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;

Результатом є те, що UPDATE ігнорується:

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 , Марко Тііккая пояснив (що згоден з відповіддю Ервіна):

Випадки вставки-оновлення та вставлення-видалення не працюють, тому що ОНОВЛЕННЯ та ВИДАЛЕННЯ не мають змоги побачити ВСТАНОВЛЕНІ рядки через їх знімок, зроблений до того, як сталося ВСТАВЛЕННЯ. У цих двох випадках немає нічого непередбачуваного.

Тож причину, через яку ваше твердження не оновлюється, можна пояснити першим пунктом вище (про "знімки"). Що відбувається, коли ви змінюєте 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 твердження. Один, який вставляє, а другий для оновлення.


Існують і інші подібні ситуації, наприклад, якщо у висловлюванні було вказано а, INSERTа потім а DELETEна одних і тих же рядках. Видалення не вдалося б із абсолютно тих самих причин.

Деякі інші випадки, пов’язані з оновленням-оновленням та оновленням-видаленням, та їх поведінка пояснюються у наступному параграфі на тій же сторінці документів.

Спроба оновити один і той же рядок двічі в одному операторі не підтримується. Відбувається лише одна з модифікацій, але надійно передбачити, яку з них неможливо (а іноді й неможливо). Це стосується також видалення рядка, який уже був оновлений у тому самому операторі: виконується лише оновлення. Тому слід уникати спроб змінити один рядок двічі в одному операторі. Зокрема, уникайте написання підрепортажів СИ, які можуть впливати на ті самі рядки, змінені в основному операторі або підзаговорниках побратимів. Наслідки такого твердження не будуть передбачуваними.

А у відповідь Марко Тііккая:

Випадки оновлення-оновлення та оновлення-видалення явно не викликаються тими ж основними деталями реалізації (як випадки вставлення-оновлення та вставки-видалення).
Випадок оновлення-оновлення не працює, оскільки він всередині виглядає як проблема Хеллоуїна, і Postgres не може знати, які кортежі було б добре оновлювати двічі, а які могли б знову ввести проблему Хеллоуїна.

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

У вставці-оновленні (як у вашому випадку) та подібному вставці-видаленні результати передбачувані. Лише вставка відбувається, оскільки друга операція (оновлення або видалення) не має можливості бачити та впливати на щойно вставлені рядки.


Пропоноване рішення, однак, однакове для всіх випадків, які намагаються змінити одні й ті ж рядки не один раз: Не робіть цього. Або пишіть заяви, які змінюють кожен рядок один раз, або використовуйте окремі (2 або більше) операторів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.