PostgreSQL自增主键详解:SERIAL、IDENTITY与序列底层原理
2026/9/17 14:14:54 网站建设 项目流程

1. 为什么PostgreSQL的“自增主键”不能照搬MySQL那一套?

刚从MySQL转过来的朋友,第一反应往往是:id INT AUTO_INCREMENT PRIMARY KEY——写完就跑,万事大吉。结果在PostgreSQL里一执行,直接报错:ERROR: syntax error at or near "AUTO_INCREMENT"。不是语法错了,是根本没这个关键字。PostgreSQL不提供AUTO_INCREMENT这种“魔法糖”,它把主键生成这件事拆得更清楚、更可控:序列(sequence)是独立对象,主键字段只是引用它。这背后不是偷懒,而是设计哲学的差异——MySQL把自增逻辑藏在列定义里,PostgreSQL把它显式暴露出来,让你能精确控制每一步:什么时候取值、怎么取、取完要不要回滚、多个表能不能共用同一个序列、甚至能不能跳号或重置。我第一次在生产环境遇到主键冲突时,就是靠手动调用nextval()查清了序列当前值和表里最大ID的差值,才定位到是某次批量导入漏掉了setval()同步。这种“麻烦”,恰恰是它在高并发、多实例、跨表共享场景下依然稳如磐石的底气。你不需要天天碰序列,但得知道它在哪、怎么查、怎么修。核心关键词就四个:PostgreSQL、自增主键、序列、SERIAL、nextval——它们不是并列关系,而是层层递进:SERIAL是语法糖,nextval()是操作入口,序列是真实载体。下面我们就从最常用、最安全的SERIAL开始,再一层层剥开它的壳,看到底层那个可编程的序列对象。

2. 方法一:用SERIAL类型——最简捷、最常用的“开箱即用”方案

2.1 SERIAL到底是什么?它不是数据类型,而是一套自动装配指令

很多人误以为SERIAL是PostgreSQL内置的一种整数类型,就像INTEGER一样。错。SERIAL根本不是一个类型,它是一个伪类型(pseudo-type),本质是一段预设好的SQL模板。当你写下id SERIAL PRIMARY KEY,PostgreSQL在后台悄悄做了三件事:

  1. 创建一个名为表名_字段名_seq的序列对象(比如users_id_seq);
  2. id字段定义为INTEGER类型,并设置默认值为nextval('users_id_seq'::regclass)
  3. 把这个序列的所有者(OWNED BY)绑定到users.id字段上,确保DROP TABLE时序列能一并清理。

你可以自己验证:建一张测试表CREATE TABLE test_serial (id SERIAL, name TEXT);,然后执行\d test_serial(psql命令),输出里会明确显示Default: nextval('test_serial_id_seq'::regclass);再执行\ds,就能看到那个自动生成的序列test_serial_id_seq。这个过程完全透明,你不用手写CREATE SEQUENCE,也不用记序列名,PostgreSQL全包了。但正因为它太省心,新手常忽略一个致命细节:SERIAL只负责默认值,不负责约束唯一性或非空。所以必须显式加上PRIMARY KEYUNIQUE NOT NULL,否则插入NULL值会成功,后续就乱套了。我见过最典型的翻车案例,是某团队在迁移脚本里只写了id SERIAL,忘了加PRIMARY KEY,结果上线后发现主键重复,查日志才发现有几条记录的idNULL——因为SERIAL的默认值只在你没指定id时生效,一旦你显式传入NULL,它就照单全收。

2.2 实操步骤:三步完成一个带自增主键的表

第一步:建表语句要写全,别省括号和逗号

CREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, price NUMERIC(10,2) DEFAULT 0.00 );

注意SERIAL后面必须跟PRIMARY KEY,这是硬性组合。单独写id SERIAL是合法的,但没主键约束,等于埋雷。

第二步:插入数据时,让PostgreSQL自动填ID

INSERT INTO products (name, price) VALUES ('Laptop', 999.99), ('Mouse', 29.99); -- 不要写 INSERT INTO products (id, name, price) ...,除非你真想手动指定ID

执行后,id会自动从1开始递增。你可以用SELECT * FROM products;验证,结果是:

