尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
纯SQL生成百万级测试数据:MySQL/Oracle/PostgreSQL通用模板
给开发库塞百万行测试数据这件事我几乎每个迭代都要干一次。早年间我也跟大多数人一样第一反应是写存储过程一个 WHILE 循环从 1 加到 100 万MySQL 上要先用 DELIMITER 包起来Oracle 上换 PL/SQL 匿名块到了 PostgreSQL 又得改成 DO 语句。折腾过几轮之后你会发现存储过程这条路在 MySQL/Oracle/PostgreSQL 三种库里一点都不通用而且测试库上能建 procedure换个环境就未必有权限。其实纯 SQL 也能把同样的事情干完——一条 INSERT SELECT 就能从 0 干到百万行不依赖存储过程、不依赖外部脚本。这篇就把我在三种数据库上验证过的高效 SQL 模板、参数配置和踩坑记录汇总出来适合要做性能冒烟、灌数测试、备份恢复演练的同行直接抄作业。1. 为什么造测试数据要绕开存储过程1.1 权限、兼容性、维护成本这三道坎先说权限。开发环境里拿到的账号经常只有 SELECT、INSERT、UPDATE、DELETE 这几类权限建存储过程需要 CREATE ROUTINE 之类的权限很多团队出于安全考虑并不会放开。尤其自动化流水线里跑数用的连接账号权限更是卡得死死的。你手里有成熟的存储过程模板换个环境跑不了等于白搭。再看兼容性。MySQL 的存储过程要用 DELIMITER 改变分隔符Oracle 要用 PL/SQL 的 BEGIN...END COMMITPostgreSQL 又得写 DO $$ ... $$ LANGUAGE plpgsql。三种语法没有一个是通用的改一个字段就得同步改三份代码调试的时候还经常被各种隐式转换、异常处理折磨。纯粹为了造测试数据这个成本完全没必要。最后是维护成本。测试数据不是造一次就完事业务迭代、字段变更、数据清理之后都需要重新灌数。存储过程是持久化对象每次改动都要先 DROP 再 CREATE出问题还得去排查哪个环境还残留旧版本。换成纯 SQL 就舒服多了——在 Navicat、PL/SQL Developer、DBeaver 里直接选中执行用完即弃数据不行改一改再跑一遍没有任何历史包袱。1.2 纯 SQL 造数的适用边界纯 SQL 方案最适合的场景是单表百万至千万行级别、生成规则比较简单自增编号、固定前缀、随机数值、日期区间、需要反复重建、不需要逐行依赖前一行状态的数据。它的核心思路是先生成一个足够长的数字/日期序列再把序列映射成目标字段本质上是集合操作所以效率远高于逐行循环。但也有明显的边界。如果你要造的是亿级数据或者生成逻辑依赖前一行计算结果、需要复杂分支判断纯 SQL 就不够用了那种场景更该考虑专业数据生成工具、导出导入脚本或者外部 ETL 流程。另外纯 SQL 生成的数据分布相对机械如果要做很贴近真实业务分布的压测数据还是得配合自定义脚本做抽样和洗数。下面所有方案都默认在规则简单、单表百万行这个范围内讨论。2. MySQL递归 CTE 一把梭旧版本用笛卡尔积放大2.1 MySQL 8.0 的 WITH RECURSIVE 写法MySQL 8.0 引入递归 CTE 之后造数逻辑一下子清晰了很多。思路就是把造数变成生成一个从 1 到 N 的序列然后再在这个序列上做字段映射。下面是最基础的序列生成方式WITH RECURSIVE seq(n) AS ( SELECT 1 UNION ALL SELECT n 1 FROM seq WHERE n 1000000 ) SELECT n FROM seq;这里有个非常关键的参数cte_max_recursion_depth。MySQL 8.0 默认值只有 1000意思是一条递归 CTE 最多迭代 1000 次超过就直接报错。所以跑百万序列之前必须先设置会话级别的递归深度SET SESSION cte_max_recursion_depth 1000000;设置完之后就可以把序列塞进 INSERT SELECT 里了。拿一张常见的用户表举例包含自增 ID、用户名、手机号、邮箱、注册时间SET SESSION cte_max_recursion_depth 1000000; INSERT INTO app_user (id, user_name, phone, email, create_time) WITH RECURSIVE seq(n) AS ( SELECT 1 UNION ALL SELECT n 1 FROM seq WHERE n 1000000 ) SELECT n, CONCAT(user_, n), LPAD(FLOOR(RAND() * 10000000000), 11, 0), CONCAT(user_, n, test.com), NOW() - INTERVAL FLOOR(RAND() * 365) DAY FROM seq;我实测下来这种写法在普通 SSD 上生成 100 万行基本在几十秒量级。需要注意递归 CTE 生成的中间结果在 MySQL 里可能落到临时表如果你的tmpdir空间不足或者临时表超过tmp_table_size、max_heap_table_size的限制MySQL 会把内存临时表转成磁盘临时表速度会明显下降。真正常年造数的团队我更建议直接在库里建一张永久性的numbers数字辅助表从 1 到 1000 万一次性生成好后面所有造数 SQL 都 JOIN 这张表稳定性和速度都比每次递归 CTE 好得多。2.2 5.7 及更早版本的 UNION ALL 放大法MySQL 5.7 不支持 CTE但同样能生成百万序列手法是0 到 9 十个数字的派生表做笛卡尔积。原理很简单六张各 10 行的数字表做 CROSS JOIN组合数就是 10 的 6 次方刚好 100 万。SELECT a.n b.n * 10 c.n * 100 d.n * 1000 e.n * 10000 f.n * 100000 1 AS n FROM (SELECT 0 n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a, (SELECT 0 n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b, (SELECT 0 n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) c, (SELECT 0 n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) d, (SELECT 0 n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) e, (SELECT 0 n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) f;表 a 是个位、b 是十位、c 是百位以此类推到十万位1是为了让编号从 1 开始而不是 0。中间结果虽然组合量大但 MySQL 优化器对这种无依赖的笛卡尔积处理得挺快输出 100 万行没什么压力。唯一要注意的是别把中间结果先存成用户临时表再二次处理直接一条 SELECT 一步到位最省事。如果你连这段 UNION ALL 都嫌长还有个取巧的办法利用系统自带的元数据表做放大比如information_schema.columns或mysql.help_topic搭配用户变量自增SELECT rn : rn 1 AS n FROM information_schema.columns a, information_schema.columns b, (SELECT rn : 0) r LIMIT 1000000;不过这个方案依赖元数据表里的行数跨版本行为不稳定我一般只拿来应急规规矩矩的数字派生表才是首选。2.3 把数字序列变成看起来像业务数据的字段序列只是骨架造数的关键是让字段看起来像真的。MySQL 里常用的字段映射手法有几种固定前缀可以用CONCAT(user_, n)定长手机号用LPAD补零随机日期用NOW() - INTERVAL FLOOR(RAND() * N) DAY随机状态字段用IF(RAND() 0.7, 1, 0)。完整模板可以这样写INSERT INTO app_user (id, user_name, phone, email, reg_date, is_active) WITH RECURSIVE seq(n) AS ( SELECT 1 UNION ALL SELECT n 1 FROM seq WHERE n 1000000 ) SELECT n, CONCAT(user_, n), LPAD(FLOOR(RAND() * 10000000000), 11, 0), CONCAT(user_, n, test.com), DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 1095) DAY), IF(RAND() 0.7, 1, 0) FROM seq;这里有两个容易踩的点第一NOW()在整个语句里是常量想生成最近三年内随机时间必须靠INTERVAL搭配RAND()做偏移不能直接对NOW()做随机第二RAND()在 INSERT SELECT 里通常是逐行求值的但不同版本的优化器行为有细微差别建议先跑一个 1 万行的小批量确认随机分布符合预期再放全量。另外如果目标表有唯一索引或主键生成前要自己确保编号不重复比如上面的n就必须保证唯一。3. OracleCONNECT BY LEVEL 的百万行正确打开方式3.1 直接巡百万行会碰到的内存瓶颈Oracle 里最直觉的序列来源是CONNECT BY LEVEL写法极其短INSERT INTO app_user (id, user_name, phone, email, create_time) SELECT LEVEL, user_ || LEVEL, LPAD(FLOOR(DBMS_RANDOM.VALUE() * 10000000000), 11, 0), user_ || LEVEL || test.com, SYSDATE - FLOOR(DBMS_RANDOM.VALUE() * 1000) FROM dual CONNECT BY LEVEL 1000000;这段 SQL 在多数 Oracle 11g/12c/19c 环境下跑 100 万行问题不大LEVEL的值正好从 1 递增到 100 万可以直接拿来当 ID。但你要知道它在背后的执行方式层级查询是深度优先递归从dual出发每次迭代加深一层100 万层的深度意味着递归内部要维护一条极长的层级路径内存和 CPU 开销都不小。我试过把LEVEL 1000000改成LEVEL 10000000十有八九会碰到 ORA-30009提示not enough memory for CONNECT BY。到那个量级调大 PGA 治标不治本得换更聪明的写法。3.2 用笛卡尔积把 LEVEL 放大到任意规模既然递归深度是瓶颈那就别让递归变深改成让每层递归扇出更多行。具体做法是两个各 1000 行的平凡结果集做 CROSS JOIN1000 乘以 1000 正好 100 万再用ROWNUM做编号INSERT INTO app_user (id, user_name, phone, email, create_time) SELECT ROWNUM, user_ || ROWNUM, LPAD(FLOOR(DBMS_RANDOM.VALUE() * 10000000000), 11, 0), user_ || ROWNUM || test.com, SYSDATE - FLOOR(DBMS_RANDOM.VALUE() * 1000) FROM ( SELECT 1 FROM dual CONNECT BY LEVEL 1000 ) a CROSS JOIN ( SELECT 1 FROM dual CONNECT BY LEVEL 1000 ) b WHERE ROWNUM 1000000;这样每条分支递归深度只有 1000内存压力小了一个量级而且理论上你可以把 1000 换成 3000、5000轻松把结果集放大到千万、亿级。WHERE ROWNUM 1000000其实在这句里是冗余的因为两个集合相乘刚好 100 万保留它是为了让上限可控如果你想要任意 N 行可以让两个内层集合各自生成CEIL(SQRT(N))行外层再用ROWNUM N截断。12c 及以上版本也可以用FETCH FIRST 1000000 ROWS ONLY代替 ROWNUM 过滤语义更清晰。3.3 INSERT ALL 与 APPENDNOLOGGING 提速Oracle 批量插数据有几个常用加速手段。第一个是INSERT ALL它可以把同一份 SELECT 结果同时插入多张表非常适合造数时顺便给影子表、按日期拆分的月表各灌一份INSERT ALL INTO app_user (id, user_name, email) VALUES (id, user_name, email) INTO app_user_log (id, action) VALUES (id, INIT) SELECT LEVEL AS id, user_ || LEVEL AS user_name, user_ || LEVEL || test.com AS email FROM dual CONNECT BY LEVEL 1000000;第二个是直接路径插入。普通 INSERT 会写 redo 日志走直接路径的INSERT /* APPEND */会批量写数据块配合表上的 NOLOGGING 属性可以大幅减少 redo 量ALTER TABLE app_user NOLOGGING; INSERT /* APPEND */ INTO app_user (id, user_name, phone, email, create_time) SELECT LEVEL, user_ || LEVEL, LPAD(FLOOR(DBMS_RANDOM.VALUE() * 10000000000), 11, 0), user_ || LEVEL || test.com, SYSDATE - FLOOR(DBMS_RANDOM.VALUE() * 1000) FROM dual CONNECT BY LEVEL 1000000; COMMIT; ALTER TABLE app_user LOGGING;这里特别提醒一句NOLOGGING 意味着这些数据块不会完整记录 redo一旦数据库需要基于归档日志做恢复这些块可能变成不可读状态。所以它只适合纯测试表业务表、需要保数据的表千万别这么玩。另外 APPEND 直接路径在 RAC 环境还有更严格的限制单实例测试环境不受影响。4. PostgreSQLgenerate_series 是三家最顺手的生成器4.1 数字序列、日期序列和随机文本的生成PostgreSQL 造数体验在三家里是最好的因为内置的generate_series函数就是干这个的。生成 1 到 100 万SELECT n FROM generate_series(1, 1000000) AS g(n);generate_series支持整数、bigint、numeric也支持 timestamp 步进。生成日期序列非常灵活SELECT * FROM generate_series(2020-01-01::timestamp, 2023-12-31::timestamp, 1 day) AS g(day);随机文本随手就可以拼md5(random()::text)生成 32 位随机哈希substr(md5(random()::text), 1, 8)取前 8 位配合前缀用起来很方便。相比 MySQL 的 LPAD 和 Oracle 的 DBMS_RANDOMPG 的表达式更直白几乎不用查文档就能写出来。4.2 CTAS 直接建表 vs 先建表再 INSERTPostgreSQL 里造数有两种落地方式。如果这张测试表本来就不存在最推荐直接 CTASCREATE TABLE AS SELECT一步到位CREATE TABLE app_user AS SELECT n AS id, user_ || n AS user_name, 13 || lpad((floor(random() * 1000000000))::text, 9, 0) AS phone, user_ || n || test.com AS email, timestamp 2020-01-01 random() * interval 1000 days AS reg_date, (ARRAY[ACTIVE, INACTIVE, PENDING])[(1 floor(random() * 3))::int] AS status FROM generate_series(1, 1000000) AS g(n);CTAS 创建的表不会自动带主键、索引这对造数反而是好事——先灌数据再回头建索引通常比带索引逐行插入快得多。如果表已经存在那就规规矩矩用 INSERT SELECTINSERT INTO app_user (id, user_name, phone, email, reg_date, status) SELECT n, user_ || n, 13 || lpad((floor(random() * 1000000000))::text, 9, 0), user_ || n || test.com, timestamp 2020-01-01 random() * interval 1000 days, (ARRAY[ACTIVE, INACTIVE, PENDING])[(1 floor(random() * 3))::int] FROM generate_series(1, 1000000) AS g(n);两种方式跑百万行都很快CTAS 在纯测试场景还能配合 UNLOGGED 表再快一截这个下面专门说。4.3 贴近真实分布的字段表达式清单造数不只是填满行最好让数据分布看起来像真的。我常用的 PG 字段表达式有这么几类目标推荐表达式说明自增 IDn直接用 generate_series 的序列值用户名user_手机号13邮箱n时间timestamp 2020-01-01 random() * interval 1000 days均匀分布在区间内枚举状态(ARRAY[ACTIVE,INACTIVE,PENDING])[(1 floor(random() * 3))::int]从数组随机取一个数值floor(random() * 100)0 到 99 均匀分布随机文本substr(md5(random()::text), 1, 16)需要较真随机串时可用有一点容易忽略generate_series(1, N)默认按整数返回如果字段是 bigint 或者你在表达式里做了乘法要注意类型和溢出问题。想生成 bigint 序列就写成generate_series(1::bigint, 1000000)让 PG 自动推导类型。4.4 几个能明显提速的 PG 开关PostgreSQL 最大的写入开销在 WAL 日志。架构上任何事务提交都要写 WAL造测试数据这种场景数据库本身不追求崩溃恢复所以可以适当关掉一些保护机制。最常用的是synchronous_commit参数普通默认是on每次 commit 都要等 WAL 刷盘改成off之后 commit 不会同步等待百万行插入能快不少SET synchronous_commit off;再进一步测试表可以直接建 UNLOGGED 表。UNLOGGED 表不写 WAL插入速度提升非常明显代价是崩溃或异常重启后表数据会丢失。对一次性测试数据来说这个代价完全可接受CREATE UNLOGGED TABLE app_user AS SELECT ... FROM generate_series(1, 1000000) AS g(n);还有一个容易被低估的点单条 SQL 插入百万行是一个超大事务PG 的 vacuum 在这个事务结束前无法清理旧版本数据WAL 也会显著膨胀。如果磁盘不大建议把 100 万拆成 10 个批次每批 10 万行分别在事务里提交。这样既能控制 WAL 大小失败重跑时也不用从头再来。5. 三库横向对比方案、参数与一眼看懂的差距5.1 三种数据库百万造数方案对照表把前面讲的方案收拢到一张表里方便按库直接选型数据库首选方案百万行写法难易度灵活控制行数常用提速手段MySQL 8.0WITH RECURSIVE 递归 CTE简单但要先调会话参数改递归终止条件cte_max_recursion_depth、分片执行MySQL 5.70-9 数字表 CROSS JOIN中等SQL 较长调整笛卡尔层级先物化数字表避免中间临时表过大OracleCONNECT BY LEVEL / 笛卡尔放大简单大数量要换写法ROWNUM 或 FETCH FIRSTAPPEND、NOLOGGING、PARALLELPostgreSQLgenerate_series最简单传参即可UNLOGGED 表、synchronous_commitoff从表格里能看出一个共性所有方案的核心都是先生成序列再做字段映射。区别只在于序列生成机制——MySQL 靠 CTE 或笛卡尔积Oracle 靠层级查询PG 直接内置函数。理解了这个本质换库写 SQL 的时候就很容易迁移。5.2 决定生成速度的三个隐藏变量很多人以为造数慢是SQL 写得不好其实多数情况下瓶颈在数据库日志和约束上。第一个隐藏变量是日志写放大。MySQL 有 binlogOracle 有 redo/undoPostgreSQL 有 WAL。100 万行 INSERT SELECT 产生的日志量往往比数据本身还大日志写入时间会明显超过插入时间本身。我的经验是测试库如果没开 binlogMySQL 插入能快一倍以上Oracle 靠 NOLOGGING APPEND 把 redo 压下来PG 用 UNLOGGED 表直接绕开 WAL。这些手段的本质都是牺牲恢复能力换速度所以只用在纯测试表上。第二个隐藏变量是索引和外键。带一堆索引的表插 100 万行每个索引都要同步维护耗时跟行的数量成正比。保险做法是造数前先 DROP 掉非必要索引和禁用外键约束数据灌完再一次性 CREATE INDEX。对 MySQLSET FOREIGN_KEY_CHECKS0可以临时跳过外键检查对 Oracle外键约束要么 DISABLE要么保证插入顺序PG 则可以用SET CONSTRAINTS ALL DEFERRED延迟约束检查但需要在同一事务里。注意MySQL 的外键检查关闭是会话级的别忘了造完再打开。第三个隐藏变量是约束校验本身。NOT NULL、UNIQUE、CHECK 约束都会在插入时逐行校验如果数据本身规则简单比如编号唯一这些校验其实是白白浪费 CPU。反过来如果你生成的数据违反约束导致中途报错整个事务回滚的代价更大。所以造数 SQL 跑之前先用 EXPLAIN 或小批量验证类型、长度、唯一性是最值得的一步。5.3 分片插入与执行监控我不建议所有场景都一条 SQL 梭哈百万行。分片插入虽然看起来多跑几次但好处很明显单批失败只回滚当前批次方便定位问题日志和临时空间压力更小还能实时看到每批耗时判断账号是不是越跑越慢。三个库的分片方式各不相同。MySQL 的递归 CTE 可以把终止条件改成区间比如生成n BETWEEN 200001 AND 300000Oracle 用 CONNECT BY 加外层WHERE ROWNUM BETWEEN过滤或者干脆用前面说的笛卡尔放大配合FETCH FIRST ... OFFSET ...PG 最简单generate_series(200001, 300000)直接给区间。执行端一般就是外层循环跑十次每次打印耗时。监控方面MySQL 可以用SHOW PROCESSLIST看 SQL 是否还在跑Oracle 可以查V$SESSION_LONGOPS观察进度PG 没有专门的 INSERT 进度视图靠每个批次的结束时间估算。我自己的习惯是每批结束查一下COUNT(*)对不上数量立刻停。6. 实测踩坑记录版本差异、内存溢出和序列不同步6.1 MySQL 的 cte_max_recursion_depth 只是第一道坑MySQL 8.0 的递归 CTE最容易踩的坑就是忘记设置cte_max_recursion_depth报错信息很直白——Recursive query aborted after 1001 iterations翻译过来就是递归第 1001 次被拦下了。解决办法是执行前先SET SESSION cte_max_recursion_depth 1000000;。但这里有个隐蔽问题如果你用连接池或者 ORM 框架连接数据库这个会话变量只在当前连接里生效换一个连接就得重新设置。我就在自动化脚本里吃过亏脚本里明明 set 了跑批时连接池把连接切走了照样报错。后来我把SET SESSION写进了和造数 SQL 同一条连接的执行序列里才算彻底解决。还有一个坑是递归深度设置过大后中间结果会把 tmpdir 撑爆。CTE 生成的百万级中间结果如果超过内存临时表上限会转成磁盘临时文件写到 MySQL 的 tmpdir 里。磁盘不够或者 /tmp 是 tmpfs 的话直接报磁盘空间不足。遇到这种情况要么给临时表扩容要么换永久 numbers 表方案别跟递归 CTE 死磕。6.2 Oracle 的 ORA-30009 与临时表空间Oracle 造数最典型的问题就是 ORA-30009。前面说过直接CONNECT BY LEVEL 10000000深度递归很容易触发内存不足这时候我建议直接换成笛卡尔积放大写法而不是去调 PGA_AGGREGATE_TARGET。前者是把内存压力变成广度组合后者只是拆东墙补西墙。另一个容易忽略的问题是临时表空间。Oracle 的排序、哈希连接都可能用到临时表空间如果临时表空间没开自动扩展或者空间不足百万行插入会报 ORA-01652无法扩展临时段。造数之前先确认TEMP表空间状态比临时抱佛脚改 SQL 更省事。还遇到过一种情况目标表有触发器每次插入的行会触发 PL/SQL 逻辑直接路径插入对这种表会被降级为普通插入APPEND 的好处没了。所以造数前检查目标表有没有触发器、物化视图日志有的话要么临时禁用要么干脆换一张干净的表否则你看到的执行时间跟预期差一大截。6.3 PostgreSQL 的整型溢出和 WAL 膨胀PG 造数也有自己的暗坑。最常见的是类型溢出generate_series(1, 1000000)返回的是普通 integer如果你在表达式里做n * 2之类的运算到 100 万附近就可能超出 int 上限2147483647报 integer out of range。处理办法是显式使用generate_series(1::bigint, ...)或者对结果做n::bigint转换再运算。然后是 WAL 膨胀。我之前在一台只有 200G 磁盘的测试机上跑 1000 万行插入单条大事务直接把 pg_wal 目录撑到几十 G差点把磁盘写满。后来改成 100 万一批每批一个事务基本控制在可接受范围内。如果你的数据库开了复制槽WAL 膨胀会更严重因为复制槽会阻止 WAL 清理这种情况务必分片。最后提一句synchronous_commitoff只影响提交时的等待行为不会减少 WAL 总量想真正减量还是得靠 UNLOGGED 表。6.4 造数之后的清理与序列重置数据造完最容易翻车的是序列不同步。Oracle 和 PG 如果用了显式 ID 插入序列对象本身不会跟着往前跳下次应用插入新数据时nextval会从旧值开始跟已有主键撞车。PG 里我一般这么修SELECT setval(pg_get_serial_sequence(app_user, id), (SELECT COALESCE(MAX(id), 1) FROM app_user));Oracle 18c 以上可以直接ALTER SEQUENCE seq_name RESTART START WITH 1000001;旧版本只能先 DROP 再 CREATE。MySQL 的 AUTO_INCREMENT 反而不用太担心显式插入自增列时 InnoDB 会自动把计数器抬到 MAX1前提是你的sql_mode没有禁用这种默认行为。最后说说清理。测试数据不要后用TRUNCATE而不是DELETE。DELETE 只是逐行标记删除空间和索引都不会立刻释放大数据量下还容易撑大 undo 和 WALTRUNCATE 是干脆利落地重置整个表MySQL 里还会顺带重置 AUTO_INCREMENTPG 里不需要额外 VACUUMOracle 里则会回收已分配的区段。三种库在这个场景下行为出奇地一致。我自己现在造数的默认套路就是这套三库模板MySQL 8 直接 CTEOracle 用笛卡尔放大PG 用 generate_series每套模板前面都固定跑一个 1 万行的冒烟版本确认类型、长度、唯一性没问题之后再放全量。这个习惯帮我避免了好几次半夜被测试环境报错吵醒的情况。
RELATED

