Skip to content

PostgreSQL 序列操作函数

概述

序列(Sequence)是 PostgreSQL 中的一种特殊数据库对象,用于生成唯一的数字序列。序列对象通常用于为表的主键字段生成唯一标识符,是实现自增长字段的标准方式。PostgreSQL 提供了一组专门的函数来操作序列对象,确保在多用户并发环境下安全地获取唯一的序列值。

INFO

序列的特点

  • 序列是使用 CREATE SEQUENCE 创建的特殊单行表
  • 提供多用户安全的方法获取连续序列值
  • 支持原子操作,确保并发安全
  • 序列值不会因事务回滚而回收

核心序列函数

1. nextval() - 获取下一个序列值

nextval(regclass) 函数将序列推进到下一个值并返回该值,这是一个原子操作。

语法

sql
nextval(sequence_name) → bigint

业务场景示例:用户注册系统

问题陈述:在用户注册系统中,需要为每个新注册的用户分配唯一的用户 ID。

解决方案

sql
-- 创建用户ID序列
CREATE SEQUENCE user_id_seq
    START WITH 1000
    INCREMENT BY 1
    CACHE 50;

-- 创建用户表
CREATE TABLE users (
    user_id BIGINT DEFAULT nextval('user_id_seq') PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
sql
-- 方式1:依赖默认值自动生成ID
INSERT INTO users (username, email)
VALUES ('alice', '[email protected]');

-- 方式2:手动调用nextval
INSERT INTO users (user_id, username, email)
VALUES (nextval('user_id_seq'), 'bob', '[email protected]');

-- 批量插入
INSERT INTO users (username, email) VALUES
    ('charlie', '[email protected]'),
    ('diana', '[email protected]'),
    ('edward', '[email protected]');

输入和输出

sql
-- 查看插入的结果
SELECT user_id, username, email FROM users ORDER BY user_id;
user_idusernameemail
1000alice[email protected]
1001bob[email protected]
1002charlie[email protected]
1003diana[email protected]
1004edward[email protected]

分析过程

  1. 序列从 1000 开始,每次递增 1
  2. DEFAULT nextval('user_id_seq') 确保每次插入时自动获取下一个序列值
  3. 即使多个用户同时注册,每个用户都会获得唯一的 ID
  4. CACHE 50 提高性能,预先缓存 50 个序列值

TIP

权限要求使用 nextval() 函数需要对序列拥有 USAGEUPDATE 权限。

2. setval() - 设置序列当前值

setval() 函数用于设置序列的当前值,支持两种和三种参数形式。

语法

sql
setval(sequence_name, value) → bigint
setval(sequence_name, value, is_called) → bigint

业务场景示例:数据迁移和序列重置

问题陈述:从旧系统迁移数据到新系统时,需要保持用户 ID 的连续性,并正确设置序列的起始位置。

解决方案

sql
-- 假设从旧系统导入了最大ID为5000的用户数据
-- 需要将序列设置为从5001开始

-- 方式1:双参数形式(下一个nextval返回5001)
SELECT setval('user_id_seq', 5000);

-- 验证:下一个nextval将返回5001
SELECT nextval('user_id_seq'); -- 返回: 5001

-- 方式2:三参数形式,is_called=true(与双参数相同)
SELECT setval('user_id_seq', 5000, true);
SELECT nextval('user_id_seq'); -- 返回: 5001
sql
-- 场景:需要让下一个nextval返回指定值(比如6000)
SELECT setval('user_id_seq', 6000, false);

-- 验证:下一个nextval将返回6000(而不是6001)
SELECT nextval('user_id_seq'); -- 返回: 6000
SELECT nextval('user_id_seq'); -- 返回: 6001

完整示例:订单编号管理系统

sql
-- 创建订单序列,从特定编号开始
CREATE SEQUENCE order_number_seq
    START WITH 202401001
    INCREMENT BY 1;

-- 创建订单表
CREATE TABLE orders (
    order_id BIGINT DEFAULT nextval('order_number_seq') PRIMARY KEY,
    customer_name VARCHAR(100),
    order_date DATE DEFAULT CURRENT_DATE,
    total_amount DECIMAL(10,2)
);

-- 场景1:正常使用
INSERT INTO orders (customer_name, total_amount)
VALUES ('张三', 199.99);

-- 场景2:需要跳过某些编号(比如预留给特殊订单)
SELECT setval('order_number_seq', 202401010); -- 跳到202401010
INSERT INTO orders (customer_name, total_amount)
VALUES ('李四', 299.99); -- 将获得订单号202401011

-- 场景3:需要从特定编号重新开始
SELECT setval('order_number_seq', 202402001, false); -- 下一个将是202402001
INSERT INTO orders (customer_name, total_amount)
VALUES ('王五', 399.99); -- 将获得订单号202402001

分析过程

  • setval(seq, value) 等同于 setval(seq, value, true)
  • is_called=true 表示指定的值已经被"使用",下次 nextval 会返回 value+1
  • is_called=false 表示指定的值尚未被使用,下次 nextval 会返回 value

3. currval() - 获取当前会话序列值

currval() 函数返回当前会话中最近通过 nextval() 获取的序列值。

语法

sql
currval(sequence_name) → bigint

业务场景示例:主从表数据插入

问题陈述:在电商系统中,创建订单时需要同时插入订单主表和订单明细表,订单明细需要引用刚创建的订单 ID。

解决方案

sql
-- 创建订单表和订单明细表
CREATE TABLE orders (
    order_id BIGINT DEFAULT nextval('order_id_seq') PRIMARY KEY,
    customer_id INTEGER,
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    total_amount DECIMAL(10,2)
);

CREATE TABLE order_items (
    item_id BIGINT DEFAULT nextval('order_item_id_seq') PRIMARY KEY,
    order_id BIGINT REFERENCES orders(order_id),
    product_id INTEGER,
    quantity INTEGER,
    unit_price DECIMAL(8,2)
);

-- 创建序列
CREATE SEQUENCE order_id_seq START WITH 1;
CREATE SEQUENCE order_item_id_seq START WITH 1;

完整事务示例

sql
BEGIN;

-- 插入订单主记录
INSERT INTO orders (customer_id, total_amount)
VALUES (1001, 599.98);

-- 获取刚插入的订单ID
-- currval获取当前会话中最近的nextval值
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
    (currval('order_id_seq'), 101, 2, 199.99),
    (currval('order_id_seq'), 102, 1, 199.99);

COMMIT;

输入和输出

sql
-- 查看插入结果
SELECT o.order_id, o.customer_id, o.total_amount,
       oi.item_id, oi.product_id, oi.quantity, oi.unit_price
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
ORDER BY o.order_id, oi.item_id;
order_idcustomer_idtotal_amountitem_idproduct_idquantityunit_price
11001599.9811012199.99
11001599.9821021199.99

分析过程

  1. currval() 只能在当前会话中调用过 nextval() 之后使用
  2. 它返回会话本地值,不受其他会话影响
  3. 适用于需要获取刚生成的序列值的场景

WARNING

注意事项如果在当前会话中从未对指定序列调用过 nextval(),调用 currval() 会报错。

4. lastval() - 获取最后使用的序列值

lastval() 函数返回当前会话中最近由任意序列的 nextval() 返回的值。

语法

sql
lastval() → bigint

业务场景示例:通用日志记录系统

问题陈述:在一个应用系统中,有多个模块都需要记录操作日志,需要获取最近生成的任何序列值用于关联。

解决方案

sql
-- 创建多个业务表和对应序列
CREATE SEQUENCE user_seq START WITH 1;
CREATE SEQUENCE product_seq START WITH 1;
CREATE SEQUENCE order_seq START WITH 1;

CREATE TABLE users (
    id BIGINT DEFAULT nextval('user_seq') PRIMARY KEY,
    username VARCHAR(50)
);

CREATE TABLE products (
    id BIGINT DEFAULT nextval('product_seq') PRIMARY KEY,
    product_name VARCHAR(100)
);

CREATE TABLE operation_log (
    log_id BIGINT DEFAULT nextval('log_seq') PRIMARY KEY,
    operation_type VARCHAR(20),
    reference_id BIGINT, -- 引用最近操作的记录ID
    operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    description TEXT
);

CREATE SEQUENCE log_seq START WITH 1;

使用示例

sql
-- 创建用户
INSERT INTO users (username) VALUES ('新用户');

-- 记录操作日志,使用lastval获取刚创建的用户ID
INSERT INTO operation_log (operation_type, reference_id, description)
VALUES ('CREATE_USER', lastval(), '创建新用户账户');
sql
-- 创建产品
INSERT INTO products (product_name) VALUES ('新产品');

-- 记录操作日志
INSERT INTO operation_log (operation_type, reference_id, description)
VALUES ('CREATE_PRODUCT', lastval(), '添加新产品到目录');
sql
-- 创建通用的操作记录函数
CREATE OR REPLACE FUNCTION log_operation(
    op_type VARCHAR(20),
    description TEXT
) RETURNS VOID AS $$
BEGIN
    INSERT INTO operation_log (operation_type, reference_id, description)
    VALUES (op_type, lastval(), description);
END;
$$ LANGUAGE plpgsql;

-- 使用示例
INSERT INTO users (username) VALUES ('测试用户');
SELECT log_operation('CREATE_USER', '通过函数记录用户创建');

输入和输出

sql
-- 查看操作日志
SELECT log_id, operation_type, reference_id, description, operation_time
FROM operation_log
ORDER BY log_id;
log_idoperation_typereference_iddescriptionoperation_time
1CREATE_USER1创建新用户账户2024-01-15 10:30:00
2CREATE_PRODUCT1添加新产品到目录2024-01-15 10:31:00
3CREATE_USER2通过函数记录用户创建2024-01-15 10:32:00

分析过程

  1. lastval() 不需要指定序列名称,自动引用最近使用的序列
  2. 适用于需要获取"最后操作"相关 ID 的通用场景
  3. 简化了跨表关联操作的代码编写

DANGER

重要提醒如果当前会话中从未调用过任何序列的 nextval(),调用 lastval() 会报错。

序列函数权限要求

函数所需权限
nextval()USAGE 或 UPDATE
setval()UPDATE
currval()USAGE 或 SELECT
lastval()对最后使用的序列需要 USAGE 或 SELECT

序列行为特性和注意事项

并发安全性

事务回滚特性

WARNING

重要特性序列值不会因事务回滚而回收,这可能导致序列中出现间隙。

演示示例

sql
-- 查看当前序列值
SELECT currval('user_id_seq'); -- 假设当前是 1005

BEGIN;
    -- 获取新的序列值
    SELECT nextval('user_id_seq'); -- 返回 1006

    -- 插入操作(故意失败)
    INSERT INTO users (user_id, username, email)
    VALUES (currval('user_id_seq'), NULL, '[email protected]'); -- 违反NOT NULL约束
ROLLBACK;

-- 事务回滚后,序列值1006不会被回收
SELECT nextval('user_id_seq'); -- 返回 1007,而不是 1006

系统崩溃恢复

INFO

持久性说明

  • 如果数据库在提交包含序列操作的事务前崩溃,序列状态可能不确定
  • 对于数据库内使用是安全的,因为未提交事务的其他影响也不会可见
  • 如果需要在数据库外使用序列值,确保在使用前事务已提交

最佳实践

1. 序列命名规范

sql
-- 推荐的命名模式
CREATE SEQUENCE table_name_column_name_seq;

-- 示例
CREATE SEQUENCE users_user_id_seq;
CREATE SEQUENCE orders_order_id_seq;
CREATE SEQUENCE products_product_id_seq;

2. 性能优化配置

sql
-- 为高并发场景配置序列
CREATE SEQUENCE high_traffic_seq
    START WITH 1
    INCREMENT BY 1
    CACHE 100        -- 增加缓存大小
    NO CYCLE;        -- 明确指定不循环

3. 序列监控和维护

sql
-- 查看序列当前状态
SELECT
    schemaname,
    sequencename,
    last_value,
    start_value,
    increment_by,
    max_value,
    min_value,
    cache_value,
    is_cycled,
    is_called
FROM pg_sequences
WHERE sequencename LIKE '%user%';

-- 检查序列使用情况
SELECT
    n.nspname AS schema_name,
    c.relname AS sequence_name,
    format_type(s.seqtypid, NULL) AS data_type,
    s.seqstart AS start_value,
    s.seqincrement AS increment_by,
    s.seqmax AS max_value,
    s.seqmin AS min_value,
    s.seqcache AS cache_value,
    s.seqcycle AS is_cycled
FROM pg_sequence s
JOIN pg_class c ON c.oid = s.seqrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
ORDER BY schema_name, sequence_name;

4. 错误处理模式

sql
-- 安全的currval使用模式
CREATE OR REPLACE FUNCTION safe_currval(seq_name TEXT)
RETURNS BIGINT AS $$
DECLARE
    result BIGINT;
BEGIN
    SELECT currval(seq_name) INTO result;
    RETURN result;
EXCEPTION
    WHEN OBJECT_NOT_IN_PREREQUISITE_STATE THEN
        -- 如果currval失败,使用nextval
        RETURN nextval(seq_name);
END;
$$ LANGUAGE plpgsql;

总结

PostgreSQL 序列操作函数提供了强大而灵活的机制来生成唯一标识符:

  • nextval() - 核心函数,获取下一个序列值,保证并发安全
  • currval() - 获取当前会话的序列值,用于关联操作
  • setval() - 设置序列位置,用于数据迁移和序列重置
  • lastval() - 获取最近使用的序列值,简化通用操作

正确理解和使用这些函数,能够确保数据的一致性和唯一性,同时在高并发环境下保持良好的性能表现。

TIP

关键要点

  1. 序列操作是原子的,保证并发安全
  2. 序列值不会因事务回滚而回收
  3. 合理设置权限和缓存参数
  4. 注意会话作用域的限制
  5. 做好错误处理和监控