我有一个现有的表items
:
create table items (
id serial primary key,
name text
-- ... and other columns that aren't of relevance.
);
我想创建另一个名为item_documents
的表:
create table item_documents (
id serial primary key,
item_id integer unique foreign key items(id) on delete cascade,
document tsvector
-- ... and other computed columns.
);
item_documents
表是从items
表计算的。每次插入,更新或删除items
表时,都应重新计算相应的item_documents
记录。为了实现这一点,我将最终在items
表上创建一个触发器来重新计算插入/更新上的item_documents
(这是我完成此迁移后的目标)。
我面临的主要问题是我想回填item_documents
表。 items
表非常大。我想过只做insert/select
:
insert into item_documents (item_id, document, ...)
select id, compute_document(id, name, ...), ... from items
这有一个明显的问题:如果并发事务插入/更新items
表,则item_documents
中不会有相应的行。
我的下一个解决方案是在执行insert/select
之前添加触发器。这导致了另一个问题:如果并发事务在item_documents
运行时通过触发器插入/更新insert/select
,则该行由于唯一约束而被锁定(这也可能导致死锁)。同样,因为insert/select
锁定item_documents
表中的行,它将阻止任何并发事务运行其触发器。这是特别痛苦的,因为insert/select
到item_documents
至少需要一分钟才能运行(并且有很多并发事务)。
我的下一个解决方案是首先添加触发器,但是在较小的批次中执行insert/select
并随着时间的推移将它们展开。我可以负担额外的时间,因为在回填完成之前,不使用使用item_documents
表的功能。我的想法是item_documents
上的锁只保留到批处理完成之前。
这是确保表与减少锁定同步的正确解决方案吗?
答案 0 :(得分:1)
是的,为避免长时间的交易,您需要进行某种批处理。
我会使用此查询作为更新的基础:
SELECT id
FROM items
LEFT JOIN item_documents d ON d.item_id = items.id
WHERE d.item_id IS NULL
LIMIT 10
然后,对于此队列中的每个项目,运行compute_document
函数并填充item_documents
。
实际上,这在单个PostgreSQL语句中是可行的:
-- repeat this until done:
INSERT INTO item_documents (item_id, document)
SELECT items.id, compute_document(...)
FROM items
LEFT JOIN item_documents AS d ON d.item_id = items.id
WHERE d.item_id IS NULL -- Process all items without documents,
LIMIT 10 -- but only 10 at a time, to avoid locking;
请记住在两个表中的相关列(item_id
和id
)上创建基本索引。
作为替代方案,您可以使用布尔标志来指示处理数据。
ALTER TABLE items ADD is_processed boolean; --nulls!
CREATE INDEX items_todo ON items (id) WHERE is_processed IS DISTINCT FROM true;
-- repeat this until done:
WITH workitem AS (
INSERT INTO item_documents (item_id, document)
SELECT items.id, compute_document(...)
FROM items
WHERE is_processed IS DISTINCT FROM true
LIMIT 10
RETURNING item_id
)
UPDATE items
SET is_processed = true
FROM workitems
WHERE workitems.item_id = items.id;