相关推荐

VS编译环境命令窗口:命令行编译与自动化构建实战指南

VS编译环境命令窗口:命令行编译与自动化构建实战指南

1. 被忽视的效率利器:VS编译环境命令窗口很多人装了Visual Studio之后,日常操作就是打开IDE、点按钮、等编译、看输出,一套流程走下来行云流水,但一旦离开那个图形界面,就不知道该怎么办了。其实VS安装包里藏着一个非常…

📅 2026/10/17 9:19:26
Java电费管理系统毕设攻略:阶梯电价计费与Spring Boot实现

Java电费管理系统毕设攻略:阶梯电价计费与Spring Boot实现

简介:基于Java的电费管理系统毕业设计项目,面向计算机相关专业学生或Java开发者。系统围绕用户管理、电费计算、缴费管理、数据统计与提醒服务等核心模块展开,采用Spring BootMyBatisMySQL的主流技术栈,并涉及Spring Security权限…

📅 2026/10/17 9:19:26
基于Matlab的花朵识别系统实战:从预处理到SVM分类

基于Matlab的花朵识别系统实战:从预处理到SVM分类

在Matlab里把一朵花识别系统从零搭起来,这个需求比很多人想象中要曲折。它不一定需要高深算法,但需要把图像预处理、特征提取、分类器训练、界面交互这些环节像流水线一样串起来,任何一环掉链子,最后识别效果都会惨不忍睹。我前后…

