1. 为什么造测试数据这件事值得认真对待
先说个常见场景:开发完一个分页查询接口,本地库里只有几十条数据,随便点两下看着挺正常。等部署到测试环境,DBA导了一份生产脱敏数据进来,接口直接超时,SQL执行计划走了全表扫描。这时候你才发现,原来当初建的那个索引根本没生效,或者查询条件里字段类型对不上,隐式转换把索引废了。问题根源不在于代码写错,而在于你从来没有在“像样的数据量”上验证过。
这就是批量制造测试模拟数据的价值。它解决的核心问题有三个:第一,让你的SQL、索引、查询计划在接近真实的数据分布下接受检验;第二,让压力测试、并发测试有足够的样本量,而不是拿几十行数据自欺欺人;第三,让前端分页、导出、统计报表这些功能在数据量上来之后依然表现稳定。说白了,造数据不是糊弄事,而是把测试环境的数据基础打牢,让问题提前暴露在测试阶段,而不是等上线后被用户发现。
我见过不少团队用三种方式解决这个问题:一是写存储过程循环插入,二是用Excel手工拉数据再导入,三是找DBA要生产库脱敏快照。第一种在小数据量下能用,一到百万级就慢得让人抓狂;第二种只适合几十上百条的场景,数据量一大根本不现实;第三种效果最好,但脱敏流程繁琐、审批链路长,普通项目根本等不起。所以,自己动手用SQL脚本批量造数据,反而是最灵活、最快见效的方案。
这篇文章面向的读者很明确:后端开发、测试工程师、DBA,以及所有需要自己准备测试数据的人。我会用PG、MySQL、SqlServer三种数据库分别演示一套完整的造数据方案,从基础表结构到百万级数据量,把脚本写法、性能调优、常见坑一次讲透。
2. 造数之前必须想清楚的几件事
2.1 别急着写脚本,先理解你的表结构和约束
很多人拿到建表语句就开始写INSERT,结果插入到一半碰到外键约束报错,或者唯一索引冲突直接中断。我建议先花十分钟做一件事:把目标表的所有约束列出来,分清哪些是NOT NULL、哪些是UNIQUE、哪些是外键、哪些有CHECK约束、哪些字段有默认值。这一步看起来简单,实际上决定了你后面脚本怎么写。
举个例子,一张用户表有两个唯一约束,一个是user_name,一个是email。如果你用随机字符串去填充,冲突概率其实不小,尤其在数据量过十万之后,碰撞几乎是必然的。这时候就需要在生成逻辑里做去重处理,比如用序列号拼接固定前缀的方式保证唯一性,而不是单纯依赖随机。另一个典型坑是外键关联:订单表的user_id必须存在于用户表中,如果你先插订单后插用户,或者两个表的数据量对不上,就会一直报错。正确的做法是先插主表,再插从表,并且从表的外键值要从主表中真实存在的ID范围里取。
还有一个容易被忽略的细节是字段长度和字符集。varchar(20)的字段你塞进去一个32位字符串,数据库不会报错,但数据会被截断,后续查询结果看起来就很怪。我建议在造数之前,先跑一条DESCRIBE或者查看information_schema,把所有字段类型、长度过一遍。尤其是中文场景下,utf8mb4和utf8的字符占用不一样,字段长度不够时中文直接变成问号,这种问题排查起来特别费劲。
2.2 数据分布比数据量更重要
造数不是把随机值往表里塞就完事了,更重要的是让数据分布接近真实场景。你想象一下真实业务里的订单表,金额字段大概率的分布是大量小额、少量大额,状态字段是大部分已完成、少量待支付和退款。如果你把订单金额做成纯随机均匀分布,那查出来的数据特征就跟线上完全不一样,测试结论也没有参考价值。
所以我在造数时会刻意控制生成规则。最简单的做法是给数据加权重,比如用CASE WHEN配合随机数实现按比例分布。拿订单状态举例,我可以让70%的数据是“已完成”,20%是“待支付”,10%是“已取消”。这样后续你测试“待支付订单列表”这个功能时,查出来的数据量和真实业务就是同数量级的,不会出现全表都是待支付这种不合理的场景。同理,时间字段也要注意别全部集中在同一秒,可以用随机偏移把时间均匀打散在某个时间区间里,方便测试按时间维度的统计报表。
除了字段分布,表之间的数量比例也要提前规划。比如一个用户平均产生多少条订单,这个比例决定了联合查询时的返回行数。我建议先估算再动手,比如你要造一万用户、五十万订单,那平均每个用户50条订单,生成订单数据时user_id就要从用户表的ID范围内随机取,并根据正态分布来控制极端用户和普通用户的占比。
3. PG、MySQL、SqlServer三种数据库的造数实操方案
3.1 PostgreSQL的generate_series是造数神器
先聊PG,因为它是三种数据库里造数据最省力的。PG内置了一个叫generate_series的函数,可以直接生成一个数字序列,配合INSERT INTO ... SELECT语法,一行语句就能插入几十万行数据,不需要写循环。
我随便举个例子。假设有一张users表,字段是id、user_name、email、created_at,我想插入50万条测试数据,PG脚本可以这样写:
INSERT INTO users (user_name, email, created_at) SELECT 'user_' || gs, 'user_' || gs || '@example.com', now() - (random() * interval '365 days') FROM generate_series(1, 500000) AS gs;这里的关键点在于:generate_series(1, 500000)生成50万个数字,然后通过拼接字符串构造唯一的用户名和邮箱,时间字段用random()函数做随机偏移。整个执行计划是一个集合操作,效率比逐行循环高非常多。我实测过,在一台普通配置的机器上,插入50万行只需几十秒,具体取决于磁盘性能。
如果要造业务表,比如订单表,需要关联用户ID,那就在SELECT里加个子查询从users表取ID范围,再用随机数做偏移:
INSERT INTO orders (user_id, order_no, amount, status, created_at) SELECT u.id, 'ORD' || lpad(gs::text, 8, '0'), round((random() * 500 + 10)::numeric, 2), CASE WHEN random() < 0.7 THEN 'completed' WHEN random() < 0.9 THEN 'pending' ELSE 'canceled' END, now() - (random() * interval '90 days') FROM generate_series(1, 200000) AS gs CROSS JOIN LATERAL (SELECT id FROM users ORDER BY random() LIMIT 1) AS u;这里用到了CROSS JOIN LATERAL,简单理解就是每一行生成时都从用户表随机取一条ID。不过要提醒一句,如果用户表特别大,ORDER BY random()会导致全表排序,性能很差。更稳妥的做法是先确定ID范围,在应用层或SQL里用随机偏移量取ID。比如user_id在1到500000之间,那就直接用1 + floor(random() * 500000)来生成,不需要真实关联用户表,反正测试数据不需要严格外键一致性。
3.2 MySQL的递归CTE和内存临时表方案
MySQL的情况稍微复杂一点。MySQL 8.0及以上版本支持递归CTE,也可以实现类似generate_series的效果。但在8.0之前的版本,要么用存储过程循环,要么用数字辅助表。我这边先说8.0的写法,再补充老版本的替代方案。
递归CTE生成50万行:
INSERT INTO users (user_name, email, created_at) WITH RECURSIVE seq AS ( SELECT 1 AS n UNION ALL SELECT n + 1 FROM seq WHERE n < 500000 ) SELECT CONCAT('user_', n), CONCAT('user_', n, '@example.com'), DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY) FROM seq;这个写法跟PG的generate_series思路一致。但有一个关键区别:递归CTE默认最大递归深度有限制,如果一次生成百万级数据,可能会报错或者性能奇差。我建议把单批插入数量控制在10万以内,然后分循环执行。另一种更推荐的做法是先创建一张数字表,比如dim_numbers表,里面提前灌入从1到1000000的数字,之后所有造数场景都从这张表里取数:
INSERT INTO users (user_name, email, created_at) SELECT CONCAT('user_', n), CONCAT('user_', n, '@example.com'), DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY) FROM dim_numbers WHERE n <= 500000;这种数字辅助表方案在老版本MySQL、新版本MySQL之间通用,能避免递归CTE的性能坑,而且执行计划走的是最简单的扫描,没有递归链路的开销。我在实际项目中更喜欢这种方式,尤其是在数据量百万级以上时,明显更稳。
存储过程循环我不太推荐,尤其是逐行INSERT的写法,数据量上万就开始明显变慢。如果非要用存储过程,也建议配合批量提交,每凑满1000行做一次批量插入。不过说实话,在MySQL里用数字辅助表配合INSERT INTO ... SELECT,已经是性能和代码可读性的最佳平衡了。
3.3 SqlServer的GO批处理与递归CTE
SqlServer的语法和MySQL有一些相似之处,也支持递归CTE,另外有一个很特别的批处理指令GO,可以配合变量循环实现多次执行。
SqlServer造用户的写法:
INSERT INTO users (user_name, email, created_at) WITH seq AS ( SELECT 1 AS n UNION ALL SELECT n + 1 FROM seq WHERE n < 500000 ) SELECT CONCAT('user_', n), CONCAT('user_', n, '@example.com'), DATEADD(DAY, -FLOOR(RAND(CHECKSUM(NEWID())) * 365), GETDATE()) FROM seq OPTION (MAXRECURSION 0);这里要比MySQL多写一个OPTION (MAXRECURSION 0),因为SqlServer默认递归CTE上限是100层,不关掉这个限制,序列生成到100就走不下去了。RAND函数在SqlServer里每行调用时需要用CHECKSUM(NEWID())做种子,否则同一批次内会返回相同的随机数,这是新手特别容易踩的坑。
另一种常用的SqlServer方案是GO循环。因为SqlServer里循环逐行插入效率同样很低,我更喜欢利用GO指令让同一批INSERT重复执行,次数通过变量控制:
DECLARE @i INT = 0; WHILE @i < 50 BEGIN INSERT INTO users (user_name, email, created_at) SELECT CONCAT('user_', ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) + @i * 10000), CONCAT('user_', ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) + @i * 10000, '@example.com'), DATEADD(DAY, -FLOOR(RAND(CHECKSUM(NEWID())) * 365), GETDATE()) FROM master..spt_values CROSS JOIN master..spt_values AS s2 WHERE s1.number < 100 AND s2.number < 100; SET @i = @i + 1; END;master..spt_values是SqlServer系统表,里面有几千行数字记录,交叉连接后能产生足够多的行数来支撑INSERT。这种方式的好处是不依赖递归CTE,老版本SqlServer也能用。不过说实话,用WITH递归已经够用了,不必为了炫技去写这种系统表交叉连接。
三种数据库的写法各有特色,我整理了一个对比表方便你选型:
| 数据库 | 推荐方案 | 单次推荐数据量 | 适用场景 |
|---|---|---|---|
| PostgreSQL | generate_series + INSERT SELECT | 10万~100万 | 中大规模数据,语法简洁 |
| MySQL 8.0+ | 数字辅助表 + INSERT SELECT | 5万~20万 | 通用场景,性能稳定 |
| MySQL 老版本 | 存储过程批量循环 | 1万~5万 | 表结构简单,仅做功能联调 |
| SqlServer | 递归CTE + MAXRECURSION 0 | 5万~20万 | 通用场景,语法清晰 |
3.4 造数脚本的流程化设计:从一次执行到处处可用
很多人写完一次脚本就扔了,下次要造另一张表又从头写一遍,效率很低。我自己的习惯是把造数脚本做成一个半自动化的模板,核心思路是:表名和字段规则参数化,通过简单修改就能复用。
具体做法是这样的。先用一个变量定义目标表名、起始ID、数据量、日期范围,然后主体SQL尽量写成“基于数字序列生成行”的通用结构。拿MySQL举例,我会固定一个套路:
-- 参数区 SET @target_table = 'orders'; SET @start_id = 1; SET @batch_size = 50000; SET @user_min_id = 1; SET @user_max_id = 100000; SET @start_date = '2024-01-01'; SET @end_date = '2024-12-31'; -- 执行区 INSERT INTO orders (order_no, user_id, amount, status, created_at) SELECT CONCAT('ORD', LPAD(n, 8, '0')), @user_min_id + FLOOR(RAND() * (@user_max_id - @user_min_id + 1)), ROUND(10 + RAND() * 500, 2), CASE WHEN RAND() < 0.7 THEN 'completed' WHEN RAND() < 0.9 THEN 'pending' ELSE 'canceled' END, DATE_ADD(@start_date, INTERVAL FLOOR(RAND() * DATEDIFF(@end_date, @start_date)) DAY) FROM dim_numbers WHERE n BETWEEN @start_id AND @start_id + @batch_size - 1;这样做的好处很明显:下次换一张表,只需要改字段映射关系,而生成逻辑、参数设置、批量控制这些骨架完全不用动。我在团队里还习惯把参数区单独放在SQL文件顶部,注释写清楚每个参数的含义,这样新人接手也能直接上手改。
再进一步,如果表特别多,可以写一个简单的存储过程,接收表名参数,内部根据表名走不同的字段生成逻辑。不过坦白讲,存储过程的维护成本比普通SQL高,我更倾向于用外部脚本(比如Shell或Python)来循环调用预写好的SQL文件,这样逻辑清晰、出问题也容易定位。
3.5 造数性能优化:别让数据库卡死在插入上
百万级数据造起来,时间花在哪里?除了SQL本身的执行,还有很大一部分在索引维护、日志写入、约束检查上。所以性能优化也要从这几个方向同时下手。
先说批量大小。单条INSERT逐行提交性能最差,因为每条都要走一次事务提交、一次日志刷盘。通用做法是把批量控制在5000到50000之间,具体值取决于表结构复杂度和磁盘性能。我在MySQL里测过,批量大小从1000提升到10000,总耗时能缩短一半以上,但再往上提升就不明显了,甚至因为单条SQL太大反而变慢。
再说索引。如果目标表已经有普通索引、唯一索引,插入时每行都要更新索引,数据量大了之后索引维护会成为瓶颈。如果测试数据不需要保留原有索引,可以临时禁用或先删掉索引,插完再重建。MySQL里可以用ALTER TABLE ... DISABLE KEYS配合ENABLE KEYS,尤其对MyISAM表效果明显。InnoDB表不能直接禁用,但可以先DROP INDEX再ADD INDEX。PG可以临时把索引设置为UNLOGGED,或者造数期间关闭同步提交。SqlServer也类似,可以把表切换到堆模式,完成后再重建聚集索引。
日志也是一个容易被忽视的因素。如果你只需要造测试数据,不在乎崩溃恢复,可以临时把数据库设置为不强制每次提交都刷盘。MySQL的innodb_flush_log_at_trx_commit设为0或2,PG的synchronous_commit设为off,SqlServer可以暂时用简单恢复模式。调整之前要确认这是独立测试环境,生产库千万别动这个开关。
我整理了一个优化清单,按优先顺序排列:
- 使用INSERT INTO ... SELECT替代逐行插入
- 数据量超过十万时,删除或禁用目标表索引
- 关闭外键检查(MySQL的SET FOREIGN_KEY_CHECKS=0)
- 临时降低日志刷盘强度(仅限隔离的测试环境)
- 分批提交,每批控制在5万行以内
- 如果目标表有自增主键,避免过小步长,减少页分裂和锁竞争
4. 造数过程中的常见问题与排查实录
4.1 随机数据撞了唯一约束
造数最常见的报错就是Duplicate entry。根本原因还是随机生成的字段值在大量样本下必然产生碰撞。你拿UUID或者随机字符串去填充唯一字段,数据量到几十万之后碰撞概率一点都不低,尤其是字段长度只有十几位的情况下。
我的解决办法是:唯一性字段一律使用数字序列拼接前缀来保证,不要依赖随机。用户名就是user_00000001这种结构,订单号就是ORD00000001这种结构。有些场景要求看起来像真实数据,比如邮箱,那至少保证本地部分是用序列号生成的,@后面的域名可以随机换几个后缀。这样既保证了唯一性,又不会让数据看起来千篇一律。真要用随机,就把随机部分长度拉到碰撞概率可忽略的程度,比如32位十六进制,而不只是8位字符串。
4.2 时间字段全是同一个值
这个问题特别阴险。我在一次造数据时用了RAND()函数生成日期偏移量,结果日志显示5000行数据的created_at完全一样。查了半天才发现,MySQL的RAND()在同一个执行批次里只初始化一次,后续所有行拿到的都是同一个随机数。这其实不是RAND过期的问题,而是优化器对查询做常量折叠,把RAND()优化成了固定的值。
解决办法要么用相对随机的表达式,比如在每行关联一个序列数参与计算,让RAND()受行号影响:RAND(n + 1)。要么在应用层生成数据。PG和SqlServer也有类似问题,SqlServer的RAND()在单行函数里不加种子时每批只取一个随机值。踩过这个坑之后,我养成了一个习惯:每次造完数据先看一眼时间字段的DISTINCT计数,如果远小于行数,就得怀疑随机函数是不是被优化了。
4.3 外键约束导致的插入顺序问题
外键约束说白了就是主表和从表的插入顺序问题。我见过太多人先插子表,再插主表,结果外键校验直接报错。还有一种情况是两张表相互依赖,比如用户表和用户详情表各有一个外键指向对方,插入就卡住了。
处理思路很直接:先分析外键依赖关系,按“被引用的表在前,引用的表在后”的顺序插。如果有双向依赖,就临时禁用外键检查,插完再开启。MySQL可以用SET FOREIGN_KEY_CHECKS=0,PG可以用SET CONSTRAINTS ALL DEFERRED,SqlServer就没这么方便了,一般建议在造数阶段先把外键禁掉或者删掉,完事再重建。这里需要提醒:禁用外键检查只适用于隔离的测试库,如果在共享的开发库上操作,可能导致其他同事的数据被连带影响。
4.4 造数速度越跑越慢,怎么排查
很多时候脚本一开始跑得很快,越往后越慢,直观感受就是数据库“累了”。背后原因通常有三个。第一是索引膨胀,随着数据量增长,B+树的层级变深,每次插入定位索引位置的开销增大。第二是表碎片,重复的插入让物理存储不再连续,扫描时间变长。第三是事务日志膨胀,日志文件持续增大,刷盘压力升高。
排查方法也很直接。插入过程中监控几个指标:磁盘IO等待时间、事务日志大小、目标表的索引统计信息。如果发现IO占用很高,多半是日志刷盘太频繁,可以增大批处理量或者调整刷盘参数。如果索引统计信息长期不更新,可以手动ANALYZE(PG/MySQL)或UPDATE STATISTICS(SqlServer),让优化器重新认识数据分布。另外,如果造数脚本要执行很久,建议用后台任务方式跑,同时定时输出进度,别让终端卡死在那干等。
常见问题速查表:
| 现象 | 排查方向 | 推荐处理 |
|---|---|---|
| 唯一键冲突报错 | 字段生成逻辑是否保证唯一 | 改用序列号拼接前缀,不依赖纯随机 |
| 时间字段完全相同 | 随机函数被常量折叠 | 让随机表达式依赖行号或序列号 |
| 外键约束报错 | 从表先于主表插入 | 按依赖顺序插入,测试环境临时禁用外键 |
| 插入速度断崖式下降 | 索引膨胀、日志刷盘压力大 | 分批插入,删除索引,调整日志参数 |
| 内存占用过高 | 单批数据量过大 | 压缩单批行数,控制在5万以内 |
| 数据分布单一 | 参数写死,缺少权重设计 | 用CASE WHEN为字段增加比例分布 |
4.5 一个印象深刻的真实排查案例
有一次帮一个项目组造测试数据,表结构是典型的电商订单场景,有orders、order_items、users三张核心表。用户表要造200万用户,订单表要造500万订单,明细表要造1500万行。一开始我用了最简单的方案,挨个表执行循环插入,结果跑了一个多小时只完成了一小半,而且数据库CPU长期占满,其他同事连不上这个测试库。
我把脚本停下来,换了个思路:全程用INSERT INTO ... SELECT,第一批先把索引全部删掉,插入结束后统一重建。同时把随机数生成规则全部改为基于数字序列的确定性方案,时间字段用日期偏移量加行号取模的方式打散。重新执行后,用户表200万行大概用了两分钟,订单表500万行用了不到十分钟。重建索引反而成了最耗时的环节,但因为只做一次,整体体验好了太多。
这个案例让我总结出两条经验:第一,性能问题的根源往往不是单个SQL慢,而是选择了低效的造数策略;第二,造数脚本更像一次ETL任务,而不是简单的插入操作,要用批处理思维去设计,而不是逐行循环思维。如果你遇到类似问题,建议先算一下总行数和总字段数,如果确认是千万级,就提前做好分批和索引重建的计划。
5. 更真实一点的模拟数据:如何让假数据更像真数据
5.1 用规则的随机代替完全的随机
如果目标只是“有数据”,那随机字符串随便填。但如果目标是让前端页面看起来顺眼、让查询结果符合业务直觉,那就要让字段值符合业务规则。比如手机号字段,你不能生成一个以0开头的11位数字;邮箱字段,域名得有真实存在的后缀;姓名、地址这种字段,不能是user_123这种一看就是造出来的东西。
我常用的做法是准备一份候选数据池。比如造用户表时,姓氏从“赵钱孙李周吴郑王”等常见姓氏里取,名字从一组常用汉字里随机组合,或者更简单点,准备50个常用名然后随机拼接。地址字段准备好城市列表、街道列表、小区列表,按随机组合生成。手机号直接用13x、15x、18x的号段前缀加8位随机数字。这些候选数据池不需要很大,每个维度几十个候选值就够了,因为随机组合之后能产出的组合数已经非常多,不会出现重复。
这种“半规则随机”的做法还有一个好处:生成的测试数据可以直接用于演示、截图、用户体验验证。你给业务方展示一个订单列表页面时,数据里不会有乱码、不会出现超长字符串把布局撑坏,观感好很多。
5.2 业务口径的分布控制
业务口径的分布控制比你想的重要得多。想象一个场景:你要测试一个“近30天活跃用户”的报表接口。如果造数时没有控制登录时间字段,所有用户的活跃时间随机分布在过去一年里,那么“近30天活跃”这个筛选条件查出来的行数可能不到全表的3%,前端图表看起来稀稀拉拉,根本看不出趋势。
解决办法就是给关键字段加分布控制参数。拿登录时间举例,我先决定近30天内的用户要占总数量的60%,然后这个区间之内再均匀打散。SQL里用CASE WHEN RAND() < 0.6走“近30天”的分支,其余走更早的分支。类似的做法也适用在订单状态、金额区间、渠道来源这类业务字段上。
再拿订单金额来说,真实业务通常是大量小额订单,但你如果均匀分布,平均值两岸的行数大致相等,这跟真实数据差异极大。我常用的一种简单模型是把金额拆成几档:50元以下占40%,50到200元占30%,200到1000元占20%,1000元以上占10%。用CASE WHEN配合RAND()就能实现,代码量不大,但测试结论的参考价值高很多。
5.3 为测试场景定制特殊数据
有些测试场景需要特殊数据,这时候通用方案就不够用了。比如你要测一个分页查询在深分页场景下的性能,就需要造出几百万行历史数据,且时间全部集中在前几个月,保证当前页返回的结果很少。又比如你要测一个针对“已删除”状态的过滤逻辑,就需要造一批status='deleted'的数据,并且保证它们的deleted_at字段有值。
这种情况下我会额外写入一批标记数据。最简单的方式是单独准备几个SQL文件,专门造这种边缘数据,在基础数据造完之后执行。也别忘了给这批数据设置明显的业务标识,比如用户名以special_开头,方便后续清理和识别。造数脚本的整体思路是:基础数据批量生成,特殊数据单独定制,两者结合才能覆盖完整的测试场景。
6. 整理一份可直接上手的执行清单
6.1 造数前的准备清单
结合我自己的实操经验,每次造数前过一遍这个清单,能省掉大部分返工时间:
- 确认目标表结构和约束:列出主键、唯一键、外键、CHECK约束、非空字段、默认值、字段长度
- 统计表间依赖关系:明确插入顺序,主表先插、从表后插
- 规划数据量和数据分布:确定总行数、关键字段的比例分布、时间区间范围
- 准备候选数据池:姓名、手机号、地址、邮箱等字段的候选值,避免纯随机
- 确定批量大小:单批行数建议为5000到50000,千万级数据提前规划分批次
- 确认环境隔离:测试库独立,磁盘空间足够,数据库账号具备DDL权限
- 制定索引策略:造数期间删除索引或重建完再插入,大表先禁用外键检查
6.2 造数脚本的推荐结构
一份好的造数SQL文件,应该能做到“参数化、可复用、易排查”。我推荐按下面的结构组织:
- 顶部参数区:数据量、ID范围、日期范围、批量大小、状态分布比例
- 环境准备:禁用外键检查、删除目标表索引、修改日志刷盘参数
- 主数据生成:按表逐个执行INSERT INTO ... SELECT批量插入
- 索引重建:插完数据之后统一重建索引,更新统计信息
- 数据校验:执行SELECT COUNT(*)对比预期行数,抽查关键字段分布
- 环境恢复:恢复外键检查、恢复日志参数,确认数据可被查询
6.3 造数性能优化的参数速查
整理了一份参数速查表,你可能用得上:
| 数据库 | 关键参数 | 推荐值 | 用途 |
|---|---|---|---|
| MySQL | innodb_flush_log_at_trx_commit | 0或2(测试环境) | 减少日志刷盘频率 |
| MySQL | foreign_key_checks | 0(插入期间) | 跳过外键检查 |
| MySQL | unique_checks | 0(插入期间) | 跳过唯一约束二次校验 |
| MySQL | bulk_insert_buffer_size | 256M | 加速MyISAM批量插入 |
| PostgreSQL | synchronous_commit | off(测试环境) | 降低事务提交等待 |
| PostgreSQL | maintenance_work_mem | 512M~1G | 加速索引重建 |
| SqlServer | 恢复模式 | 简单(测试环境) | 降低日志空间占用 |
| SqlServer | MAXRECURSION | 0 | 解除递归CTE深度限制 |
以上参数都只建议在隔离的测试环境里调整。生产库或共享开发库动这些开关,后果可能很严重,轻则影响其他同事,重则造成数据丢失风险。
6.4 最后一步:校验你造出来的数据是不是合格的
数据造完别急着走。我会花几分钟跑几组查询,确认数据质量达到预期。最基本的校验是行数统计:SELECT COUNT(*)和目标值对比,差一截就说明插入过程中有部分批次失败了。然后是字段分布校验:拿订单状态举例,分别统计completed、pending、canceled的行数占比,看是否符合预设的70%、20%、10%比例。再就是抽样检查:随机看几行的完整数据,确认没有空值、没有超长字符串、没有乱码、日期字段没有超出预期范围。
还有一个容易被忽略的点:造完数据之后一定要更新统计信息。MySQL的ANALYZE TABLE、PG的ANALYZE、SqlServer的UPDATE STATISTICS,各跑一遍。否则优化器手里还是旧统计信息,执行计划可能基于错误判断走全表扫描,然后你以为是自己SQL写错了,其实是统计信息过期了。
7. 写在最后的一点个人体会
造数据这个东西,看起来是个体力活,但做久了你会发现它特别考验你对数据库的理解深度。同样一张表,不同人写出来的造数脚本,性能可能差几十倍;同样的数据量,不同字段生成规则,对测试结论的影响可能完全不一样。我在多次实战之后最大的感受是:先想清楚你要什么数据、怎么分布,再动手写SQL。脚本本身只是执行工具,真正决定数据质量的是你前期的规划和设计。
如果你现在正在为测试数据不足发愁,我建议你按这篇文章的思路先选一张核心表练手,跑通一个完整的造数流程,再慢慢扩展到其他表。大方向搞清楚了,后面就是熟练度的问题。等你把批量化造数的流程固化下来,再遇到需要造几百万条测试数据的任务,就会觉得这其实是件很有把握的事。