为什么Postgres中的表交换如此冗长?

时间:2016-11-23 20:01:07

标签: postgresql etl swap

我想回填一个大的(20M行)列,经常阅读但很少写的表。从各种articlesquestions on SO来看,最好的方法是创建一个具有相同结构的表,加载回填数据和实时交换(因为重命名非常快)。听起来不错!

但是当我实际编写脚本来执行此操作时,令人费解的是。这是一种品味:

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函数来进行这种交换。这个操作不像我做的那么常见吗?我是否低估了实现这一目标的各种方式?我是否希望将命名与非典型命名保持一致?

1 个答案:

答案 0 :(得分:2)

它可能不是那么常见。大多数表都不足以保证它,并且大多数应用程序可以容忍一些停机时间。

更重要的是,不同的应用程序可以根据工作量以不同的方式偷工减料。数据库服务器不能;它需要处理(或非常故意处理)每个可能模糊的边缘情况,这可能比你预期的要困难得多。最终,为不同的用例编写量身定制的解决方案可能更有意义。

无论如何,如果您只是尝试将计算字段实现为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;

假设您的真实情况更复杂,所有这些努力可能仍然是不必要的。我相信复制和重命名方法主要基于这样的假设:您需要在此过程的持续时间内锁定表以防止并发修改,因此目标是尽快完成。如果所有并发操作都是只读的 - 这似乎是这种情况,因为您没有锁定表格 - 那么您可能最好使用一个简单的UPDATE(赢得' t阻止SELECT s),即使它确实需要更长的时间(尽管它确实具有避免外键重新检查和TOAST表重写的优点)。

如果这种方法确实合理,我认为有一些改进的机会:

  • 您不需要重新创建/重置序列;您只需将现有序列链接到新表即可。
  • CREATE INDEX CONCURRENTLY似乎没必要,因为还没有其他人尝试访问foo_new。事实上,如果整个剧本都在一次交易中,那么此时甚至不会在外部可见。
  • 表名只需要在架构中是唯一的。如果您临时为新表格创建架构,则应该能够使用单个RENAME替换所有这些ALTER TABLE foo SET SCHEMA public
  • 即使您不希望并发写入,也不会对LOCK foo IN SHARE MODE造成伤害......

编辑:

序列重新分配比我预期的要多一些,因为它们似乎需要与父表保持相同的模式。但这是(似乎是)一个有效的例子:

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;