news 2026/10/23 17:47:15

数据库测试数据批量造数实战:SQL脚本、性能优化与常见坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据库测试数据批量造数实战:SQL脚本、性能优化与常见坑

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递归已经够用了,不必为了炫技去写这种系统表交叉连接。

三种数据库的写法各有特色,我整理了一个对比表方便你选型:

数据库推荐方案单次推荐数据量适用场景
PostgreSQLgenerate_series + INSERT SELECT10万~100万中大规模数据,语法简洁
MySQL 8.0+数字辅助表 + INSERT SELECT5万~20万通用场景,性能稳定
MySQL 老版本存储过程批量循环1万~5万表结构简单,仅做功能联调
SqlServer递归CTE + MAXRECURSION 05万~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可以暂时用简单恢复模式。调整之前要确认这是独立测试环境,生产库千万别动这个开关。

我整理了一个优化清单,按优先顺序排列:

  1. 使用INSERT INTO ... SELECT替代逐行插入
  2. 数据量超过十万时,删除或禁用目标表索引
  3. 关闭外键检查(MySQL的SET FOREIGN_KEY_CHECKS=0)
  4. 临时降低日志刷盘强度(仅限隔离的测试环境)
  5. 分批提交,每批控制在5万行以内
  6. 如果目标表有自增主键,避免过小步长,减少页分裂和锁竞争

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文件,应该能做到“参数化、可复用、易排查”。我推荐按下面的结构组织:

  1. 顶部参数区:数据量、ID范围、日期范围、批量大小、状态分布比例
  2. 环境准备:禁用外键检查、删除目标表索引、修改日志刷盘参数
  3. 主数据生成:按表逐个执行INSERT INTO ... SELECT批量插入
  4. 索引重建:插完数据之后统一重建索引,更新统计信息
  5. 数据校验:执行SELECT COUNT(*)对比预期行数,抽查关键字段分布
  6. 环境恢复:恢复外键检查、恢复日志参数,确认数据可被查询

6.3 造数性能优化的参数速查

整理了一份参数速查表,你可能用得上:

数据库关键参数推荐值用途
MySQLinnodb_flush_log_at_trx_commit0或2(测试环境)减少日志刷盘频率
MySQLforeign_key_checks0(插入期间)跳过外键检查
MySQLunique_checks0(插入期间)跳过唯一约束二次校验
MySQLbulk_insert_buffer_size256M加速MyISAM批量插入
PostgreSQLsynchronous_commitoff(测试环境)降低事务提交等待
PostgreSQLmaintenance_work_mem512M~1G加速索引重建
SqlServer恢复模式简单(测试环境)降低日志空间占用
SqlServerMAXRECURSION0解除递归CTE深度限制

以上参数都只建议在隔离的测试环境里调整。生产库或共享开发库动这些开关,后果可能很严重,轻则影响其他同事,重则造成数据丢失风险。

6.4 最后一步:校验你造出来的数据是不是合格的

数据造完别急着走。我会花几分钟跑几组查询,确认数据质量达到预期。最基本的校验是行数统计:SELECT COUNT(*)和目标值对比,差一截就说明插入过程中有部分批次失败了。然后是字段分布校验:拿订单状态举例,分别统计completed、pending、canceled的行数占比,看是否符合预设的70%、20%、10%比例。再就是抽样检查:随机看几行的完整数据,确认没有空值、没有超长字符串、没有乱码、日期字段没有超出预期范围。

还有一个容易被忽略的点:造完数据之后一定要更新统计信息。MySQL的ANALYZE TABLE、PG的ANALYZE、SqlServer的UPDATE STATISTICS,各跑一遍。否则优化器手里还是旧统计信息,执行计划可能基于错误判断走全表扫描,然后你以为是自己SQL写错了,其实是统计信息过期了。

7. 写在最后的一点个人体会

造数据这个东西,看起来是个体力活,但做久了你会发现它特别考验你对数据库的理解深度。同样一张表,不同人写出来的造数脚本,性能可能差几十倍;同样的数据量,不同字段生成规则,对测试结论的影响可能完全不一样。我在多次实战之后最大的感受是:先想清楚你要什么数据、怎么分布,再动手写SQL。脚本本身只是执行工具,真正决定数据质量的是你前期的规划和设计。

如果你现在正在为测试数据不足发愁,我建议你按这篇文章的思路先选一张核心表练手,跑通一个完整的造数流程,再慢慢扩展到其他表。大方向搞清楚了,后面就是熟练度的问题。等你把批量化造数的流程固化下来,再遇到需要造几百万条测试数据的任务,就会觉得这其实是件很有把握的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 23:06:15

我在赫兹号卡网泡了半个月,终于选到一张不踩坑的流量卡

说出来你们可能不信&#xff0c;我之前办流量卡&#xff0c;被坑过不止一次。有一次在某平台看到“19元200G”的广告&#xff0c;心动下单&#xff0c;收到卡激活后才发现&#xff0c;通用流量只有1个G。刷了两天抖音&#xff0c;通用流量就见了底&#xff0c;后面全靠日租宝续…

作者头像 李华
网站建设 2026/10/23 17:46:19

PFC2D5.0热力耦合岩石热损伤模拟:空心圆颗粒案例代码与避坑指南

简介&#xff1a;这份资源面向岩石力学与地下工程方向的科研人员和研究生&#xff0c;聚焦PFC2D5.0颗粒流离散元在热力耦合条件下的岩石热损伤模拟&#xff0c;重点解析空心圆盘岩样轴心受热损伤的案例代码。内容围绕材料属性定义、模型初始化、热载荷施加与结果分析等环节展开…

作者头像 李华
网站建设 2026/10/23 13:34:15

Kong 类型恢复详解:LLM 如何从字段访问中合成完整结构体

【免费下载链接】kong The worlds first agentic reverse engineer. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/kong6/kong 点击查看 免费下载 Kong 是一款基于 LLM 的 Agentic 逆向工程框架&#xff0c;自动驱动 Ghidra 分析 stripped 二进制。它的**类型恢复&am…

作者头像 李华
网站建设 2026/10/20 16:17:42

这几天都在聊换季的傍晚,我更想推荐《失真少年》

当睡前还不愿关灯的人走到夜里耳机刚戴上的那几秒&#xff0c;《失真少年》往往会比空泛安慰更先开口——不是要你热闹起来&#xff0c;而是把说不清的那截情绪&#xff0c;轻轻按进旋律里。凌晨三点屏幕像一面霜。添火乐队《失真少年》「凌晨三点的屏幕像一面霜&#xff0f;弹…

作者头像 李华
网站建设 2026/10/22 22:49:10

WSL2部署Cadence Virtuoso IC25.1:Ubuntu 26.04排障实录

干IC设计的朋友应该都有过这种经历&#xff1a;手头这套Cadence Virtuoso IC25.1的设计环境&#xff0c;平时在实验室的Linux工作站上跑得好好的&#xff0c;哪天临时想在Windows笔记本上把版图、电路仿真顺手看一下&#xff0c;却发现一套环境两天装不好。尤其是版本越新的套件…

作者头像 李华