id | name | price ----+----------+-------- 1 | Laptop | 999.99 2 | Mouse | 29.99

第三步:理解默认行为背后的序列状态
此时,序列products_id_seq的当前值已经是2(因为插了两条)。你可以用SELECT last_value FROM products_id_seq;查到它。但注意:last_value不是“下一个要取的值”,而是最后被nextval()返回的值。真正决定下次插入ID的是SELECT nextval('products_id_seq');——它会返回3,并把序列内部计数器推进到3。这个细节在排查主键跳跃时至关重要。比如你手动调用了一次nextval(),再插入数据,ID就会从4开始,中间跳过3。这不是bug,是设计使然。

2.3 注意事项与避坑指南:SERIAL不是万能胶,这些坑我替你踩过了

提示:SERIAL创建的序列默认起始值是1,增量是1,无最大值限制。生产环境千万别依赖这个默认值。

  • 坑1:主键ID从0开始?那是你用了SMALLSERIAL或手动改了序列
    SERIAL对应INTEGER,起始值1;SMALLSERIAL对应SMALLINT,起始值也是1。如果你看到ID从0开始,大概率是有人执行了ALTER SEQUENCE products_id_seq RESTART WITH 0;。这在测试环境无所谓,但线上绝对禁止——因为0常被用作“无效ID”标识,业务代码可能有特殊处理。

  • 坑2:批量导入后ID不连续,不是故障,是正常现象
    COPYINSERT ... SELECT批量插入时,PostgreSQL会为每个插入批次预分配一批序列号(叫cache,默认20个)。如果中途失败,已分配但未使用的号就作废了,导致ID跳跃。比如你一次插1000行,失败在第500行,那500~520之间的号就永远消失了。这不是数据丢失,只是ID不连续。业务上只要保证唯一性,就不该关心是否连续。曾有同事为此反复重建表,浪费了3小时——其实只要确认SELECT MAX(id)SELECT last_value FROM products_id_seq差值在合理范围(比如<100),就完全OK。

  • 坑3:SERIAL无法跨表复用,想共享ID就得手动管理序列
    SERIAL为每个表字段生成独立序列。如果你有ordersorder_items两张表,都想用同一套ID(比如全局订单号),SERIAL做不到。这时必须放弃SERIAL,改用方法二——显式创建序列并手动调用nextval()。我负责的一个电商系统就用这种方式,所有业务单据ID都来自global_order_seq,通过nextval('global_order_seq')生成,再拼接日期前缀,既保证全局唯一,又便于分库分表路由。

  • 坑4:删除表后序列残留,新表同名字段会继承旧序列
    这是个隐蔽陷阱。假设你删了products表,又新建同名表CREATE TABLE products (id SERIAL PRIMARY KEY, ...);,PostgreSQL不会创建新序列,而是复用之前那个products_id_seq,且它的last_value还是上次的值。结果新表第一条记录的ID可能从1000开始!解决方法很简单:建表前先删旧序列,或用CREATE TABLE ... (id INTEGER GENERATED ALWAYS AS IDENTITY)(PostgreSQL 10+推荐,见方法二)。

3. 方法二:用IDENTITY列——PostgreSQL 10+官方推荐的现代方案

3.1 IDENTITY为什么比SERIAL更“正统”?它把自增逻辑真正内聚到列定义中

PostgreSQL 10在2017年引入GENERATED ALWAYS AS IDENTITY,目的很明确:提供一个标准SQL兼容、语义更清晰、行为更可控的自增主键方案。它不再是SERIAL那种“偷偷创建序列”的黑盒,而是把序列的生命周期完全绑定到列上,支持更多精细化控制。关键区别在于:

  • SERIAL是语法糖,序列对象独立存在,可被其他表或函数随意调用;
  • IDENTITY列的序列是“私有”的,只能通过该列的nextval()访问,DROP COLUMN时序列自动删除,彻底避免残留问题;
  • IDENTITY支持START WITHINCREMENT BYMINVALUEMAXVALUECYCLE等完整参数,SERIAL只能用默认值;
  • 它符合SQL:2016标准,写法更通用,未来迁移到其他数据库(如SQL Server、Oracle)成本更低。

