财务凭证号不能用row_number(),因其不持久、不防重、并发不安全;必须用自增列+业务前缀拼接,如'yz2024'+lpad(id,6,'0'),确保唯一、有序、可追溯。

财务凭证号为什么不能直接用 ROW_NUMBER()?
因为 ROW_NUMBER() 是纯窗口函数,只按查询结果排序生成连续数字,不保存、不持久、不防重——每次查都重算,跨事务或并发插入时必然冲突。财务凭证号必须唯一、有序、可追溯、带业务前缀(如 'YZ2024' + 6位流水),所以得靠数据库机制保障,不能只靠查询时“算出来”。
用自增字段 + 业务前缀拼接最稳妥
核心思路:用数据库原生自增列(如 MySQL 的 AUTO_INCREMENT、PostgreSQL 的 SERIAL、SQL Server 的 IDENTITY)保序保唯一,再在应用层或视图里拼前缀和补零。这是生产环境最常见、最可靠的做法。
以 PostgreSQL 为例:
CREATE TABLE voucher ( id SERIAL PRIMARY KEY, biz_date DATE NOT NULL, amount NUMERIC(12,2), -- 其他字段... );
插入后,通过以下方式生成凭证号(如 'YZ2024000001'):
- 应用层取刚插入的
id,格式化为 6 位(LPAD(id::TEXT, 6, '0')),再拼前缀'YZ' || EXTRACT(YEAR FROM CURRENT_DATE)::TEXT - 或建一个计算字段视图:
SELECT *, 'YZ' || EXTRACT(YEAR FROM biz_date)::TEXT || LPAD(id::TEXT, 6, '0') AS voucher_no FROM voucher
注意:不要在触发器里用 ROW_NUMBER() OVER (PARTITION BY EXTRACT(YEAR FROM biz_date) ORDER BY id) 生成号——它无法保证并发安全,且每次查询都变,不能作为凭证号存档依据。
如果硬要用 ROW_NUMBER() 分组编号,仅限只读报表场景
比如要查某月每笔凭证在当月内的序号(用于对账单页码或临时排序),这时可以分组用 ROW_NUMBER(),但绝不能把它当主键或凭证号字段存库。
示例(按月份分组,从 1 开始编号):
SELECT
*,
'YZ' || EXTRACT(YEAR FROM biz_date)::TEXT ||
LPAD(ROW_NUMBER() OVER (
PARTITION BY EXTRACT(YEAR FROM biz_date), EXTRACT(MONTH FROM biz_date)
ORDER BY biz_date, id
)::TEXT, 6, '0') AS monthly_voucher_no
FROM voucher
WHERE biz_date >= '2024-01-01';
风险点:
-
PARTITION BY字段若有 NULL(如biz_date为空),整组会被归为一类,导致编号错乱 - ORDER BY 不明确(比如只按日期不按时间戳或ID),同一秒多笔插入时序不确定,编号不可重现
- 该结果随数据增删实时变化,无法用于归档、打印、第三方对接等需要稳定值的环节
真正要“分组自动递增”,得靠序列 + 业务逻辑控制
MySQL 不支持按年/月重置的序列,PostgreSQL 和 SQL Server 可以结合序列与条件判断模拟。但更推荐:为每个业务周期(如每年)单独建序列,由应用控制创建时机。
PostgreSQL 示例(每年一个序列):
CREATE SEQUENCE voucher_seq_2024 START 1 INCREMENT 1;
插入时手动取值:
INSERT INTO voucher (id, biz_date, amount)
VALUES (NEXTVAL('voucher_seq_2024'), '2024-03-15', 1234.56);
关键细节:
- 序列名需动态管理(如用应用代码生成
voucher_seq_+ 当前年份),避免硬编码 - 首次使用前需检查序列是否存在,不存在则
CREATE SEQUENCE;否则报错 - 序列值不回滚,即使事务失败,号也已跳过——这反而是财务要求(防重、不可逆)
这种做法比 ROW_NUMBER() 实在得多,但代价是运维复杂度上升:序列生命周期、权限、跨年切换都要管。
实际落地时,90% 的财务系统最终都放弃“全自动分组递增”,转而用全局自增 ID 拼业务前缀——简单、稳定、可审计。真正难的不是生成号码,而是确保凭证号一旦生成就不能改、不能重、不能丢,这些靠函数计算永远做不到。










