为什么 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(不会阻塞SELECTs),即使它确实需要更长的时间(尽管它确实具有避免外键重新检查和 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;