向高流量的大型 PostgreSQL 表添加主键

suj*_*eet 5 postgresql index primary-key amazon-rds

我需要向一个高流量的大型 PostgreSQL 表(大约 2TB)添加主键。这是一项关键操作,我正在寻找如何有效地完成该操作的指导。

我已经尝试过以下步骤:

-- Step 1: Add id identity column 
ALTER TABLE users
ADD COLUMN id BIGINT GENERATED ALWAYS as IDENTITY;

-- Step 2: Add unique index on (id, user_id) concurrently
CREATE UNIQUE INDEX CONCURRENTLY id_user_id_idx
   ON users (id, user_id);

-- verify that step 2 is completed
-- Step 3: Add primary key
ALTER TABLE users
   ADD CONSTRAINT users_pkey PRIMARY KEY USING INDEX id_user_id_idx;
Run Code Online (Sandbox Code Playgroud)

我面临两个问题:

  • 表完全锁定在“步骤 1”本身上。

    我知道这是预料之中的,但如果有任何选择可以避免这种情况,请提出建议。

  • 我收到以下错误,

错误:无法扩展文件“base/16401/90996”:设备上没有剩余空间提示:检查可用磁盘空间。

600GB我的服务器上还有剩余的存储空间。

由于表将被锁定在“第 1 步”,并且如果没有选项可以避免这种情况,我可以利用停机时间id先添加列,然后运行其他两个脚本。

我不知道这是否可以解决存储错误。

请提供任何建议,以便我能够以尽可能少的停机时间添加 PK。

PostgreSQL v14.6

Erw*_*ter 12

为什么?

您的步骤 1 需要的空间远远超过 600 GB(暂时)。该表大约有 2 TB。至少必须有尽可能多的bigint可用空间(减去可能的膨胀,再加上每行新列 8 个字节),因为该更改迫使 Postgres 重写整个表。

最大限度地减少阻塞并最大限度地减少总存储需求

相反,请按以下顺序执行:

小提琴

添加一个没有默认值的可为空的列id,因此它最初是这样的null

ALTER TABLE users ADD COLUMN id bigint;
Run Code Online (Sandbox Code Playgroud)

这样,Postgres 就可以应付微小的元数据更改。没有表重写,没有阻塞。
我会将 PK 列命名为“user_id”,而不喜欢广泛使用、非描述性且高度重复的名称“id”。但保留“id”以与问题保持一致。

手动创建SEQUENCE

CREATE SEQUENCE users_id_seq;
Run Code Online (Sandbox Code Playgroud)

使列“拥有”序列:

ALTER SEQUENCE users_id_seq OWNED BY users.id;
Run Code Online (Sandbox Code Playgroud)

添加列默认值,该列仅在新行中生效。

ALTER TABLE users ALTER COLUMN id SET DEFAULT nextval('users_id_seq');
Run Code Online (Sandbox Code Playgroud)

看:

null以总大小(或其他大小)的 1% 左右的批量更新预先存在的行(仍包含值)。在单独的事务中,允许自动清理启动并标记死行以供重用。这样,表就不会增长太多,600 GB 就足够了。

自从Postgres 11中添加了SQL程序,我们就可以COMMIT在匿名代码块中。假设有一个timestamptzusers.inserted_at(最好有一个索引!),这样的东西可以工作:

DO
$do$
DECLARE
   _ts timestamptz := (SELECT COALESCE(min(inserted_at), now()) FROM users);  -- must not be NULL
   _step interval  := '7 days';  -- adjust to your data !!!
BEGIN
   LOOP
      RAISE NOTICE 'Updating rows starting from %', _ts;  -- optional
      
      UPDATE users
      SET    id = nextval('users_id_seq')
      WHERE  inserted_at >= _ts
      AND    inserted_at <  _ts + _step
      AND    id IS NULL;  -- optional

      EXIT WHEN NOT FOUND AND _ts >= now();  -- adjust to your case !!!

      COMMIT;  -- Requires Postgres 11+ !!!
      PERFORM pg_sleep(10);  -- adapt to your setup: long enough so let autovacuum kick in
      _ts := _ts + _step;
   END LOOP;
END
$do$;
Run Code Online (Sandbox Code Playgroud)

或者,在客户端中循环,并VACUUM users;在迭代之间运行以确保空间得到重用。(VACUUM不能在事务内运行。)

看:

最终,所有旧行都被更新。

现在创建唯一索引CONCURRENTLY,以避免阻塞插入。与您的步骤 2 类似,但仅限于(id)

CREATE UNIQUE INDEX CONCURRENTLY users_id_idx ON users (id);
Run Code Online (Sandbox Code Playgroud)

我看不出加入user_idPK 的充分理由。如果您需要它进行仅索引扫描,请考虑使用INCLUDE (user_id). 但这并不总是有益的。看:

现在使用唯一索引添加新的 PK,而不阻止插入(您的步骤 3):

ALTER TABLE users ADD CONSTRAINT users_pkey PRIMARY KEY USING INDEX users_id_idx;
Run Code Online (Sandbox Code Playgroud)

这也将隐式设置列NOT NULL

最后,使用Peter Eisentraut 的函数 upgrade_serial_to_identity(tbl regclass, col name)将 转换serialIDENTITY列。作为超级用户

SELECT upgrade_serial_to_identity('users'::regclass, 'id')
Run Code Online (Sandbox Code Playgroud)

或者坚持serialPK,也许就足够了。

有关的: