Лучший способ тестирования SQL-запросов [закрыто]


110

Я столкнулся с проблемой, при которой сложные SQL-запросы по-прежнему выдаются с ошибками. По сути, это приводит к отправке почты неправильным клиентам и другим подобным «проблемам».

Какой у всех опыт создания подобных SQL-запросов? Каждую вторую неделю мы создаем новые когорты данных.

Итак, вот некоторые из моих мыслей и их ограничения:

  • Создание тестовых данных Хотя это доказывает, что у нас есть все верные данные, это не приводит к исключению аномалий в производстве. Это данные, которые сегодня считались бы неправильными, но, возможно, были правильными 10 лет назад; это не было задокументировано, и поэтому мы узнаем об этом только после извлечения данных.

  • Создание диаграмм Венна и карт данных. Кажется, это надежный способ проверить структуру запроса, однако он не гарантирует правильность реализации. Это заставляет разработчиков заранее планировать и думать о том, что происходит, когда они пишут.

Спасибо за любой вклад, который вы можете внести в мою проблему.

Ответы:


164

Вы бы не написали приложение с функциями длиной в 200 строк. Вы бы разложили эти длинные функции на более мелкие функции, каждая из которых несет четко определенную ответственность.

Зачем так писать свой SQL?

Разложите свои запросы так же, как вы разложите свои функции. Это делает их короче, проще, проще для понимания, тестирования и рефакторинга. И это позволяет вам добавлять «прокладки» между ними и «оболочки» вокруг них, как вы это делаете в процедурном коде.

Как ты делаешь это? Превращая каждую важную вещь, которую делает запрос, в представление. Затем вы сочиняете более сложные запросы из этих более простых представлений, так же как вы составляете более сложные функции из более примитивных функций.

И самое замечательное в том, что для большинства композиций представлений вы получите точно такую ​​же производительность от своей СУБД. (Для некоторых вы этого не сделаете; и что? Преждевременная оптимизация - корень всех зол. Сначала кодируйте правильно, а затем оптимизируйте, если вам нужно.)

Вот пример использования нескольких представлений для разложения сложного запроса.

В этом примере, поскольку каждое представление добавляет только одно преобразование, каждое из них может быть независимо протестировано для поиска ошибок, и тесты просты.

Вот базовая таблица в примере:

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, я счастлив за день :)
Максим ARNSTAMM

1
потрясающие sql-хаки
CodeFarmer

13
Это здорово, но зачем использовать однобуквенные имена для столбцов и едва различимые названия представлений? Почему SQL должен быть менее самодокументированным или читаемым, чем Python?
snl

1
Замечательное объяснение чего-то полезного, чего я никогда не видел в мире SQL / DB. Также мне нравится, как вы здесь тестировали базу данных.
Джекстин 08

В качестве предупреждения я видел, что представления sql, которые присоединяются к представлениям sql, очень плохо работают в PostgreSQL. Однако я эффективно использовал эту технику с M $ SQL.
Ben Liyanage

6

Создайте базу данных тестовой системы, которую вы можете перезагружать так часто, как захотите. Загрузите свои данные или создайте свои данные и сохраните их. Создайте простой способ его перезарядки. Подключите свою систему разработки к этой базе данных и проверьте свой код, прежде чем переходить к производству. Пинайте себя каждый раз, когда вам удается запустить задачу в производство. Создайте набор тестов, чтобы проверить известные проблемы и со временем расширить свой набор тестов.


4

Возможно, вы захотите проверить DbUnit , поэтому вы можете попробовать написать модульные тесты для своих программ с фиксированным набором данных. Таким образом вы сможете писать запросы с более или менее предсказуемыми результатами.

Еще одна вещь, которую вы, возможно, захотите сделать, - это профилировать свой стек выполнения SQL Server и выяснить, действительно ли все запросы правильные, например, если вы используете только один запрос, который возвращает как правильные, так и неправильные результаты, то очевидно, что запрос под вопросом, но как насчет того, если ваше приложение отправляет разные запросы в разных точках кода?

Любая попытка исправить ваш запрос будет бесполезна ... мошеннические запросы все равно могут вызывать неправильные результаты.


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  

Обратите внимание, что это проверяет только то, что значения am для последовательных месяцев будут последовательными, а не то, что последовательные данные существуют (что, вероятно, именно то, что вы планировали изначально). Это всегда будет проходить, если ни одно из ваших исходных данных не является последовательным (например, у вас есть только четные месяцы), даже если ваш расчет am полностью неверен.

Кроме того, я что-то упустил, или вторая половина этого предложения ON приводит к неправильному значению месяца? (т.е. проверяет, что 12/2011 наступает после 1/2010)

Что еще хуже, если я правильно помню, SQL Server, по крайней мере, позволяет вам иметь менее 10 уровней представлений, прежде чем оптимизатор поднимет свои виртуальные руки в воздух и начнет выполнять полное сканирование таблицы при каждом запросе, поэтому не переусердствуйте с этим подходом.

Не забудьте проверить свои тестовые примеры!

В противном случае создание очень широкого набора данных, охватывающего большинство или все возможные формы входных данных, использование SqlUnit, DbUnit или любого другого * модуля для автоматизации проверки ожидаемых результатов по этим данным, а также просмотр, поддержка и обновление их по мере необходимости обычно кажется путь идти.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.