La mejor forma de probar consultas SQL [cerrado]

109

Me he encontrado con un problema en el que seguimos teniendo consultas SQL complejas con errores. Básicamente, esto da como resultado el envío de correo a los clientes incorrectos y otros 'problemas' como ese.

¿Cuál es la experiencia de todos con la creación de consultas SQL como esa? Estamos creando nuevas cohortes de datos cada dos semanas.

Así que aquí están algunos de mis pensamientos y las limitaciones de ellos:

  • Creación de datos de prueba Si bien esto demostraría que tenemos todos los datos correctos, no impone la exclusión de anomalías en la producción. Esos son datos que se considerarían incorrectos hoy, pero que pueden haber sido correctos hace 10 años; no estaba documentado y, por lo tanto, solo lo conocemos después de que se extraen los datos.

  • Crear diagramas de Venn y mapas de datos Esta parece ser una forma sólida de probar el diseño de una consulta, sin embargo, no garantiza que la implementación sea correcta. Hace que los desarrolladores planifiquen con anticipación y piensen en lo que está sucediendo mientras escriben.

Gracias por cualquier comentario que pueda dar a mi problema.

Bluephlame
fuente

Respuestas:

164

No escribirías una aplicación con funciones de 200 líneas de largo. Descompondría esas funciones largas en funciones más pequeñas, cada una con una única responsabilidad claramente definida.

¿Por qué escribir su SQL así?

Descomponga sus consultas, al igual que descompone sus funciones. Esto los hace más cortos, más simples, más fáciles de comprender, más fáciles de probar y más fáciles de refactorizar. Y le permite agregar "calces" entre ellos y "envoltorios" alrededor de ellos, tal como lo hace en el código de procedimiento.

¿Cómo haces esto? Al convertir cada cosa significativa que hace una consulta en una vista. Luego, redacta consultas más complejas a partir de estas vistas más simples, del mismo modo que compone funciones más complejas a partir de funciones más primitivas.

Y lo mejor es que, para la mayoría de las composiciones de vistas, obtendrá exactamente el mismo rendimiento de su RDBMS. (Para algunos no lo hará; ¿y qué? La optimización prematura es la raíz de todos los males. Primero codifique correctamente y luego optimice si es necesario).

A continuación, se muestra un ejemplo del uso de varias vistas para descomponer una consulta complicada.

En el ejemplo, debido a que cada vista agrega solo una transformación, cada una se puede probar de forma independiente para encontrar errores, y las pruebas son simples.

Aquí está la tabla base en el ejemplo:

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

Esta tabla es defectuosa porque usa dos columnas, mes y año, para representar un dato, un mes absoluto. Aquí está nuestra especificación para la nueva columna calculada:

Lo haremos como una transformación lineal, de modo que se ordene igual que (año, mes), y tal que para cualquier tupla (año, mes) hay un único valor, y todos los valores son consecutivos:

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

Ahora lo que tenemos que probar es inherente a nuestra especificación, es decir, que para cualquier tupla (año, mes), hay uno y solo uno (mes_absoluto), y que (mes_absoluta) s son consecutivos. Escribamos algunas pruebas.

Nuestra prueba será una selectconsulta SQL , con la siguiente estructura: un nombre de prueba y una declaración de caso catenados juntos. El nombre de la prueba es solo una cadena arbitraria. La declaración del caso es solo case whendeclaraciones de prueba then 'passed' else 'failed' end.

Las declaraciones de prueba serán solo selecciones de SQL (subconsultas) que deben ser verdaderas para que la prueba pase.

Aquí está nuestra primera prueba:

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

Ejecutar esa consulta produce este resultado: For every (year, month) there is one and only one (absolute_month): passed

Siempre que haya suficientes datos de prueba en month_value, esta prueba funciona.

También podemos agregar una prueba para obtener suficientes datos de prueba:

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

Ahora probemos si es consecutivo:

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

