Плохой стиль программирования на SQL и процедурных языках
Как пример отсутствия познаний в применении реляционных подходов к программированию, рассмотрим сообщение, опубликованное в группе новостей comp.databases.ms-sqlserver в январе 2005 г. Тема сообщения — “Как искать в таблице отсутствующие записи” — уже говорит о том, что автор письма мыслит в терминах файловой системы, а не в терминах системы управления реляционными базами данных.
В описании таблицы содержался обычный для новичка столбец id, и не было ни ключа, ни каких-либо ограничений. В таблице содержались строки для каждого дня одного года, которые идентифицировались порядковым номером недели внутри года (от 1 до 53) и порядковым номером дня недели (от 1 до 7). Ниже представлена структура рассматриваемой таблицы. От оригинальной она отличается только именами атрибутов:
После удаления бесполезного столбца id и добавления ограничений мы получаем такую таблицу:
Хотя на словах автор указал на множество ограничений, в определении таблиц он их применить не удосужился. У новичков таблица ассоциируется с файлом, а не с множеством. Для них единственным критерием, в соответствии с которым данные необходимо поместить в файл, является сам процесс записи в файл. А файл не может проверить достоверность содержащихся в нем данных. Введение в таблицу автонумерации симулирует нумерацию записей, свойственную последовательной файловой системе.
Сама задача заключалась в нахождении первого отсутствующего дня внутри каждой недели с последующим добавлением соответствующей строки. Если к записываемым данных и предъявлялись какие-либо требования, то в описании они представлены не были. Вот собственное решение автора задачи (оно переведено на стандартный SQL с диалекта T-SQL с небольшими изменениями в именах переменных и атрибутов):
Это типичная имитация оператора цикла FOR, который использовался во всех языках программирования третьего поколения. Однако если вы посмотрите на этот код в течение двух секунд, то увидите, что он плох и как программа на процедурном языке! Отсутствие программистских навыков не исправляется даже средствами SQL Больше того, последствия неудачного решения, вызванного стремлением подражать процедурным языкам программирования, в SQL только усугубляются. Оптимизаторы и компиляторы SQL не рассчитаны на оптимизацию кода, написанного в стиле, свойственном процедурным языкам. Даже если автор и решил придерживаться процедурного стиля программирования, он должен был следовать классическому построению кода. А для этого нужно было удалить лишние локальные переменные и избавиться от замаскированных операторов безусловного перехода GOTO:
CREATE FUNCTION InsertNewWeekDay (IN my_week_nbr INTEGER) RETURNS INTEGER LANGUAGE SQL BEGIN DECLARE answerjibr INTEGER;SET answer_nbr = 1; WHILE answer_nbr < 8 DO IF NOT EXISTS (SELECT * FROM WeeklyReport WHERE day_nbr = answer_nbr AND week_nbr = my_week_nbr) THEN RETURN answer_nbr; ELSE SET answer_nbr = answer_nbr + 1; END IF; END WHILE;RETURN CAST (NULL AS INTEGER); - приводит к ошибке END;
Здесь необходимо обратить внимание на следующее упущение автора. Он не объяснил, как следует обходиться с той неделей, в которой уже представлены все семь дней. В исходной структуре таблицы благодаря отсутствию ограничений в качестве номера дня и недели допускается любое целое значение. В исправленном описании любое значение номера дня недели, выходящее за рамки интервала от 1 до 7, нарушает ограничение для первичного ключа. Это, конечно, не лучшее решение, но, по крайней мере, оно позволяет удовлетворить очевидным требованиям с минимальными усилиями.
Сможем ли мы решить эту задачу без циклической структуры и получить чисто непроцедурное SQL-решение? Да, и здесь возможно несколько вариантов. Конечной целью поиска отсутствующего дня недели является вставка в таблицу строки. Так почему бы не сделать все это в рамках одной процедуры, вместо того чтобы осуществлять поиск в одной функции, а последующее добавление строки — на последующем процедурном шаге? Давайте будем мыслить на уровне процесса в целом, а не в рамках последовательных шагов.
Первое решение смотрится неуклюже и трудно обобщается. Но оно работает достаточно быстро, если оптимизатор обеспечивает вынос всех подзапросов из операторов WHEN и их обработку как единого целого. В данном решении локальные переменные уже не используются:
CREATE PROCEDURE InsertNewWeekDay (IN my_week_nbr INTEGER)LANGUAGE SQLINSERT INTO WeeklyReport (week_nbr, day_nbr)VALUES (my_week_nbr, (CASE WHEN 1 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr = my_week_nbr) THEN 1 WHEN 2 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr = my_week_nbr) THEN 2 WHEN 3 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr = my_week_nbr) THEN 3 WHEN 4 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr = my_week_nbr) THEN 4 WHEN 5 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr = my_week_nbr) THEN 5 WHEN 6 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr = my_week_nbr) THEN 6 WHEN 7 NOT IN (SELECT day_nbr FROM WeeklyReport WHERE week_nbr = my_week_nbr) THEN 7ELSE NULL END; -- NULL нарушает ограничение для первичного ключа
Идея состояла в том, чтобы собрать все возможные номера дней недели и сравнить их с каждым значением из упорядоченного списка. Выражение CASE просто прячет этот список. Хотя это уже шаг вперед, в действительности данное решение еще не является ориентированным на множество.
Ниже представлена другая версия, использующая конструктор таблицы. Она более компактна и легко обобщается. Здесь уже мы действительно используем решение, ориентированное на множество! Из множества всех возможных дней мы выделяем подмножество порядковых номеров дней недели, а затем в границах этого подмножества выполняем поиск минимального значения:
Также вы можете использовать чистый набор операций над множествами. С их помощью можно убрать сразу все подмножество дней недели, уже присутствующих в таблице, а затем из остатка отобрать наименьшее значение:
CREATE PROCEDURE InsertNewWeekDay (IN my_week_nbr INTEGER)LANGUAGE SQLINSERT INTO WeeklyReport (week_nbr, day_nbr)SELECT my_week_nbr, MIN(n) FROM (VALUES (1),(2),(3),(4),(5),(6),(7) EXCEPT SELECT day_nbr FROM WeeklyReport AS W WHERE W.week_nbr = my_week_nbr) AS N(n);Если в таблице присутствуют все семь дней недели, мы получим пустое множество, что приведет к попытке присваивания атрибуту day_nbr пустого значения NULL В свою очередь, это вызовет нарушение допустимых границ значений для первичного ключа.
Далее представлена третья обобщенная версия решения с использованием таблицы Sequence, в которой в общем случае может заключаться любой необходимый диапазон целочисленных значений. Только запомните, что наличие данной таблицы должно быть определено соответствующими инструкциями DDL
В случае семи анализируемых значений большого различия в производительности представленных здесь решений не будет. Однако, если таких значений очень много, к заметному улучшению производительности по сравнению с циклом приведет использование хэширования или индексирования.