Использовать несколько аргументов конфликт_целевых в предложении ON CONFLICT

95

У меня есть два столбца в таблице 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.

Что говорится в документации

конфликт_target может выполнять уникальный вывод индекса. При выполнении вывода он состоит из одного или нескольких столбцов index_column_name и / или выражений index_expression и необязательного index_predicate. Все уникальные индексы table_name, которые, безотносительно порядка, содержат именно столбцы / выражения, указанные в параметре Conflict_target, выводятся (выбираются) как индексы-арбитры. Если указан index_predicate, он должен, в качестве дополнительного требования для вывода, удовлетворять индексам арбитра.

Создается впечатление, что следующий запрос должен работать, но это не так, потому что на самом деле он потребовал бы вместе уникального индекса для 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');
e4c5
источник
3
Этот способ работает, но немного больше работы / логики, чем необходимо, все, что вам действительно нужно сделать, это создать уникальное ограничение для двух столбцов. Смотрите мой ответ ниже.
Jubair
Могу ли я использовать решение merge_db также, если я вставляю сразу несколько наборов ЗНАЧЕНИЙ?
daniyel 06
@daniyel, тебе придется переписать сохраненную функцию
e4c5 06
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); Конечно, OP четко не указывает, являются ли два столбца уникальными по отдельности или вместе.
e4c5
Почему postgres иногда сообщает, что нет столбца с именем индекса, и не может его использовать ON CONFLICT?
Пак
@Pak похоже, что вы должны написать свой собственный вопрос, указав конкретную команду, которую вы используете, и полученное сообщение об ошибке.
Пол А Юнгвирт,
@PaulAJungwirth Я не знаю, ваш ответ точен - уникальный индекс как ограничение для on conflictкоманды. Ошибка просто «столбец my_index_name не существует».
Пак
Я все равно пробовал это с отдельным уникальным ограничением для каждого столбца, как просил OP, и это не сработало. Не то чтобы я этого ожидал, но я надеялся.
sudo
6

В наши дни это (кажется) невозможно. Ни последняя версия 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
Джубайр
источник
5
Извините, но вы неправильно поняли вопрос. 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.
Кишор Реланги
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 и другими базами данных

user2625834
источник
Он не атомарный, поэтому может дать сбой и дать неверные результаты в случае одновременного подключения нескольких соединений.
Богдан Март