Meilleur moyen de tester les requêtes SQL [fermé]

109

J'ai rencontré un problème dans lequel nous continuons à avoir des requêtes SQL complexes avec des erreurs. Essentiellement, cela se traduit par l'envoi de courrier aux clients incorrects et d'autres «problèmes» du genre.

Quelle est l'expérience de chacun avec la création de requêtes SQL comme ça? Nous créons de nouvelles cohortes de données toutes les deux semaines.

Voici donc quelques-unes de mes pensées et leurs limites:

  • Création de données de test Si cela prouverait que nous disposons de toutes les données correctes, cela n'impose pas l'exclusion des anomalies de production. Ce sont des données qui seraient considérées comme erronées aujourd'hui mais qui étaient peut-être exactes il y a 10 ans; il n'a pas été documenté et nous ne le savons donc qu'après l'extraction des données.

  • Créer des diagrammes de Venn et des cartes de données Cela semble être un moyen solide de tester la conception d'une requête, mais cela ne garantit pas que l'implémentation est correcte. Cela permet aux développeurs de planifier à l'avance et de réfléchir à ce qui se passe pendant qu'ils écrivent.

Merci pour toute contribution que vous pouvez apporter à mon problème.

Bluephlame
la source

Réponses:

164

Vous n'écririez pas une application avec des fonctions de 200 lignes. Vous décomposeriez ces longues fonctions en fonctions plus petites, chacune avec une seule responsabilité clairement définie.

Pourquoi écrire votre SQL comme ça?

Décomposez vos requêtes, tout comme vous décomposez vos fonctions. Cela les rend plus courts, plus simples, plus faciles à comprendre, plus faciles à tester , plus faciles à refactoriser. Et cela vous permet d'ajouter des "shims" entre eux, et des "wrappers" autour d'eux, comme vous le faites dans le code procédural.

Comment est-ce que tu fais ça? En transformant chaque chose importante d'une requête en vue. Ensuite, vous composez des requêtes plus complexes à partir de ces vues plus simples, tout comme vous composez des fonctions plus complexes à partir de fonctions plus primitives.

Et ce qui est formidable, c'est que pour la plupart des compositions de vues, vous obtiendrez exactement les mêmes performances de votre SGBDR. (Pour certains, vous ne le ferez pas; et alors? L'optimisation prématurée est la racine de tous les maux. Commencez par coder correctement, puis optimisez si vous en avez besoin.)

Voici un exemple d'utilisation de plusieurs vues pour décomposer une requête complexe.

Dans l'exemple, étant donné que chaque vue n'ajoute qu'une seule transformation, chacune peut être testée indépendamment pour trouver des erreurs, et les tests sont simples.

Voici la table de base dans l'exemple:

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

Ce tableau est imparfait, car il utilise deux colonnes, mois et année, pour représenter une donnée, un mois absolu. Voici nos spécifications pour la nouvelle colonne calculée:

Nous ferons cela comme une transformation linéaire, de sorte qu'elle trie de la même manière que (année, mois), et de telle sorte que pour tout tuple (année, mois), il y ait une et seule valeur, et toutes les valeurs sont consécutives:

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

Maintenant, ce que nous devons tester est inhérent à notre spécification, à savoir que pour tout tuple (année, mois), il y a un et un seul (mois_absolu), et que les (mois_absolus) sont consécutifs. Écrivons quelques tests.

Notre test sera une selectrequête SQL , avec la structure suivante: un nom de test et une instruction case caténés ensemble. Le nom du test est simplement une chaîne arbitraire. L'instruction case n'est que case whendes instructions de test then 'passed' else 'failed' end.

Les instructions de test ne seront que des sélections SQL (sous-requêtes) qui doivent être vraies pour que le test réussisse.

Voici notre premier test:

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

L'exécution de cette requête produit ce résultat: For every (year, month) there is one and only one (absolute_month): passed

Tant qu'il y a suffisamment de données de test dans month_value, ce test fonctionne.

Nous pouvons également ajouter un test pour des données de test suffisantes:

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

Maintenant, testons c'est consécutif:

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

