测试SQL查询的最佳方法


109

我遇到了一个问题,就是我们不断使复杂的SQL查询出错。从本质上讲,这导致将邮件发送给错误的客户以及类似的其他“问题”。

每个人创建这样的SQL查询的经验是什么?我们每隔一周就会创建一组新的数据。

所以这是我的一些想法及其局限性:

  • 创建测试数据尽管这将证明我们拥有所有正确的数据,但不会强制排除生产中的异常情况。这些数据今天被认为是错误的,但可能在10年前是正确的。它没有记录,因此我们仅在提取数据后才知道。

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

感谢您的输入,可以解决我的问题。

Answers:


164

您不会编写功能长度为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测试语句then 'passed' else 'failed' end

测试语句只是要通过测试的SQL选择(子查询)。

这是我们的第一个测试:

--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.

运行该查询将产生以下结果: For every (year, month) there is one and only one (absolute_month): passed

只要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 );

现在让我们将只是查询的测试放入文件中,然后对数据库运行该脚本。确实,如果我们将视图定义存储在要针对数据库运行的脚本(或一个脚本,我建议每个相关视图一个文件)中,则可以将每个视图的测试添加到同一脚本中,以便( -)创建视图也将运行视图的测试。这样,我们都可以在重新创建视图时进行回归测试,并且当视图创建针对生产运行时,该视图也将在生产中进行测试。


27
这是我第一次在sql中看到简洁的代码和单元测试,今天过
得很

1
很棒的sql
骇客

13
很好,但是为什么要对列使用一个字母名称,而视图名称却难以辨认?为什么SQL的自文档性或可读性不如Python?
snl

1
很棒的解释,对于我在SQL / DB世界中从未见过的有用的东西。我也喜欢您在这里测试数据库的方式。
Jackstine

作为警告,我已经看到加入sql视图的sql视图在PostgreSQL上的表现非常差。我已经在M $ SQL中有效地使用了此技术。
Ben Liyanage,

6

创建一个测试系统数据库,您可以根据需要多次重新加载它。加载数据或创建数据并保存。产生一种简单的方法来重新加载它。将开发系统附加到该数据库,并在投入生产之前验证您的代码。每当您设法使问题进入生产阶段时,都要踢自己。创建一组测试以验证已知问题并随着时间的推移扩展您的测试套件。


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值是否连续,而不检查是否存在连续数据(这可能是您最初打算的)。如果您的源数据都不是连续的(例如,您只有偶数月),即使您的计算完全不进行,这也将始终通过。

我是否还在丢失某些内容,或者该ON子句的后半部分是否颠倒了错误的月份值?(即检查12/2011是否在1/2010之后)

更糟糕的是,如果我没记错的话,SQL Server至少允许您少于10个级别的视图,然后优化器将其虚拟手投入空中,并开始对每个请求进行全表扫描,因此不要过度使用这种方法。

记住要测试一下测试用例!

否则,创建一个很宽的数据集涵盖大部分或所有可能的形式输入,使用SqlUnit或DbUnit的或任何其他*单位自动检查针对该数据预期的结果,并审查,维护和更新其在必要时通常似乎是要走的路。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.