为什么 Postgres 中的 table-swapping 如此冗长?
Why is table-swapping in Postgres so verbose?
我想回填一个大的(2000 万行)经常阅读但很少写入的列 table。从各种 articles and questions on SO 来看,最好的方法似乎是创建一个具有相同结构的 table,加载回填数据,然后实时交换(因为重命名非常快)。听起来不错!
但是当我实际编写脚本来执行此操作时,长得令人难以置信。来尝一尝:
BEGIN;
CREATE TABLE foo_new (LIKE foo);
-- I don't use INCLUDING ALL, because that produces Indexes/Constraints with different names
-- This is the only part of the script that is specific to my case.
-- Everything else is standard for any table swap
INSERT INTO foo_new (id, first_name, last_name, email, full_name)
(SELECT id, first_name, last_name, email, first_name || last_name) FROM foo);
CREATE SEQUENCE foo_new_id_seq
START 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
SELECT setval('foo_new_id_seq', COALESCE((SELECT MAX(id)+1 FROM foo_new), 1), false);
ALTER SEQUENCE foo_new_id_seq OWNED BY foo_new.id;
ALTER TABLE ONLY foo_new ALTER COLUMN id SET DEFAULT nextval('foo_new_id_seq'::regclass);
ALTER TABLE foo_new
ADD CONSTRAINT foo_new_pkey
PRIMARY KEY (id);
COMMIT;
-- Indexes are made concurrently, otherwise they would block reads for
-- a long time. Concurrent index creation cannot occur within a transaction.
CREATE INDEX CONCURRENTLY foo_new_on_first_name ON foo_new USING btree (first_name);
CREATE INDEX CONCURRENTLY foo_new_on_last_name ON foo_new USING btree (last_name);
CREATE INDEX CONCURRENTLY foo_new_on_email ON foo_new USING btree (email);
-- One more line for each index
BEGIN;
ALTER TABLE foo RENAME TO foo_old;
ALTER TABLE foo_new RENAME TO foo;
ALTER SEQUENCE foo_id_seq RENAME TO foo_old_id_seq;
ALTER SEQUENCE foo_new_id_seq RENAME TO foo_id_seq;
ALTER TABLE foo_old RENAME CONSTRAINT foo_pkey TO foo_old_pkey;
ALTER TABLE foo RENAME CONSTRAINT foo_new_pkey TO foo_pkey;
ALTER INDEX foo_on_first_name RENAME TO foo_old_on_first_name;
ALTER INDEX foo_on_last_name RENAME TO foo_old_on_last_name;
ALTER INDEX foo_on_email RENAME TO foo_old_on_email;
-- One more line for each index
ALTER INDEX foo_new_on_first_name RENAME TO foo_on_first_name;
ALTER INDEX foo_new_on_last_name RENAME TO foo_on_last_name;
ALTER INDEX foo_new_on_email RENAME TO foo_on_email;
-- One more line for each index
COMMIT;
-- TODO: drop old table (CASCADE)
而且这甚至不包括外键或其他限制!由于只有 INSERT INTO
位中特定于我的案例,我很惊讶没有内置的 Postgres 函数来进行这种交换。这个手术是不是没有我想象的那么普遍?我是否低估了实现这一目标的多种方式?我想要保持命名一致的愿望是非典型的吗?
这可能并不常见。大多数 table 都不够大,无法保证,而且大多数应用程序都可以容忍偶尔出现的停机时间。
更重要的是,不同的应用程序可以根据其工作负载以不同的方式偷工减料。数据库服务器不能;它需要处理(或者非常有意地 不 处理)所有可能的模糊边缘情况,这可能比您预期的要难得多。最终,为不同的用例编写量身定制的解决方案可能更有意义。
无论如何,如果您只是想将计算字段实现为 first_name || last_name
,还有更好的方法:
ALTER TABLE foo RENAME TO foo_base;
CREATE VIEW foo AS
SELECT
id,
first_name,
last_name,
email,
(first_name || last_name) AS full_name
FROM foo_base;
假设你的实际情况更复杂,所有这些努力可能仍然是不必要的。我相信复制和重命名方法主要基于这样的假设,即您需要在此过程期间锁定 table 以防止并发修改,因此目标是尽快完成它.如果所有并发操作都是只读的——这似乎是这种情况,因为你没有锁定 table——那么你可能最好使用简单的 UPDATE
(不会阻塞SELECT
s),即使它确实需要更长的时间(尽管它确实具有避免外键重新检查和 TOAST table 重写的优势)。
如果这种方法真的合理,我认为有一些改进的机会:
- 您不需要 recreate/reset 序列;您可以 link 现有序列到新的 table.
CREATE INDEX CONCURRENTLY
似乎没有必要,因为还没有其他人尝试访问 foo_new
。事实上,如果整个脚本在一个事务中,此时它甚至不会在外部可见。
- Table 名称只需要在模式中是唯一的。如果您临时为新 table 创建一个模式,您应该能够用单个
ALTER TABLE foo SET SCHEMA public
. 替换所有这些 RENAME
- 即使您不期望并发写入,
LOCK foo IN SHARE MODE
也无妨...
编辑:
序列重新分配比我预期的要复杂一些,因为它们似乎需要与父级保持在相同的架构中 table。但这是(看起来是)一个有效的例子:
BEGIN;
LOCK public.foo IN SHARE MODE;
CREATE SCHEMA tmp;
CREATE TABLE tmp.foo (LIKE public.foo);
INSERT INTO tmp.foo (id, first_name, last_name, email, full_name)
SELECT id, first_name, last_name, email, (first_name || last_name) FROM public.foo;
ALTER TABLE tmp.foo ADD CONSTRAINT foo_pkey PRIMARY KEY (id);
CREATE INDEX foo_on_first_name ON tmp.foo (first_name);
CREATE INDEX foo_on_last_name ON tmp.foo (last_name);
CREATE INDEX foo_on_email ON tmp.foo (email);
ALTER TABLE tmp.foo ALTER COLUMN id SET DEFAULT nextval('public.foo_id_seq');
ALTER SEQUENCE public.foo_id_seq OWNED BY NONE;
DROP TABLE public.foo;
ALTER TABLE tmp.foo SET SCHEMA public;
ALTER SEQUENCE public.foo_id_seq OWNED BY public.foo.id;
DROP SCHEMA tmp;
COMMIT;
我想回填一个大的(2000 万行)经常阅读但很少写入的列 table。从各种 articles and questions on SO 来看,最好的方法似乎是创建一个具有相同结构的 table,加载回填数据,然后实时交换(因为重命名非常快)。听起来不错!
但是当我实际编写脚本来执行此操作时,长得令人难以置信。来尝一尝:
BEGIN;
CREATE TABLE foo_new (LIKE foo);
-- I don't use INCLUDING ALL, because that produces Indexes/Constraints with different names
-- This is the only part of the script that is specific to my case.
-- Everything else is standard for any table swap
INSERT INTO foo_new (id, first_name, last_name, email, full_name)
(SELECT id, first_name, last_name, email, first_name || last_name) FROM foo);
CREATE SEQUENCE foo_new_id_seq
START 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
SELECT setval('foo_new_id_seq', COALESCE((SELECT MAX(id)+1 FROM foo_new), 1), false);
ALTER SEQUENCE foo_new_id_seq OWNED BY foo_new.id;
ALTER TABLE ONLY foo_new ALTER COLUMN id SET DEFAULT nextval('foo_new_id_seq'::regclass);
ALTER TABLE foo_new
ADD CONSTRAINT foo_new_pkey
PRIMARY KEY (id);
COMMIT;
-- Indexes are made concurrently, otherwise they would block reads for
-- a long time. Concurrent index creation cannot occur within a transaction.
CREATE INDEX CONCURRENTLY foo_new_on_first_name ON foo_new USING btree (first_name);
CREATE INDEX CONCURRENTLY foo_new_on_last_name ON foo_new USING btree (last_name);
CREATE INDEX CONCURRENTLY foo_new_on_email ON foo_new USING btree (email);
-- One more line for each index
BEGIN;
ALTER TABLE foo RENAME TO foo_old;
ALTER TABLE foo_new RENAME TO foo;
ALTER SEQUENCE foo_id_seq RENAME TO foo_old_id_seq;
ALTER SEQUENCE foo_new_id_seq RENAME TO foo_id_seq;
ALTER TABLE foo_old RENAME CONSTRAINT foo_pkey TO foo_old_pkey;
ALTER TABLE foo RENAME CONSTRAINT foo_new_pkey TO foo_pkey;
ALTER INDEX foo_on_first_name RENAME TO foo_old_on_first_name;
ALTER INDEX foo_on_last_name RENAME TO foo_old_on_last_name;
ALTER INDEX foo_on_email RENAME TO foo_old_on_email;
-- One more line for each index
ALTER INDEX foo_new_on_first_name RENAME TO foo_on_first_name;
ALTER INDEX foo_new_on_last_name RENAME TO foo_on_last_name;
ALTER INDEX foo_new_on_email RENAME TO foo_on_email;
-- One more line for each index
COMMIT;
-- TODO: drop old table (CASCADE)
而且这甚至不包括外键或其他限制!由于只有 INSERT INTO
位中特定于我的案例,我很惊讶没有内置的 Postgres 函数来进行这种交换。这个手术是不是没有我想象的那么普遍?我是否低估了实现这一目标的多种方式?我想要保持命名一致的愿望是非典型的吗?
这可能并不常见。大多数 table 都不够大,无法保证,而且大多数应用程序都可以容忍偶尔出现的停机时间。
更重要的是,不同的应用程序可以根据其工作负载以不同的方式偷工减料。数据库服务器不能;它需要处理(或者非常有意地 不 处理)所有可能的模糊边缘情况,这可能比您预期的要难得多。最终,为不同的用例编写量身定制的解决方案可能更有意义。
无论如何,如果您只是想将计算字段实现为 first_name || last_name
,还有更好的方法:
ALTER TABLE foo RENAME TO foo_base;
CREATE VIEW foo AS
SELECT
id,
first_name,
last_name,
email,
(first_name || last_name) AS full_name
FROM foo_base;
假设你的实际情况更复杂,所有这些努力可能仍然是不必要的。我相信复制和重命名方法主要基于这样的假设,即您需要在此过程期间锁定 table 以防止并发修改,因此目标是尽快完成它.如果所有并发操作都是只读的——这似乎是这种情况,因为你没有锁定 table——那么你可能最好使用简单的 UPDATE
(不会阻塞SELECT
s),即使它确实需要更长的时间(尽管它确实具有避免外键重新检查和 TOAST table 重写的优势)。
如果这种方法真的合理,我认为有一些改进的机会:
- 您不需要 recreate/reset 序列;您可以 link 现有序列到新的 table.
CREATE INDEX CONCURRENTLY
似乎没有必要,因为还没有其他人尝试访问foo_new
。事实上,如果整个脚本在一个事务中,此时它甚至不会在外部可见。- Table 名称只需要在模式中是唯一的。如果您临时为新 table 创建一个模式,您应该能够用单个
ALTER TABLE foo SET SCHEMA public
. 替换所有这些 - 即使您不期望并发写入,
LOCK foo IN SHARE MODE
也无妨...
RENAME
编辑:
序列重新分配比我预期的要复杂一些,因为它们似乎需要与父级保持在相同的架构中 table。但这是(看起来是)一个有效的例子:
BEGIN;
LOCK public.foo IN SHARE MODE;
CREATE SCHEMA tmp;
CREATE TABLE tmp.foo (LIKE public.foo);
INSERT INTO tmp.foo (id, first_name, last_name, email, full_name)
SELECT id, first_name, last_name, email, (first_name || last_name) FROM public.foo;
ALTER TABLE tmp.foo ADD CONSTRAINT foo_pkey PRIMARY KEY (id);
CREATE INDEX foo_on_first_name ON tmp.foo (first_name);
CREATE INDEX foo_on_last_name ON tmp.foo (last_name);
CREATE INDEX foo_on_email ON tmp.foo (email);
ALTER TABLE tmp.foo ALTER COLUMN id SET DEFAULT nextval('public.foo_id_seq');
ALTER SEQUENCE public.foo_id_seq OWNED BY NONE;
DROP TABLE public.foo;
ALTER TABLE tmp.foo SET SCHEMA public;
ALTER SEQUENCE public.foo_id_seq OWNED BY public.foo.id;
DROP SCHEMA tmp;
COMMIT;