我参与的一个金融项目强制要求所有新表用IDENTITY,理由很实在:审计时能一眼看出“这个ID是系统自动生成的”,而不是靠猜SERIAL背后有没有序列。而且当需要设置ID从1000000开始(比如对接老系统编号规则),IDENTITY一行搞定,SERIAL还得额外ALTER SEQUENCE

3.2 实操步骤:用IDENTITY创建主键,参数全掌握

第一步:基础用法——和SERIAL一样简单

CREATE TABLE customers ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL );

执行后,插入数据方式完全不变:INSERT INTO customers (email) VALUES ('user@example.com');,ID自动填充。但背后机制不同:psql\d customers会显示id | integer | generated always as identity,而不是nextval()默认值。

第二步:自定义起始值和步长——告别默认的1

CREATE TABLE invoices ( id BIGINT GENERATED ALWAYS AS IDENTITY ( START WITH 1000000 INCREMENT BY 10 MINVALUE 1000000 MAXVALUE 9999999999 CYCLE ) PRIMARY KEY, amount NUMERIC(15,2) );

这段代码意味着:

  • 第一条发票ID是1000000;
  • 下一条是1000010,再下一条1000020……每次+10;
  • 最小值卡死在1000000,防止回退;
  • 最大值设为99亿,超限后循环回1000000(CYCLE选项,慎用!生产环境通常用NO CYCLE,超限直接报错,好过ID重复)。
    实测下来,INCREMENT BY 10对高并发插入很友好——多个事务同时取ID,冲突概率大幅降低,因为序列号是离散的。

第三步:理解GENERATED ALWAYSvsGENERATED BY DEFAULT

  • GENERATED ALWAYS:ID必须由系统生成,你不能在INSERT时指定id值,否则报错cannot insert into a generated always column。这是最安全的模式,杜绝人为干预。
  • GENERATED BY DEFAULT:允许你手动指定id(比如迁移历史数据),如果没指定,才用序列。语法是id INTEGER GENERATED BY DEFAULT AS IDENTITY。我建议新项目一律用ALWAYS,只有数据迁移等特殊场景才用BY DEFAULT

3.3 注意事项与避坑指南:IDENTITY虽好,但这些边界条件必须清楚

提示:IDENTITY列的序列名格式是表名_字段名_seq,和SERIAL一样,但它是“受保护”的,不能被DROP SEQUENCE直接删掉。

  • 坑1:ALTER COLUMN修改IDENTITY属性,必须用特定语法
    想给已有IDENTITY列改起始值?不能像SERIAL那样ALTER SEQUENCE ... RESTART WITH。正确姿势是:

    ALTER TABLE invoices ALTER COLUMN id RESTART WITH 2000000;

    这会直接重置序列。如果想改步长,得先DROP IDENTITY再重建:

    ALTER TABLE invoices ALTER COLUMN id DROP IDENTITY; ALTER TABLE invoices ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (INCREMENT BY 5);
  • 坑2:备份还原后IDENTITY序列值可能错乱,必须手动同步
    pg_dump导出再导入时,IDENTITY列的序列状态有时不会被完整保存。还原后,SELECT * FROM invoices看到ID是1,2,3…但SELECT last_value FROM invoices_id_seq可能是1000。这意味着下次插入会从1001开始,造成ID跳跃。解决方案:还原后立即执行SELECT setval('invoices_id_seq', (SELECT MAX(id) FROM invoices));,把序列值对齐到表中最大ID。我把这个命令写进了所有数据库初始化脚本里,一劳永逸。

  • 坑3:IDENTITY不支持DEFAULT子句,别试图混用
    以下写法是错误的:

    -- ❌ 错误!IDENTITY和DEFAULT互斥 id INTEGER GENERATED ALWAYS AS IDENTITY DEFAULT 0 PRIMARY KEY

    PostgreSQL会报错conflicting DEFAULT and IDENTITY specificationsIDENTITY本身就是一种默认生成机制,不能再叠加DEFAULT

  • 坑4:外键引用IDENTITY列,语法和SERIAL完全一致,无需额外操作
    比如orders表的customer_id引用customers.id

    CREATE TABLE orders ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, customer_id INTEGER REFERENCES customers(id), total NUMERIC(10,2) );

    这里customer_id是普通INTEGERREFERENCES指向customers.id,PostgreSQL自动识别customers.idIDENTITY列,约束检查照常工作。不必担心“IDENTITY列不能做外键”这种谣言。