📅 2026/10/17 9:19:26
MORE NEWS

更多资讯

📰

AI无法处理请求?从自然语言处理到提示工程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

两节锂电池主动均衡设计实战:MP2642+PIC32MZ方案详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

CTF Web入门刷题实战:从SQL注入到PHP反序列化

CTF的Web方向,算是所有方向里最容易上手、也最容易让新人产生“我好像会了”错觉的领域。我最近在某个CTF练习平台上把Web方向的1~10题完整刷了一遍,整理了一份writeup,顺便把做题过程中踩过的坑、试错的路径和对题目设计的思考一起写出来。对…

📰

基于Motus2的机器人自主导航实战:从强化学习到真机部署

去年年底我接手了一个挺头疼的项目:一台四轮差速底盘,要求它在仓库里自主找货架、绕过障碍、把货物送到指定工位。老板的需求很简短——"让它自己想明白怎么办"。传统导航方案倒是能跑通,但货架位置一变、地上多了几个托盘&#xf…

📰

金融系统开发为何必须基于具体业务场景

我无法根据当前输入生成符合要求的博文。原因如下:项目标题 "financial-services" 过于宽泛,仅为一个行业领域名词,未指向具体技术实现、业务场景、工具集成、问题解决或实操项目;项目正文为空,无任何功能描…

📰

基于MP2642GR-Z与ATmega1280的两串电池主动均衡方案设计与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