Najlepszy sposób na testowanie zapytań SQL [zamknięty]

109

Napotkałem problem polegający na tym, że złożone zapytania SQL zawierają błędy. Zasadniczo powoduje to wysyłanie poczty do niewłaściwych klientów i inne tego typu „problemy”.

Jakie jest doświadczenie każdego z tworzeniem takich zapytań SQL? Co dwa tygodnie tworzymy nowe kohorty danych.

Oto kilka moich przemyśleń i ich ograniczeń:

  • Tworzenie danych testowych Chociaż dowodziłoby to, że mamy wszystkie prawidłowe dane, nie wymusza wykluczenia anomalii w produkcji. Są to dane, które dziś byłyby uważane za błędne, ale 10 lat temu mogły być poprawne; nie zostało to udokumentowane i dlatego wiemy o tym dopiero po wyodrębnieniu danych.

  • Tworzenie diagramów Venna i map danych Wydaje się, że jest to solidny sposób testowania projektu zapytania, jednak nie gwarantuje, że implementacja jest poprawna. To sprawia, że ​​programiści planują z wyprzedzeniem i myślą o tym, co się dzieje, gdy piszą.

Dziękuję za wszelkie uwagi, które możesz wnieść do mojego problemu.

Bluephlame
źródło

Odpowiedzi:

164

Nie napisałbyś aplikacji z funkcjami o długości 200 linii. Rozłożyłbyś te długie funkcje na mniejsze funkcje, z których każda ma jedną jasno określoną odpowiedzialność.

Po co pisać w ten sposób swój SQL?

Dekomponuj swoje zapytania, tak jak rozkładasz swoje funkcje. To sprawia, że ​​są krótsze, prostsze, łatwiejsze do zrozumienia, łatwiejsze do przetestowania , łatwiejsze do refaktoryzacji. Pozwala też na dodawanie „podkładek” między nimi i „otoków” wokół nich, tak jak w kodzie proceduralnym.

Jak Ty to robisz? Dokonując każdej istotnej rzeczy, jaką zapytanie robi w widoku. Potem komponować bardziej złożonych zapytań z tych prostszych poglądów, tak jak komponować bardziej złożone funkcje z funkcjami bardziej prymitywnych.

A wspaniałe jest to, że w przypadku większości kompozycji widoków uzyskasz dokładnie taką samą wydajność ze swojego RDBMS. (Dla niektórych tego nie zrobisz; i co z tego? Przedwczesna optymalizacja jest źródłem wszelkiego zła. Najpierw koduj poprawnie, a następnie optymalizuj, jeśli zajdzie taka potrzeba).

Oto przykład użycia kilku widoków do dekompozycji skomplikowanego zapytania.

W tym przykładzie, ponieważ każdy widok dodaje tylko jedną transformację, każdą można niezależnie przetestować w celu znalezienia błędów, a testy są proste.

Oto tabela podstawowa w przykładzie:

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

Ta tabela jest wadliwa, ponieważ używa dwóch kolumn, miesiąca i roku, do reprezentowania jednej wartości odniesienia, bezwzględnego miesiąca. Oto nasza specyfikacja nowej kolumny obliczeniowej:

Zrobimy to jako transformację liniową, taką, że będzie ona sortowana tak samo jak (rok, miesiąc) i taka, że ​​dla każdej krotki (rok, miesiąc) jest jedna i jedyna wartość, a wszystkie wartości są następujące po sobie:

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

Teraz to, co musimy przetestować, jest nieodłącznym elementem naszej specyfikacji, a mianowicie, że dla każdej krotki (roku, miesiąca) jest jeden i tylko jeden (bezwzględny_miesiąc), a te (bezwzględne_miesiąc) następują po sobie. Napiszmy kilka testów.

Naszym testem będzie selectzapytanie SQL o następującej strukturze: nazwa testu i instrukcja case zebrane razem. Nazwa testu to po prostu dowolny ciąg. Instrukcja case to tylko case wheninstrukcje testowe then 'passed' else 'failed' end.

Instrukcje testowe będą po prostu selekcjami SQL (podzapytaniami), które muszą być prawdziwe, aby test zakończył się pomyślnie.

Oto nasz pierwszy 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.

Uruchomienie tego zapytania daje następujący wynik: For every (year, month) there is one and only one (absolute_month): passed

Tak długo, jak istnieje wystarczająca ilość danych testowych w month_value, ten test działa.

Możemy również dodać test dla wystarczających danych testowych:

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

Teraz przetestujmy to po kolei:

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