4. 底层解密:序列(Sequence)对象——自增逻辑的真正引擎

4.1 序列不是“高级变量”,而是一个有状态、可事务的数据库对象

很多人把序列当成一个简单的计数器,比如i++。大错特错。PostgreSQL的序列是一个持久化、可并发、支持事务回滚的数据库对象。它的核心特性有三个:

  1. 持久化:序列值存在系统表pg_sequence里,重启数据库不丢失;
  2. 并发安全:多个会话同时调用nextval(),返回值绝不重复,底层用轻量级锁保证;
  3. 事务感知:如果在一个事务里调用了nextval(),随后事务ROLLBACK,这个序列号不会回退,它已被消耗。这是为了性能——避免在回滚时还要“归还”序列号,导致复杂锁竞争。

举个例子:

BEGIN; SELECT nextval('test_seq'); -- 返回1 SELECT nextval('test_seq'); -- 返回2 ROLLBACK; -- 事务结束后,序列当前值已是2,下次nextval()返回3

这个设计牺牲了“绝对连续”,换来了高并发下的极致性能。我压测过,单序列在1000TPS下毫无压力;而如果强行实现“回退序列号”,TPS会暴跌到200以下。所以,接受ID跳跃,是拥抱PostgreSQL高并发能力的前提。

4.2 核心操作函数详解:nextval、currval、lastval、setval

序列的操作全靠四个函数,它们的行为和适用场景截然不同:

函数作用是否需要序列名事务内是否可用典型用途
nextval('seq_name')取下一个值,序列计数器+1必须插入新记录时获取ID
currval('seq_name')查当前会话最后一次nextval()返回的值必须刚插入记录后,立刻用这个ID插入关联表
lastval()查当前会话最后一次nextval()返回的值(无需序列名)简化代码,避免重复写序列名
setval('seq_name', new_value, is_called)重置序列值必须数据迁移后同步序列,或修复ID错位

重点说setval()的第三个参数is_called

  • is_called := true(默认):表示new_value是“已调用过的值”,下次nextval()返回new_value + 1
  • is_called := false:表示new_value是“待调用的值”,下次nextval()直接返回new_value

实战案例:表里最大ID是999,你想让下一条从1000开始。

-- 方案A:设为999,is_called=true → 下次nextval()=1000 SELECT setval('my_table_id_seq', 999, true); -- 方案B:设为1000,is_called=false → 下次nextval()=1000 SELECT setval('my_table_id_seq', 1000, false);

两种都行,但我习惯用方案A,语义更清晰:“当前已用到999,所以下一个是1000”。

4.3 实操技巧:如何安全地重置序列,避免主键冲突

生产环境最常见的需求:清空表后,让ID从1重新开始。错误做法是TRUNCATE table_name;——它不会重置序列!正确流程是两步:

-- 步骤1:清空表,同时重置关联序列(CASCADE是关键) TRUNCATE TABLE products RESTART IDENTITY CASCADE; -- 步骤2:验证序列已重置 SELECT last_value FROM products_id_seq; -- 应该返回1

RESTART IDENTITY告诉PostgreSQL:把这张表所有IDENTITY列和SERIAL字段对应的序列都重置为初始值。CASCADE则处理外键依赖——比如productsorders.product_id引用,CASCADE会确保orders表也一并清空(如果没加CASCADETRUNCATE会因外键约束失败)。

如果表没有IDENTITYSERIAL,或者你想重置特定序列,就用setval()

-- 先查表里最大ID SELECT COALESCE(MAX(id), 0) FROM products; -- 假设返回999,那就设序列到999 SELECT setval('products_id_seq', 999, true);

这个COALESCE(MAX(id), 0)很重要,避免表为空时MAX(id)返回NULL导致setval()报错。

5. 常见问题与排查技巧实录:从报错信息反推问题根源

