Використовуйте декілька конфліктних цілей у реченні ON CONFLICT


93

У мене є два стовпці в таблиці col1, col2обидва вони унікально проіндексовані (col1 унікальний і так само col2).

Мені потрібно вставити в цю таблицю, використовувати ON CONFLICTсинтаксис та оновити інші стовпці, але я не можу використовувати обидва стовпці у conflict_targetреченні.

Це працює:

INSERT INTO table
...
ON CONFLICT ( col1 ) 
DO UPDATE 
SET 
-- update needed columns here

Але як це зробити для кількох стовпців, приблизно так:

...
ON CONFLICT ( col1, col2 )
DO UPDATE 
SET 
....

4
"col1, col2, вони обидва індексуються унікально." це означає, що col1 унікальний і col2 унікальний, або поєднання col1, col2 унікальні?
e4c5,

1
чи означає це, що col1 унікальний, а col2 унікальний, окремо
Ото Шавадзе

Відповіді:


48

Зразок таблиці та даних

CREATE TABLE dupes(col1 int primary key, col2 int, col3 text,
   CONSTRAINT col2_unique UNIQUE (col2)
);

INSERT INTO dupes values(1,1,'a'),(2,2,'b');

Відтворення проблеми

INSERT INTO dupes values(3,2,'c')
ON CONFLICT (col1) DO UPDATE SET col3 = 'c', col2 = 2

Назвемо це Q1. Результат є

ERROR:  duplicate key value violates unique constraint "col2_unique"
DETAIL:  Key (col2)=(2) already exists.

Що говорить документація

конфликт_цель може виконувати унікальний умовивід індексу. При виконанні умовиводу він складається з одного або декількох стовпців_імена_індексу та / або виразів_вираження індексу та необов'язкового предиката_індексу. Усі унікальні індекси табличних імен, які, незалежно від порядку, містять точно вказані стовпці / вирази, задані конфліктом, визначаються (вибираються) як індекси арбітра. Якщо вказано предикат index_, він, як додаткова вимога для умовиводу, повинен задовольняти індекси арбітра.

Це створює враження, що наступний запит повинен працювати, але це не так, оскільки він насправді потребує спільного унікального індексу на col1 та col2. Однак такий індекс не гарантує, що col1 та col2 будуть унікальними в індивідуальному порядку, що є однією з вимог OP.

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT (col1,col2) DO UPDATE SET col3 = 'c', col2 = 2

Давайте назвемо цей запит Q2 (це не вдається із синтаксичною помилкою)

Чому?

Postgresql поводиться так, тому що те, що має статися, коли конфлікт виникає у другій колонці, не є чітко визначеним. Існує безліч можливостей. Наприклад, у наведеному вище запитанні Q1, чи слід postgresql оновлюватись, col1коли виникає конфлікт col2? Але що, якщо це призведе до чергового конфлікту col1? як очікується, що postgresql впорається з цим?

Рішення

Рішенням є поєднання ON CONFLICT зі старомодним UPSERT .

CREATE OR REPLACE FUNCTION merge_db(key1 INT, key2 INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        UPDATE dupes SET col3 = data WHERE col1 = key1 and col2 = key2;
        IF found THEN
            RETURN;
        END IF;

        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently, or key2
        -- already exists in col2,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO dupes VALUES (key1, key2, data) ON CONFLICT (col1) DO UPDATE SET col3 = data;
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            BEGIN
                INSERT INTO dupes VALUES (key1, key2, data) ON CONFLICT (col2) DO UPDATE SET col3 = data;
                RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- Do nothing, and loop to try the UPDATE again.
            END;
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

Вам потрібно було б змінити логіку цієї збереженої функції, щоб вона оновлювала стовпці саме так, як ви хочете. Закликайте це як

SELECT merge_db(3,2,'c');
SELECT merge_db(1,2,'d');

3
Це працює, але трохи більше роботи / логіки, ніж потрібно, все, що вам потрібно зробити - це створити унікальне обмеження для двох стовпців. Дивіться мою відповідь нижче.
Джубайр

чи можу я використовувати рішення merge_db також, якщо я вставляю кілька наборів ЗНАЧЕНЬ одночасно?
daniyel

@daniyel вам доведеться переписати збережену функцію
e4c5

3
Мені незрозуміло, як корисно пропонувати використовувати старомодний upsert - це запитання добре посилається на "postgres upsert 9.5", і це може бути краще, пояснивши, як використовувати його з усіма параметрами constraint_names.
Пак

3
@Pak Вам незрозуміло, тому що ви не чітко прочитали питання. Оператор не шукає складеного ключа в цих полях. Інша відповідь працює для складених клавіш
e4c5,

65

ON CONFLICTдля виявлення конфлікту потрібен унікальний індекс *. Тож вам просто потрібно створити унікальний індекс для обох стовпців:

t=# create table t (id integer, a text, b text);
CREATE TABLE
t=# create unique index idx_t_id_a on t (id, a);
CREATE INDEX
t=# insert into t values (1, 'a', 'foo');
INSERT 0 1
t=# insert into t values (1, 'a', 'bar') on conflict (id, a) do update set b = 'bar';
INSERT 0 1
t=# select * from t;
 id | a |  b  
----+---+-----
  1 | a | bar

* На додаток до унікальних індексів, ви також можете використовувати обмеження виключення . Це трохи загальніше, ніж унікальні обмеження. Припустимо, у вашій таблиці були стовпці для idі valid_timevalid_timeє a tsrange), і ви хотіли дозволити дублікати ids, але не для періодів, що перекриваються. Унікальне обмеження вам не допоможе, але з обмеженням виключення ви можете сказати "виключити нові записи, якщо вони idдорівнюють старому, idа також valid_timeперекривають його valid_time".