Teraz umieśćmy nasze testy, które są tylko zapytaniami, w pliku i uruchommy ten skrypt w bazie danych. Rzeczywiście, jeśli przechowujemy nasze definicje widoków w skrypcie (lub skryptach, polecam jeden plik na powiązane widoki) do uruchomienia w bazie danych, możemy dodać nasze testy dla każdego widoku do tego samego skryptu, tak aby akt (ponownie -) tworzenie naszego widoku uruchamia również testy widoku. W ten sposób obaj otrzymujemy testy regresji podczas ponownego tworzenia widoków, a gdy tworzenie widoku działa w środowisku produkcyjnym, widok zostanie również przetestowany w środowisku produkcyjnym.

tpdi
źródło
27
Po raz pierwszy widzę czysty kod i testy jednostkowe w sql, cieszę się na ten dzień :)
Maxime ARNSTAMM
1
niesamowite hacki sql
CodeFarmer
13
To świetnie, ale po co używać jednoliterowych nazw kolumn i mało czytelnych nazw widoków? Dlaczego SQL miałby być mniej samodokumentujący lub czytelny niż Python?
snl
1
Niesamowite wyjaśnienie czegoś użytecznego, na co nigdy nie patrzyłem w świecie SQL / DB. Podoba mi się również sposób, w jaki przetestowałeś bazę danych tutaj.
Jackstine
Jako ostrzeżenie, widziałem widoki sql, które dołączają się do widoków sql, działają bardzo słabo w PostgreSQL. Jednak skutecznie zastosowałem tę technikę z M $ SQL.
Ben Liyanage,
6

Utwórz bazę danych systemu testowego, którą możesz przeładowywać tak często, jak chcesz. Załaduj swoje dane lub utwórz dane i zapisz je. Stwórz łatwy sposób na przeładowanie go. Dołącz swój system programistyczny do tej bazy danych i sprawdź poprawność kodu przed przejściem do produkcji. Kopnij się za każdym razem, gdy uda ci się pozwolić, aby jakiś problem wszedł do produkcji. Utwórz zestaw testów, aby zweryfikować znane problemy i poszerzyć swój zestaw testów w czasie.

ojblass
źródło
4

Możesz chcieć sprawdzić DbUnit , więc możesz spróbować napisać testy jednostkowe dla swoich programów z ustalonym zestawem danych. W ten sposób powinieneś być w stanie pisać zapytania z mniej lub bardziej przewidywalnymi wynikami.

Inną rzeczą, którą możesz chcieć zrobić, jest profilowanie stosu wykonywania SQL Server i sprawdzenie, czy wszystkie zapytania są rzeczywiście poprawne, np. Jeśli używasz tylko jednego zapytania, które zwraca zarówno poprawne, jak i niepoprawne wyniki, używany jest w wątpliwość, ale co, jeśli aplikacja wysyła różne zapytania w różnych punktach kodu?

Każda próba naprawienia zapytania byłaby wtedy daremna ... nieuczciwe zapytania i tak mogą nadal powodować nieprawidłowe wyniki.

Jon Limjap
źródło
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  

Zwróć uwagę, że to tylko sprawdza, czy wartości dla kolejnych miesięcy będą następujące po sobie, a nie, czy istnieją kolejne dane (co prawdopodobnie było tym, czego początkowo zamierzałeś). To zawsze mija, jeśli żadne z twoich danych źródłowych nie następują po sobie (np. Masz tylko miesiące parzyste), nawet jeśli twoje obliczenia są całkowicie wyłączone.

Czy też czegoś mi brakuje, czy też druga połowa tej klauzuli ON wpływa na niewłaściwą wartość miesiąca? (tj. sprawdza, czy 12/2011 przypada po 1/2010)

Co gorsza, jeśli dobrze pamiętam, SQL Server pozwala przynajmniej na mniej niż 10 poziomów widoków, zanim optymalizator wyrzuci swoje wirtualne ręce w powietrze i zacznie wykonywać pełne skanowanie tabeli przy każdym żądaniu, więc nie przesadzaj z tym podejściem.

Pamiętaj, aby przetestować wszystkie przypadki testowe!

W przeciwnym razie tworzenie bardzo szerokiego zestawu danych obejmującego większość lub wszystkie możliwe formy danych wejściowych, przy użyciu SqlUnit lub DbUnit lub dowolnej innej * jednostki w celu zautomatyzowania sprawdzania oczekiwanych wyników w odniesieniu do tych danych oraz przeglądanie, utrzymywanie i aktualizowanie ich w razie potrzeby ogólnie wydaje się droga do przebycia.

Mars the Infomage
źródło