Семантика INSERT SELECT для обновления по конфликту ничего не возвращает - PullRequest
0 голосов
/ 03 ноября 2018

Мы столкнулись с очень специфической проблемой нашей производственной системы. К сожалению, несмотря на большие усилия, я не смог воспроизвести проблему локально, поэтому я не могу предоставить минимальный, полный и поддающийся проверке пример. Кроме того, поскольку это производственный код, мне пришлось изменить имена таблиц в следующем примере. Однако я полагаю, что представляю все соответствующие факты.

У нас есть четыре таблицы bucket_holder, bucket, item и bucket_total, созданные следующим образом:

CREATE TABLE bucket_holder (
  id SERIAL PRIMARY KEY,
  bucket_holder_uid UUID NOT NULL
);

CREATE TABLE bucket ( 
  id SERIAL PRIMARY KEY, 
  bucket_uid UUID NOT NULL, 
  bucket_holder_id INTEGER NOT NULL REFERENCES bucket_holder (id), 
  default_bucket BOOLEAN NOT NULL
);

CREATE TABLE item ( 
  id SERIAL PRIMARY KEY, 
  item_uid UUID NOT NULL, 
  bucket_id INTEGER NOT NULL REFERENCES bucket (id), 
  amount NUMERIC NOT NULL 
);

CREATE TABLE bucket_total ( 
  bucket_id INTEGER NOT NULL REFERENCES bucket (id), 
  amount NUMERIC NOT NULL 
);

В соответствующих столбцах также есть индексы:

CREATE UNIQUE INDEX idx1 ON bucket_holder (bucket_holder_uid);
CREATE UNIQUE INDEX idx2 ON bucket (bucket_uid);
CREATE UNIQUE INDEX idx3 ON item (item_uid);
CREATE UNIQUE INDEX idx4 ON bucket_total (bucket_id);

Идея состоит в том, что bucket_holder содержит bucket с, один из которых default_bucket, bucket с удерживает item с, и каждый bucket имеет уникальную запись bucket_total, содержащую сумму сумм всех item с.

Мы пытаемся выполнить массовые вставки в таблицу item следующим образом:

WITH
unnested AS ( 
  SELECT * 
  FROM UNNEST(
    ARRAY['00000000-0000-0000-0000-00000000001a', '00000000-0000-0000-0000-00000000002a']::UUID[], 
    ARRAY['00000000-0000-0000-0000-00000000001c', '00000000-0000-0000-0000-00000000002c']::UUID[], 
    ARRAY[1.11, 2.22]::NUMERIC[]
  ) 
  AS T(bucket_holder_uid, item_uid, amount) 
), 
inserted_item AS ( 
  INSERT INTO item (bucket_id, item_uid, amount) 
  SELECT bucket.id, unnested.item_uid, unnested.amount 
  FROM unnested 
  JOIN bucket_holder ON unnested.bucket_holder_uid = bucket_holder.bucket_holder_uid 
  JOIN bucket ON bucket.bucket_holder_id = bucket_holder.id 
  JOIN bucket_total ON bucket_total.bucket_id = bucket.id 
  WHERE bucket.default_bucket 
  FOR UPDATE OF bucket_total 
  ON CONFLICT DO NOTHING 
  RETURNING bucket_id, amount 
), 
total_for_bucket AS ( 
  SELECT bucket_id, SUM(amount) AS total 
  FROM inserted_item 
  GROUP BY bucket_id 
) 
UPDATE bucket_total 
SET amount = amount + total_for_bucket.total 
FROM total_for_bucket 
WHERE bucket_total.bucket_id = total_for_bucket.bucket_id

В действительности передаваемые массивы являются динамическими и имеют длину до 1000, но все 3 массива имеют одинаковую длину. Массивы всегда сортируются так, чтобы bucket_holder_uids были в порядке, чтобы гарантировать, что тупик не может произойти. Смысл ON CONFLICT DO NOTHING в том, что мы должны быть в состоянии справиться с ситуацией, когда некоторые из item уже присутствовали (конфликт на item_uid). В этом случае bucket_total, конечно, не следует обновлять.