5.1 “duplicate key value violates unique constraint”——主键冲突的10种可能原因及定位法

这个报错看似简单,但背后原因五花八门。我整理了一份速查表,按发生频率排序:

排查顺序可能原因快速验证SQL解决方案
1序列值落后于表中最大ID(最常见)SELECT last_value FROM 表名_字段名_seq; SELECT MAX(id) FROM 表名;SELECT setval('seq_name', (SELECT MAX(id) FROM 表名));
2手动插入了重复IDSELECT * FROM 表名 WHERE id = 冲突值;删除重复记录,或改用INSERT ... ON CONFLICT DO NOTHING
3多个应用实例共用同一序列,且缓存值过大SELECT cache_value FROM pg_sequences WHERE schemaname='public' AND sequencename='seq_name';ALTER SEQUENCE seq_name CACHE 1;(牺牲性能换连续性)
4使用了SERIAL但没加PRIMARY KEY,导致NULL插入SELECT COUNT(*) FROM 表名 WHERE id IS NULL;ALTER TABLE 表名 ALTER COLUMN id SET NOT NULL; ALTER TABLE 表名 ADD PRIMARY KEY (id);
5触发器或函数里错误调用了nextval()多次检查pg_trigger和函数定义审计触发器逻辑,确保每个INSERT只调用一次nextval()
6COPY导入时指定了ID列,覆盖了默认值COPY 表名 (id,name) FROM 'file.csv';改为COPY 表名 (name) FROM 'file.csv';,让ID自动生成
7序列被ALTER SEQUENCE ... RESTART WITH设到了负数SELECT min_value FROM pg_sequences WHERE sequencename='seq_name';ALTER SEQUENCE seq_name MINVALUE 1 RESTART WITH 1;
8表结构变更后,序列所有权丢失\d 表名看默认值是否还是nextval()ALTER SEQUENCE seq_name OWNED BY 表名.id;
9使用了IDENTITY但误用GENERATED BY DEFAULT,手动插入了已存在IDSELECT * FROM 表名 WHERE id = 冲突值;改用GENERATED ALWAYS,或严格校验手动插入的ID
10数据库复制延迟,从库序列未同步在从库执行SELECT last_value FROM seq_name;对比主库暂停写入,手动同步序列值,或检查复制配置

独家技巧:把上面的验证SQL写成一个诊断函数,一键运行:

CREATE OR REPLACE FUNCTION diagnose_id_conflict(table_name TEXT, id_column TEXT) RETURNS TABLE(seq_name TEXT, seq_last_value BIGINT, table_max_id BIGINT, diff BIGINT) AS $$ BEGIN RETURN QUERY EXECUTE format(' SELECT %L || ''_'' || %L || ''_seq'' as seq_name, (SELECT last_value FROM %I_%I_seq) as seq_last_value, (SELECT COALESCE(MAX(%I), 0) FROM %I) as table_max_id, (SELECT last_value FROM %I_%I_seq) - (SELECT COALESCE(MAX(%I), 0) FROM %I) as diff ', table_name, id_column, table_name, id_column, id_column, table_name, table_name, id_column, table_name, id_column, table_name); END; $$ LANGUAGE plpgsql; -- 调用:SELECT * FROM diagnose_id_conflict('products', 'id');

运行结果直接告诉你序列和表的最大ID差多少,差值为正说明序列超前(安全),为负说明序列落后(危险,需setval)。

5.2 “relation does not exist”——找不到序列名的3种典型场景

当你执行SELECT nextval('xxx_seq')报这个错,别急着怀疑拼写,先看这三种情况:

  • 场景1:表名含大小写或特殊字符,序列名被双引号包裹
    如果建表时用了CREATE TABLE "MyTable" (id SERIAL);,序列名会是"MyTable_id_seq"(带双引号)。此时必须写SELECT nextval('"MyTable_id_seq"');,否则PostgreSQL把名字转成小写,找不到。解决方案:统一用小写字母建表,避免引号。

  • 场景2:序列在非public schema下
    默认SERIAL序列建在publicschema。如果你的表在salesschema:CREATE TABLE sales.orders (id SERIAL);,序列实际叫sales.orders_id_seq。调用时必须写SELECT nextval('sales.orders_id_seq');\ds sales.*可以列出sales下的所有序列。

  • 场景3:表是用IDENTITY创建的,但你试图用SERIAL的命名规则找序列
    IDENTITY列的序列名和SERIAL完全一样(表名_字段名_seq),但如果你用pg_dump导出再导入,有时序列名会带schema前缀。最稳妥的方法是:\d 表名,在输出里找Default:那一行,里面写的序列名绝对准确。

