加载一个大的table,被其他人引用,高效
Loading a big table, referenced by others, efficiently
我的用例如下:
我有大量 table users
(约 2 亿行)用户使用 user_id
作为主键。 users
被其他几个使用外键 ON DELETE CASCADE
的 table 引用。
每天我都必须使用大量的 csv 文件来替换 users
的全部内容。 (请不要问我为什么要那样做,我只是不得不...)
我的想法是将主键和所有外键设置为 DEFERRED,然后在同一个事务中,删除整个 table 并使用 COPY 命令复制所有 csvs。预期的结果是所有检查和索引计算都将在交易结束时进行。
但实际上插入过程非常慢(4 小时,如果我插入并放置主键则需要 10 分钟)并且没有外键可以引用可延迟的主键。
由于外键,我无法在插入过程中删除主键。我也不想摆脱外键,因为我必须手动模拟 ON DELETE CASCADE
的行为。
所以基本上我正在寻找一种方法告诉 postgres 在事务结束之前不要关心主键索引或外键检查。
PS1:我编造了用户 table,我实际上正在处理非常不同类型的数据,但它与问题并不真正相关。
PS2:粗略估计,我每天在 200 多万条记录中删除 10 条记录,更新 100 万条,添加 100 万条。
全删+全插入会造成级联FK泛滥,
这将不得不由 DEFERRED 推迟,
这将在提交时对 DBMS 造成大量后果。
相反,不要{delete+create} 键,而是将它们保留在原处。
另外,不要碰不需要碰的记录。
-- staging table
CREATE TABLE tmp_users AS SELECT * FROM big_users WHERE 1=0;
COPY TABLE tmp_users (...) FROM '...' WITH CSV;
-- ... and more copying ...
-- ... from more files ...
-- If this fails, you have a problem!
ALTER TABLE tmp_users
ADD PRIMARY KEY (id);
-- [EDIT]
-- I added this later, because the user_comments table
-- was not present in the original question.
DELETE FROM user_comments c
WHERE NOT EXISTS (
SELECT * FROM tmp_users u WHERE u.id = c.user_id
);
-- These deletes are allowed to cascade
-- [we assume that the mport of the CSV files was complete, here ...]
DELETE FROM big_users b
WHERE NOT EXISTS (
SELECT *
FROM tmp_users t
WHERE t.id = b.id
);
-- Only update the records that actually **change**
-- [ updates are expensive in terms of I/O, because they create row-versions
-- , and the need to delete the old row-versions, afterwards ]
-- Note that the key (id) does not change, so there will be no cascading.
-- ------------------------------------------------------------
UPDATE big_users b
SET name_1 = t.name_1
, name_2 = t.name_2
, address = t.address
-- , ... ALL THE COLUMNS here, except the key(s)
FROM tmp_users t
WHERE t.id = b.id
AND (t.name_1, t.name_2, t.address, ...) -- ALL THE COLUMNS, except the key(s)
IS DISTINCT FROM
(b.name_1, b.name_2, b.address, ...)
;
-- Maybe there were some new records in the CSV files. Add them.
INSERT INTO big_users (id,name_1,name_2,address, ...)
SELECT id,name_1,name_2,address, ...
FROM tmp_users t
WHERE NOT EXISTS (
SELECT *
FROM big_users x
WHERE x.id = t.id
);
我找到了一个 hacky 解决方案:
update pg_index set indisvalid = false, indisready=false where indexrelid= 'users_pkey'::regclass;
DELETE FROM users;
COPY TABLE users FROM 'file.csv';
REINDEX INDEX users_pkey;
DELETE FROM user_comments c WHERE NOT EXISTS (SELECT * FROM users u WHERE u.id = c.user_id )
commit;
神奇的脏 hack 是禁用 postgres 目录中的主键索引,并在最后强制重建索引(这将覆盖我们所做的更改)。我不能将外键与 ON DELETE CASCADE
一起使用,因为出于某种原因,它会立即执行约束...所以我的外键是 ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED
,我必须自己删除。
这对我来说效果很好,因为只有少数用户在其他表中被引用。
不过我希望有一个更简洁的解决方案...
我的用例如下:
我有大量 table users
(约 2 亿行)用户使用 user_id
作为主键。 users
被其他几个使用外键 ON DELETE CASCADE
的 table 引用。
每天我都必须使用大量的 csv 文件来替换 users
的全部内容。 (请不要问我为什么要那样做,我只是不得不...)
我的想法是将主键和所有外键设置为 DEFERRED,然后在同一个事务中,删除整个 table 并使用 COPY 命令复制所有 csvs。预期的结果是所有检查和索引计算都将在交易结束时进行。
但实际上插入过程非常慢(4 小时,如果我插入并放置主键则需要 10 分钟)并且没有外键可以引用可延迟的主键。
由于外键,我无法在插入过程中删除主键。我也不想摆脱外键,因为我必须手动模拟 ON DELETE CASCADE
的行为。
所以基本上我正在寻找一种方法告诉 postgres 在事务结束之前不要关心主键索引或外键检查。
PS1:我编造了用户 table,我实际上正在处理非常不同类型的数据,但它与问题并不真正相关。
PS2:粗略估计,我每天在 200 多万条记录中删除 10 条记录,更新 100 万条,添加 100 万条。
全删+全插入会造成级联FK泛滥, 这将不得不由 DEFERRED 推迟, 这将在提交时对 DBMS 造成大量后果。
相反,不要{delete+create} 键,而是将它们保留在原处。 另外,不要碰不需要碰的记录。
-- staging table
CREATE TABLE tmp_users AS SELECT * FROM big_users WHERE 1=0;
COPY TABLE tmp_users (...) FROM '...' WITH CSV;
-- ... and more copying ...
-- ... from more files ...
-- If this fails, you have a problem!
ALTER TABLE tmp_users
ADD PRIMARY KEY (id);
-- [EDIT]
-- I added this later, because the user_comments table
-- was not present in the original question.
DELETE FROM user_comments c
WHERE NOT EXISTS (
SELECT * FROM tmp_users u WHERE u.id = c.user_id
);
-- These deletes are allowed to cascade
-- [we assume that the mport of the CSV files was complete, here ...]
DELETE FROM big_users b
WHERE NOT EXISTS (
SELECT *
FROM tmp_users t
WHERE t.id = b.id
);
-- Only update the records that actually **change**
-- [ updates are expensive in terms of I/O, because they create row-versions
-- , and the need to delete the old row-versions, afterwards ]
-- Note that the key (id) does not change, so there will be no cascading.
-- ------------------------------------------------------------
UPDATE big_users b
SET name_1 = t.name_1
, name_2 = t.name_2
, address = t.address
-- , ... ALL THE COLUMNS here, except the key(s)
FROM tmp_users t
WHERE t.id = b.id
AND (t.name_1, t.name_2, t.address, ...) -- ALL THE COLUMNS, except the key(s)
IS DISTINCT FROM
(b.name_1, b.name_2, b.address, ...)
;
-- Maybe there were some new records in the CSV files. Add them.
INSERT INTO big_users (id,name_1,name_2,address, ...)
SELECT id,name_1,name_2,address, ...
FROM tmp_users t
WHERE NOT EXISTS (
SELECT *
FROM big_users x
WHERE x.id = t.id
);
我找到了一个 hacky 解决方案:
update pg_index set indisvalid = false, indisready=false where indexrelid= 'users_pkey'::regclass;
DELETE FROM users;
COPY TABLE users FROM 'file.csv';
REINDEX INDEX users_pkey;
DELETE FROM user_comments c WHERE NOT EXISTS (SELECT * FROM users u WHERE u.id = c.user_id )
commit;
神奇的脏 hack 是禁用 postgres 目录中的主键索引,并在最后强制重建索引(这将覆盖我们所做的更改)。我不能将外键与 ON DELETE CASCADE
一起使用,因为出于某种原因,它会立即执行约束...所以我的外键是 ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED
,我必须自己删除。
这对我来说效果很好,因为只有少数用户在其他表中被引用。
不过我希望有一个更简洁的解决方案...