В этом запросе предполагается, что соответствующие записи bucket_holder, bucket и bucket_total уже существуют. В противном случае запрос может завершиться ошибкой, так как на практике такая ситуация не возникает. Вот пример настройки некоторых примеров данных:

INSERT INTO bucket_holder (bucket_holder_uid) VALUES ('00000000-0000-0000-0000-00000000001a');
INSERT INTO bucket (bucket_uid, bucket_holder_id, default_bucket) VALUES ('00000000-0000-0000-0000-00000000001b', (SELECT id FROM bucket_holder WHERE bucket_holder_uid = '00000000-0000-0000-0000-00000000001a'), TRUE);
INSERT INTO bucket_total (bucket_id, amount) VALUES ((SELECT id FROM bucket WHERE bucket_uid = '00000000-0000-0000-0000-00000000001b'), 0);

INSERT INTO bucket_holder (bucket_holder_uid) VALUES ('00000000-0000-0000-0000-00000000002a');
INSERT INTO bucket (bucket_uid, bucket_holder_id, default_bucket) VALUES ('00000000-0000-0000-0000-00000000002b', (SELECT id FROM bucket_holder WHERE bucket_holder_uid = '00000000-0000-0000-0000-00000000002a'), TRUE);
INSERT INTO bucket_total (bucket_id, amount) VALUES ((SELECT id FROM bucket WHERE bucket_uid = '00000000-0000-0000-0000-00000000002b'), 0);

Этот запрос, кажется, выполнил правильные действия в течение сотен тысяч item с, но для нескольких item с * bucket_total было обновлено вдвое больше, чем item. Я не знаю, обновлялось ли оно дважды или обновлялось ли оно вдвое больше, чем item. Однако в этих случаях был вставлен только один item (вставка дважды была бы невозможна в любом случае, так как существует ограничение уникальности для item_uid). Наши журналы показывают, что для затронутых bucket s два потока выполняли запрос одновременно.

Может кто-нибудь увидеть и объяснить любую проблему с этим запросом и указать, как он может быть переписан?

Мы используем версию PG9.6.6

UPDATE

Мы говорили об этом с основным разработчиком postgres, который, очевидно, не видит здесь проблемы параллелизма. Сейчас мы исследуем очень неприятные возможности, такие как повреждение индекса или (удаленный) шанс ошибки pg.

1 Ответ

0 голосов
/ 08 ноября 2018

Некоторые мысли, ожидая больше данных

В зависимости от вашей проблемы, звучит как любой вставленный элемент CTE. возвращает дубликаты или оператор update как-то выполнен дважды. Оба звучат странно, наверное, ошибка в pg? Может быть, попытаться максимально упростить запрос

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

эта строка, похоже, ничего не делает: ПРИСОЕДИНЯЙТЕСЬ к bucket_total ON bucket_total.bucket_id = bucket.id

Вероятно, было бы достаточно просто вставить данные в таблицу позиций. Почему бы не иметь bucket_total в качестве представления (например, выбрать bucket_id, sum (amount) ... from items ...) Если для заполнения требуется время, возможно, используйте его в качестве материализованного представления или таблицы отчетов. Или если вы запускаете этот скрипт много раз в течение дня, вероятно, создайте триггер на таблице элементов для добавления / вычитания 1 в корзину при вставке / удалении

при условии, что вы можете уменьшить свой запрос до чего-то вроде этого:

WITH
unnested AS (....), 

  INSERT INTO item (bucket_id, item_uid, amount) 
  SELECT bucket_holder2.dflt_bucket_id, unnested.item_uid, unnested.amount 
  FROM unnested 
  JOIN bucket_holder2 ON unnested.bucket_holder_uid = bucket_holder2.bucket_holder_uid 
   ON CONFLICT DO NOTHING 

обновление Пытался выполнить эти запросы на 9.6, и он работал нормально. Так что я думаю, что нет никаких проблем с запросом и pg, возможно, пришло время воссоздать таблицу / базу данных. Еще одна идея для тестирования - вы можете попробовать изменить «UPDATE» на «INSERT» для обновления bucket_total, удалив текущий уникальный ключ и создав инкрементный первичный ключ. Таким образом, вы можете поймать / исправить двойные вставки (если это так)

...