测试SQL查询的最佳方法

122

我遇到了一个问题,我们一直在使用复杂的SQL查询出现错误。这实际上导致向错误的客户发送邮件等“问题”。

大家在创建这样的SQL查询方面有什么经验?我们每两周就要创建新的数据集。

以下是我的一些想法和其中的限制:

  • 创建测试数据 虽然这可以证明我们拥有所有正确的数据,但它并不能强制在生产环境中排除异常情况。即使今天被认为是错误的数据,在10年前可能是正确的;它没有记录,因此只有在提取数据后才会发现。

  • 创建Venn图和数据映射 这似乎是测试查询设计的可靠方法,但并不能保证实现是正确的。 它让开发人员提前规划并在编写时思考正在发生什么。

感谢您能为我的问题提供任何帮助。

4个回答

193

你不会写一个有200行的函数应用程序,你会将那些长函数分解为更小的函数,每个函数都有一个明确定义的单一职责。

为什么不像这样编写SQL呢?

像分解函数一样分解查询语句, 这使它们更短、更简单、更容易理解、更容易测试和重构。并且允许你在它们之间添加"垫片"和"包装器",就像在过程性代码中所做的那样。

如何实现这一点?通过将每个重要的查询操作变成视图。然后你可以像组合原始函数一样,从这些简单的视图组合出更复杂的查询。

最好的事情是,对于大多数视图组合,您将从您的RDBMS获得完全相同的性能。(对于一些视图组合,您可能不会得到相同的性能;那又怎样?过早优化是万恶之源。首先正确编码,如果需要再进行优化。)

这里有一个使用几个视图分解复杂查询的示例。

在这个例子中,因为每个视图只添加了一种转换,所以可以独立测试每个视图以查找错误,并且测试非常简单。

这是示例中的基本表:

create table month_value( 
    eid int not null, month int, year int,  value int );

这张表格存在缺陷,因为它使用了两列——月份和年份——来代表一个数据,即一个绝对月份。以下是我们针对新的计算列的规定:

我们将进行线性转换,使其与 (年份, 月份) 相同,并且对于任何 (年份, 月份) 元组只有一个值,所有值都是连续的:

create view cm_absolute_month as 
select *, year * 12 + month as absolute_month from month_value;

现在我们需要测试的内容与我们的规范有关,即对于任何元组(年份、月份),都有一个且仅有一个(absolute_month),而且(absolute_month)是连续的。让我们编写一些测试。

我们的测试将是一个 SQL select 查询,具有以下结构:测试名称和一个拼接在一起的 case 语句。测试名称只是一个任意的字符串。case 语句只是一个 case when test statements then 'passed' else 'failed' end 的条件语句。

测试语句将只是必须为测试通过而为真的 SQL select(子查询)。

这是我们的第一个测试:

--a select statement that catenates the test name and the case statement
select concat( 
-- the test name
'For every (year, month) there is one and only one (absolute_month): ', 
-- the case statement
   case when 
-- one or more subqueries
-- in this case, an expected value and an actual value 
-- that must be equal for the test to pass
  ( select count(distinct year, month) from month_value) 
  --expected value,
  = ( select count(distinct absolute_month) from cm_absolute_month)  
  -- actual value
  -- the then and else branches of the case statement
  then 'passed' else 'failed' end
  -- close the concat function and terminate the query 
  ); 
  -- test result.

运行该查询会产生以下结果:对于每个(年份,月份),都存在一个且仅存在一个(绝对月份):通过

只要month_value中有足够的测试数据,此测试就可以工作。

我们也可以为足够的测试数据添加一个测试:

select concat( 'Sufficient and sufficiently varied month_value test data: ',
   case when 
      ( select count(distinct year, month) from month_value) > 10
  and ( select count(distinct year) from month_value) > 3
  and ... more tests 
  then 'passed' else 'failed' end );

现在让我们测试它的连续性:

select concat( '(absolute_month)s are consecutive: ',
case when ( select count(*) from cm_absolute_month a join cm_absolute_month b 
on (     (a.month + 1 = b.month and a.year = b.year) 
      or (a.month = 12 and b.month = 1 and a.year + 1 = b.year) )  
where a.absolute_month + 1 <> b.absolute_month ) = 0 
then 'passed' else 'failed' end );

现在让我们将测试(仅为查询)放入一个文件中,并对该脚本运行数据库。如果我们将视图定义存储在脚本(或多个脚本,我建议每个相关视图一个文件)中以针对数据库运行,则可以将每个视图的测试添加到同一脚本中,从而重新创建视图时同时运行视图的测试。这样,当我们重新创建视图时,就可以得到回归测试,而当视图创建针对生产运行时,该视图也将在生产中进行测试。


35
这是我第一次看到 SQL 中的清晰代码和单元测试,今天真是让我开心 :) - Maxime ARNSTAMM
16
为什么要使用单个字母作为列名和几乎无法辨认的视图名称呢?SQL为什么应该比Python更难以自我记录或阅读? - snl
1
这是一个关于SQL/DB世界中我从未关注过的有用内容的精彩解释。此外,我也很喜欢你在这里测试数据库的方式。 - Jackstine
只是提醒一下,我曾经在PostgreSQL上看到过连接SQL视图的SQL视图性能非常差的情况。然而,我在M$ SQL上成功地使用了这种技术。 - Ben Liyanage
虽然这是个不错的想法,但关于视图加入视图的评论感觉像是Postgres的反模式。特别是构建视图层级结构可能会使事情变得混乱快速。更不用说需要管理更多的代码和复杂性了。 - rocksteady
显示剩余2条评论

8
创建一个测试系统数据库,您可以随意重载它。加载数据或创建数据并保存它。提供一种简单的方法来重新加载它。将开发系统连接到该数据库,并在进入生产之前验证您的代码。每次成功将问题引入生产环境时,请反思自己。创建一系列测试以验证已知问题,并随着时间的推移扩展您的测试套件。

4
您可能需要查看DbUnit,以便您可以尝试使用固定数据集编写单元测试程序。这样,您应该能够编写具有更可预测结果的查询。

另一件事是对SQL Server执行堆栈进行分析,找出所有查询是否确实正确,例如,如果您只使用一个查询返回正确和不正确的结果,那么显然正在使用的查询存在问题,但是如果您的应用程序在代码的不同点发送不同的查询呢?

任何尝试修复查询的努力都将是徒劳的...恶意查询仍然可能是触发错误结果的查询。


2

回复:tpdi

case when ( select count(*) from cm_abs_month a join cm_abs_month b  
on (( a.m + 1 = b.m and a.y = b.y) or (a.m = 12 and b.m = 1 and a.y + 1 = b.y) )   
where a.am + 1 <> b.am ) = 0  

请注意,这只检查连续月份的am值是否连续,而不是检查连续数据是否存在(这可能是您最初想要的)。如果您的源数据没有连续的数据(例如,您只有偶数月份),即使您的am计算完全错误,这也始终会通过。
此外,我是否漏掉了什么,或者ON子句的后半部分是否会提高错误的月份值?(即检查12/2011是否在1/2010之后)
更糟糕的是,如果我记得正确,SQL Server至少允许您在优化器对每个请求进行完整表扫描之前拥有不到10个级别的视图,因此不要过度使用此方法。
记得好好测试你的测试用例!
否则,创建一个非常广泛的数据集来涵盖大多数或所有可能的输入形式,使用SqlUnit或DbUnit或任何其他*Unit自动检查预期结果,并根据需要进行审核、维护和更新通常是比较好的方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接