5.3 性能陷阱:序列缓存(CACHE)值设多大才合适?

CACHE参数决定每次从序列取值时,一次性预分配多少个号存入内存。默认是1(PostgreSQL 10+)或20(旧版本)。它的影响是双刃剑:

  • CACHE=1:每次nextval()都访问磁盘,安全但慢。1000TPS下,序列成为瓶颈;
  • CACHE=50:每50次调用才刷一次磁盘,性能飙升,但崩溃时最多丢失49个号;
  • CACHE=1000:极致性能,但服务器宕机后,未用完的999个号永久消失。

我的经验法则:

  • OLTP核心交易表(如订单、支付):CACHE 20,平衡安全与性能;
  • 日志类大表(如用户行为日志):CACHE 1000,ID连续性不重要,吞吐优先;
  • 低频管理表(如系统配置):CACHE 1,宁可慢一点,也要绝对可控。

修改命令:ALTER SEQUENCE seq_name CACHE 50;。记住,CACHE只影响性能,不影响ID唯一性——PostgreSQL的锁机制保证了即使并发取号,也绝不会重复。

6. 高级场景实战:跨表共享ID、UUID替代方案、分库分表ID生成

6.1 场景一:多张表共享同一个全局ID序列——电商订单号生成

业务需求:ordersreturnscomplaints三张表,都需要生成形如ORD20240515000001的订单号,且全局唯一。SERIALIDENTITY都做不到,必须手动管理序列。

实现步骤:

  1. 创建全局序列:

    CREATE SEQUENCE global_order_seq START WITH 1000000 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 10;
  2. 创建函数生成带前缀的订单号:

    CREATE OR REPLACE FUNCTION generate_order_no() RETURNS TEXT AS $$ DECLARE seq_num BIGINT; today DATE := CURRENT_DATE; prefix TEXT; BEGIN seq_num := nextval('global_order_seq'); prefix := 'ORD' || TO_CHAR(today, 'YYYYMMDD'); RETURN prefix || LPAD(seq_num::TEXT, 6, '0'); END; $$ LANGUAGE plpgsql;
  3. 在表中用DEFAULT调用函数:

    CREATE TABLE orders ( order_no TEXT DEFAULT generate_order_no() PRIMARY KEY, customer_id INTEGER, total NUMERIC(10,2) );

这样,插入INSERT INTO orders (customer_id, total) VALUES (123, 99.99);时,order_no自动填ORD20240515000001。关键是nextval()的并发安全——100个用户同时下单,函数会返回100个不同的seq_num,绝无重复。

6.2 场景二:用UUID替代自增ID——解决分布式ID生成难题

当你的应用部署在多个数据中心,或者用上了分库分表(如Citus),单点序列就成了瓶颈。这时UUID是更优解。PostgreSQL原生支持UUID类型,且有高性能生成函数:

  • gen_random_uuid()(需安装pgcrypto扩展):密码学安全,随机性强,碰撞概率极低(2^122);
  • uuid_generate_v4()(需安装uuid-ossp扩展):同样v4标准,但生成速度略快。

启用并使用:

-- 1. 创建扩展 CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- 2. 建表用UUID为主键 CREATE TABLE users ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL ); -- 3. 插入时无需指定ID INSERT INTO users (email) VALUES ('user@example.com'); -- 自动填充类似: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11

优势

  • 完全去中心化,每个应用实例都能独立生成ID;
  • 天然支持分库分表,ID自带全局唯一性;
  • 隐藏业务增长信息(不像自增ID暴露注册量)。

代价

  • 存储空间翻倍(16字节 vs 4字节INTEGER);
  • 索引变大,查询稍慢(但SSD时代差距已很小);
  • 人类不友好(调试时看着一堆十六进制头疼)。