Maintenant, mettons nos tests, qui ne sont que des requêtes, dans un fichier, et exécutons ce script sur la base de données. En effet, si nous stockons nos définitions de vue dans un script (ou des scripts, je recommande un fichier par vues associées) à exécuter sur la base de données, nous pouvons ajouter nos tests pour chaque vue au même script, de sorte que l'acte de (re -) la création de notre vue exécute également les tests de la vue. De cette façon, nous obtenons tous les deux des tests de régression lorsque nous recréons des vues et, lorsque la création de la vue s'exécute contre la production, la vue sera également testée en production.

tpdi
la source
27
C'est la première fois que je vois du code propre et des tests unitaires en SQL, je suis heureux pour la journée :)
Maxime ARNSTAMM
1
hacks sql géniaux
CodeFarmer
13
C'est génial, mais pourquoi utiliser des noms d'une seule lettre pour les colonnes et les noms de vues à peine lisibles? Pourquoi SQL devrait-il être moins auto-documenté ou lisible que Python?
snl
1
Explication géniale pour quelque chose d'utile que je n'ai jamais regardé dans le monde SQL / DB. J'adore également la façon dont vous avez testé la base de données ici.
Jackstine
Juste à titre d'avertissement, j'ai vu des vues sql qui se rejoignent sur des vues SQL fonctionnent très mal sur PostgreSQL. J'ai cependant utilisé cette technique efficacement avec M $ SQL.
Ben Liyanage
6

Créez une base de données du système de test que vous pouvez recharger aussi souvent que vous le souhaitez. Chargez vos données ou créez vos données et enregistrez-les. Créez un moyen simple de le recharger. Attachez votre système de développement à cette base de données et validez votre code avant de passer à la production. Donnez-vous un coup de pied chaque fois que vous parvenez à laisser un problème entrer en production. Créez une suite de tests pour vérifier les problèmes connus et développer votre suite de tests au fil du temps.

ojblass
la source
4

Vous voudrez peut-être vérifier DbUnit , vous pouvez donc essayer d'écrire des tests unitaires pour vos programmes avec un ensemble fixe de données. De cette façon, vous devriez être capable d'écrire des requêtes avec des résultats plus ou moins prévisibles.

L'autre chose que vous voudrez peut-être faire est de profiler votre pile d'exécution SQL Server et de savoir si toutes les requêtes sont effectivement les bonnes, par exemple, si vous utilisez une seule requête qui renvoie à la fois des résultats corrects et incorrects, alors clairement la requête est utilisé est en question, mais qu'en est-il si votre application envoie différentes requêtes à différents points du code?

Toute tentative de corriger votre requête serait alors futile ... les requêtes non fiables pourraient encore être celles qui déclenchent les mauvais résultats de toute façon.

Jon Limjap
la source
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  

Notez que cela vérifie uniquement que les valeurs am pour des mois consécutifs seront consécutives, et non que des données consécutives existent (ce qui est probablement ce que vous vouliez initialement). Cela passera toujours si aucune de vos données sources n'est consécutive (par exemple, vous n'avez que des mois pairs), même si votre calcul AM est totalement désactivé.

Est-ce que je manque également quelque chose, ou est-ce que la seconde moitié de cette clause ON remplace la mauvaise valeur du mois? (c'est à dire vérifie que 12/2011 vient après 1/2010)

Ce qui est pire, si je me souviens bien, SQL Server vous permet au moins moins de 10 niveaux de vues avant que l'optimiseur ne jette ses mains virtuelles en l'air et ne commence à effectuer des analyses complètes de la table à chaque demande, alors ne sur-faites pas cette approche.

N'oubliez pas de tester à fond vos cas de test!

Sinon, créer un très large ensemble de données pour englober la plupart ou toutes les formes d'entrées possibles, en utilisant SqlUnit ou DbUnit ou toute autre unité * pour automatiser la vérification des résultats attendus par rapport à ces données, et réviser, les maintenir et les mettre à jour si nécessaire semble généralement être le chemin à parcourir.

Mars l'Infomage
la source