Мы столкнулись с очень специфической проблемой нашей производственной системы. К сожалению, несмотря на большие усилия, я не смог воспроизвести проблему локально, поэтому я не могу предоставить минимальный, полный и поддающийся проверке пример. Кроме того, поскольку это производственный код, мне пришлось изменить имена таблиц в следующем примере. Однако я полагаю, что я представляю все соответствующие факты.
У нас есть четыре таблицы 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 s.
Мы пытаемся сделать массовые вставки в таблицу 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 были в порядке, чтобы гарантировать, что тупик не может возникнуть. item_uid 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 два потока выполняли запрос одновременно.
Может кто-нибудь увидеть и объяснить любую проблему с этим запросом и указать, как он может быть переписан?
Мы используем версию PG9.6.6
ОБНОВИТЬ
Мы говорили об этом с основным разработчиком Postgres, который, очевидно, не видит здесь проблемы с параллелизмом. Сейчас мы исследуем действительно неприятные возможности, такие как повреждение индекса или (удаленный) шанс ошибки pg.