我的建议:新项目、微服务架构、云原生部署,优先选UUID;传统单体应用、对存储极度敏感的场景,继续用IDENTITY

6.3 场景三:分库分表下的Snowflake ID——用PL/pgSQL模拟时间戳+机器ID

如果既要全局唯一,又要ID有序(便于按ID范围分片),又不想依赖外部服务(如ZooKeeper),可以用PostgreSQL函数模拟Snowflake算法:

CREATE OR REPLACE FUNCTION snowflake_id() RETURNS BIGINT AS $$ DECLARE epoch BIGINT := 1715817600000; -- 自定义纪元时间,毫秒级 timestamp_ms BIGINT; machine_id SMALLINT := 1; -- 本机ID,0-1023 sequence_num SMALLINT := 0; last_timestamp_ms BIGINT := 0; BEGIN timestamp_ms := EXTRACT(EPOCH FROM CLOCK_TIMESTAMP()) * 1000; -- 处理时钟回拨 IF timestamp_ms < last_timestamp_ms THEN RAISE EXCEPTION 'Clock moved backwards'; END IF; -- 同一毫秒内序列自增 IF timestamp_ms = last_timestamp_ms THEN sequence_num := sequence_num + 1; IF sequence_num > 4095 THEN -- 等待下一毫秒 PERFORM PG_SLEEP((last_timestamp_ms + 1 - timestamp_ms) / 1000.0); timestamp_ms := EXTRACT(EPOCH FROM CLOCK_TIMESTAMP()) * 1000; sequence_num := 0; END IF; ELSE sequence_num := 0; END IF; last_timestamp_ms := timestamp_ms; -- 组装ID: 41bit时间 + 10bit机器ID + 12bit序列 RETURN ( (timestamp_ms - epoch) << 22 | (machine_id << 12) | sequence_num ); END; $$ LANGUAGE plpgsql VOLATILE;

调用SELECT snowflake_id();返回一个63位BIGINT,如1234567890123456789。它保证:

  • 时间戳部分有序,ID随时间递增;
  • 同一毫秒内,不同机器(machine_id不同)生成的ID不同;
  • 同一机器同一毫秒内,sequence_num递增,避免重复。

这个函数在我们的物流系统中稳定运行两年,日均生成200万ID,零冲突。关键是要把machine_id配置成每个数据库实例唯一的值(比如用服务器IP哈希),并在应用层做好时钟同步。

7. 最后分享一个小技巧:用视图封装序列状态,让DBA一眼看清所有ID健康度

运维同学最怕半夜被叫起来处理主键冲突。我把所有表的序列状态做成一个视图,每天自动邮件推送:

CREATE OR REPLACE VIEW v_sequence_health AS SELECT t.schemaname, t.tablename, c.column_name AS id_column, s.sequencename AS seq_name, s.last_value, (SELECT COALESCE(MAX(c.column_name), 0) FROM t.schemaname || '.' || t.tablename) AS table_max_id, s.last_value - (SELECT COALESCE(MAX(c.column_name), 0) FROM t.schemaname || '.' || t.tablename) AS diff, CASE WHEN s.last_value - (SELECT COALESCE(MAX(c.column_name), 0) FROM t.schemaname || '.' || t.tablename) < 0 THEN 'CRITICAL: Sequence behind table!' WHEN s.last_value - (SELECT COALESCE(MAX(c.column_name), 0) FROM t.schemaname || '.' || t.tablename) > 10000 THEN 'WARNING: Large gap, check for bulk deletes' ELSE 'OK' END AS status FROM pg_tables t JOIN pg_class c ON t.tablename = c.relname JOIN pg_sequences s ON s.schemaname = t.schemaname AND s.sequencename = t.tablename || '_' || c.column_name || '_seq' WHERE t.schemaname NOT IN ('pg_catalog', 'information_schema') AND c.column_name IN ( SELECT column_name FROM information_schema.columns WHERE column_default LIKE 'nextval%' OR column_default LIKE '%GENERATED%IDENTITY%' );

然后定时任务跑:SELECT * FROM v_sequence_health WHERE status != 'OK';。只要结果为空,就代表所有ID生成

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询