怎样生成有关联的测试数据

sjjt-218

在向用户推荐新的数据处理技术,特别是涉及性能优化的场景时,经常会碰到生成测试数据的需求。毕竟,新方案要经过验证才能提交,而优化过程也不是一次就做完的,需要多次不断的迭代改进,这就需要有一套好的测试数据才能实施。

用户常常也会提供一些例子数据,但一般不会很多。如果只是验证算法正确性,那用这些少量数据经常也可以了,但如果是验证性能,就还需要足够大的数据量才行。而用户的数据常常比较敏感,很多情况下不可能把全量数据提供出来,而且,数据量太大时也不合适搬来搬去。所以,最好还是能自己造出测试数据,特别地,还要根据用户提供的例子数据来造。


如果只是单个表(比如多维分析的场景),那还相对简单。把例子数据复制 n 遍到期望的规模,或者干脆随机生成,过程中注意对主键进行一些处理保证不重复(比如不断地加 1,具体手段视数据类型而定),大多数情况也够用。但如果涉及到多个关联表就麻烦了,因为运算中可能涉及 JOIN,简单复制例子数据,可能导致 JOIN 结果集和例子数据的规模相近,而完全随机生成则很可能就关联不上使得 JOIN 结果集是个空集,下一步的运算就会建立在一个虚假的小结果集上,严重误导性能测试的结果。

那么,该如何在多表情况下生成大规模测试数据时还能保证合理的关联性呢?

一. 分析数据结构与关联关系

说白了,就是画出 E-R 图,搞清各个表之间的引用关系,特别是要搞清谁是谁的外键。

我们以前在讲 JOIN 运算时说过(《JOIN 运算剖析》),数据库表的关联关系可以分成外键、同维、主子三种情况。传统的 E-R 图并没有这么明确的区分,但在这里我们就需要做这件事情了。根据关联字段是否是表的主键(或部分主键)就可以区分出来。同一个表同时是两个表的子表时,我们把这两个主表看成子表的外键表,而不作为主子表处理,这样能保证没有子表有多个主表。这个问题在讨论以前 JOIN 简化和提速时不需要涉及,但这里要提出来。

有时用户会明确给出数据结构和 E-R 图,那直接使用就可以了。有时候只会给一批用于运算 SQL 语句,这就要从其中的 JOIN 子句来分析,根据 JOIN 字段是否是表的主键(或部分主键)可以判断是哪种关联。如果发生这三种之外的情况(比如出现非主键和非主键 JOIN 的多对多情况),那多半是用户的 SQL 语句很错了,要明确指出来。

有些外键是隐含的关系,比如身份证号中有个日期和地区码,如果运算中有从身份证号提供这些部分信息的动作,那也需要把这些隐含的关联关系找出来,相当于把身证份号这样的字段拆成几段。日期本身也有类似的隐含关系,它本身是一个外键字段,但一般数据库中都很少会有一个日期表来作为它的指向表,这个也需要补充出来(在《JOIN 延伸:维度概念》中提到过假表的概念)。在单表生成测试数据时也有这类问题。

二. 排定次序

知道了关联关系后,就可以排定生成数据的次序了。

我们把同维表看成一个逻辑表一起处理,主子表中的子表则依附于主表先隐藏起来,子表的其它外键表也被视为主表的外键表,等主表处理完再来处理子表。这样,关联关系中就只剩外键表。

现在,我们先给每个表标上数字 1。然后,对于每个表,如果它有的所有外键表的标号最大值为 n,则把它本身的标号改成 n+1,反复执行此动作直到所有标号不再变动为止。

这个动作能执行结束的前提是 E-R 图中没有有向圈(A 表是 B 表的外键表时从 B 表画一条指向 A 表有向边),这个要求对绝大多数数据库结构来讲都是满足的。有时会发生某个表自己是自己外键表的现象,这时在标号时忽略这个外键关系就可以了。但如果发生涉及多个表的有向圈时的情况就复杂多了,我们这里限于篇幅不讨论这种情况了,毕竟也非常罕见。

三. 按次序生成测试数据

现在,根据表的标号从小到大的次序去生成数据就行了。

在为标号为 n+1 的表生成数据时,它引用的外键表的标号都不超过 n,已经被生成了,则它的外键值从这些已经生成的表的主键中随机取就可以了。

同维表可以一起生成,子表则等待主表完成后再生成,如果有多个子表之间还有引用关系(很罕见了,多个子表都较为少见),也可以用上面的办法再排出次序来生成。