4
Це створює спільний унікальний індекс, створює унікальний індекс idx_t_id_a на t (id, a); Звичайно, в ОП чітко не вказано, унікальні ці дві колонки окремо чи разом.
e4c5,

Чому postgres іноді каже, що немає стовпця, названого за індексом, і не використовується ON CONFLICT?
Пак

@Pak здається, що вам слід написати власне запитання за допомогою конкретної команди, яку ви використовуєте, і повідомлення про помилку, яке ви отримаєте.
Paul A Jungwirth,

@PaulAJungwirth Я не знаю, ваша відповідь на місці - унікальний індекс як обмеження для on conflictкоманди. Помилка просто "стовпець my_index_name не існує".
Пак

Я все-таки спробував це з окремим унікальним обмеженням для кожного стовпця, як просив OP, і це не спрацювало. Не те щоб я сподівався, але я сподівався.
sudo

5

У наш час це (здається) неможливо. Ні остання версія ON CONFLICT синтаксису не дозволяє повторити пункт, ні CTE : неможливо зламати INSERT з ON CONFLICT, щоб додати більше конфліктних цілей.


3

Якщо ви використовуєте postgres 9.5, ви можете використовувати ВИКЛЮЧЕНИЙ пробіл.

Приклад взято з Що нового в PostgreSQL 9.5 :

INSERT INTO user_logins (username, logins)
VALUES ('Naomi',1),('James',1)
ON CONFLICT (username)
DO UPDATE SET logins = user_logins.logins + EXCLUDED.logins;

2
  1. Створіть обмеження (наприклад, зовнішній індекс).

АБО / І

  1. Подивіться на існуючі обмеження (\ d у psq).
  2. Використовуйте ON CONSTRAINT (ім'я обмеження) у реченні INSERT.

1

Влад зрозумів правильну ідею.

Спочатку вам потрібно створити унікальне обмеження таблиці для стовпців. col1, col2 Потім, як тільки ви це зробите, ви зможете зробити наступне:

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT ON CONSTRAINT dupes_pkey 
DO UPDATE SET col3 = 'c', col2 = 2

4
Вибачте, але ви неправильно зрозуміли питання. OP не хоче спільного унікального обмеження.
e4c5,

1

Напевно хакі, але я вирішив це, об’єднавши два значення з col1 і col2 у новий стовпець, col3 (щось на зразок індексу двох) і порівняв з цим. Це працює лише в тому випадку, якщо він вам потрібен, щоб він відповідав І ДВОМУ col1 і col2.

INSERT INTO table
...
ON CONFLICT ( col3 ) 
DO UPDATE 
SET 
-- update needed columns here

Де col3 = об'єднання значень з col1 та col2.


3
Ви можете створити унікальний індекс для цих двох стовпців і вказати це обмеження в on conflict.
Kishore

0

Типово (я думаю) ви можете створити заяву лише з одним, on conflictякий вказує єдине обмеження, яке є важливим для того, що ви вставляєте.

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

Приклад:
(ліцензія: не CC0, лише CC-By)

// there're these unique constraints:
//   unique (site_id, people_id, page_id)
//   unique (site_id, people_id, pages_in_whole_site)
//   unique (site_id, people_id, pages_in_category_id)
// and only *one* of page-id, category-id, whole-site-true/false
// can be specified. So only one constraint is "active", at a time.

val thingColumnName = thingColumnName(notfificationPreference)

val insertStatement = s"""
  insert into page_notf_prefs (
    site_id,
    people_id,
    notf_level,
    page_id,
    pages_in_whole_site,
    pages_in_category_id)
  values (?, ?, ?, ?, ?, ?)
  -- There can be only one on-conflict clause.
  on conflict (site_id, people_id, $thingColumnName)   <—— look
  do update set
    notf_level = excluded.notf_level
  """

val values = List(
  siteId.asAnyRef,
  notfPref.peopleId.asAnyRef,
  notfPref.notfLevel.toInt.asAnyRef,
  // Only one of these is non-null:
  notfPref.pageId.orNullVarchar,
  if (notfPref.wholeSite) true.asAnyRef else NullBoolean,
  notfPref.pagesInCategoryId.orNullInt)

runUpdateSingleRow(insertStatement, values)

І:

private def thingColumnName(notfPref: PageNotfPref): String =
  if (notfPref.pageId.isDefined)
    "page_id"
  else if (notfPref.pagesInCategoryId.isDefined)
    "pages_in_category_id"
  else if (notfPref.wholeSite)
    "pages_in_whole_site"
  else
    die("TyE2ABK057")

on conflictЗастереження динамічно, в залежності від того, що я намагаюся зробити. Якщо я вставляю налаштування сповіщення для сторінки - тоді може існувати унікальний конфлікт щодо site_id, people_id, page_idобмеження. І якщо я налаштовую префікси сповіщень, для категорії - то натомість я знаю, що обмеження, яке може бути порушене, є site_id, people_id, category_id.

Тож я можу, і, ймовірно, ти теж, у вашому випадку ?, створити правильне on conflict (... columns ), тому що я знаю, що хочу зробити, а потім знаю, яке саме з багатьох унікальних обмежень може бути порушене.


-4

ON CONFLICT - дуже незграбне рішення, біжи

UPDATE dupes SET key1=$1, key2=$2 where key3=$3    
if rowcount > 0    
  INSERT dupes (key1, key2, key3) values ($1,$2,$3);

працює над Oracle, Postgres та всіма іншими базами даних


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