Ahora coloquemos nuestras pruebas, que son solo consultas, en un archivo y ejecutemos ese script en la base de datos. De hecho, si almacenamos nuestras definiciones de vista en un script (o scripts, recomiendo un archivo por vistas relacionadas) para ejecutarlo en la base de datos, podemos agregar nuestras pruebas para cada vista al mismo script, de modo que el acto de (re -) la creación de nuestra vista también ejecuta las pruebas de la vista. De esa manera, ambos obtenemos pruebas de regresión cuando volvemos a crear vistas y, cuando la creación de la vista se ejecuta en producción, la vista también se probará en producción.

tpdi
fuente
27
Esta es la primera vez que veo código limpio y pruebas unitarias en sql, estoy feliz por el día :)
Maxime ARNSTAMM
1
increíbles hacks de sql
CodeFarmer
13
Esto es genial, pero ¿por qué usar nombres de una letra para columnas y nombres de vista apenas legibles? ¿Por qué SQL debería ser menos autodocumentado o legible que Python?
snl
1
Increíble explicación de algo útil que nunca miré en el mundo SQL / DB. También me encanta la forma en que probó la base de datos aquí.
Jackstine
Solo como advertencia, he visto vistas SQL que se unen a vistas SQL funcionan muy mal en PostgreSQL. Sin embargo, he usado esta técnica de manera efectiva con M $ SQL.
Ben Liyanage
6

Cree una base de datos del sistema de prueba que pueda recargar tantas veces como desee. Cargue sus datos o cree sus datos y guárdelos. Produce una forma sencilla de recargarlo. Adjunte su sistema de desarrollo a esa base de datos y valide su código antes de pasar a producción. Patéate cada vez que consigas dejar que un problema entre en producción. Cree un conjunto de pruebas para verificar problemas conocidos y hacer crecer su conjunto de pruebas con el tiempo.

ojblass
fuente
4

Es posible que desee verificar DbUnit , por lo que puede intentar escribir pruebas unitarias para sus programas con un conjunto fijo de datos. De esa forma, debería poder escribir consultas con resultados más o menos predecibles.

La otra cosa que podría querer hacer es perfilar su pila de ejecución de SQL Server y averiguar si todas las consultas son realmente las correctas, por ejemplo, si está utilizando solo una consulta que devuelve resultados correctos e incorrectos, entonces claramente la consulta es used está en cuestión, pero ¿qué pasa si su aplicación envía diferentes consultas en diferentes puntos del código?

Cualquier intento de corregir su consulta sería inútil ... las consultas deshonestas podrían ser las que generen resultados incorrectos de todos modos.

Jon Limjap
fuente
2

Re: 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  

Tenga en cuenta que esto solo verifica que los valores de am para meses consecutivos sean consecutivos, no que existan datos consecutivos (que es probablemente lo que pretendía inicialmente). Esto siempre pasará si ninguno de sus datos de origen es consecutivo (por ejemplo, solo tiene meses pares), incluso si su cálculo de am está totalmente equivocado.

¿También me falta algo o la segunda mitad de esa cláusula ON aumenta el valor del mes incorrecto? (es decir, comprueba que 12/2011 viene después del 1/2010)

Lo que es peor, si mal no recuerdo, SQL Server al menos le permite menos de 10 niveles de vistas antes de que el optimizador lance sus manos virtuales al aire y comience a hacer escaneos completos de tablas en cada solicitud, así que no exagere con este enfoque.

¡Recuerde probar a fondo sus casos de prueba!

De lo contrario, crear un conjunto muy amplio de datos para abarcar la mayoría o todas las formas posibles de entradas, utilizando SqlUnit o DbUnit o cualquier otra * Unidad para automatizar la verificación de los resultados esperados contra esos datos, y revisarlos, mantenerlos y actualizarlos según sea necesario, generalmente parece ser el camino a seguir.

Marte el Infomaje
fuente