HAVING без GROUP BY (I)
Хорошо известным (и малопонятным) фактом является то, что запросы на языке SQL могут включать раздел HAVING без соответствующего раздела GROUP BY. Рассмотрим, например, следующий запрос:
Q8: Выдать общий объем поставок для всех деталей, если и только если минимальный объем поставки каждой детали больше 50. На языке SQL возможна следующая формулировка:
SELECT SUM(SP.QTY) AS TQY FROM SP HAVING MIN(SP.QTY) > 50 ;
Если база данных содержит значения, приведенные в первой части заметки, результатом запроса будет таблица с одним столбцом с именем TQY и одной строкой, содержащей значение 3100.
Пояснение: Поскольку раздел GROUP BY отсутствует, то раздел HAVING применяется к "сгруппированной" версии SP, содержащей ровно одну группу. Если условие раздела HAVING вычисляется в true для этой группы (а так оно и есть для базы данных наших примеров), то раздел SELECT возвращает требуемую сумму значений. Если же при вычислении условия HAVING было бы получено false, то группа была бы отвергнута и окончательный результат был бы пустым. (Более точно, результатом была бы таблица с одним столбцом, не содержащая ни одной строки.)
Можно ли сформулировать запрос без использования HAVING? Очевидная попытка (с использованием "преобразования Типа 2") дала бы следующий результат:
SELECT DISTINCT SUM(SP.QTY) AS TQY FROM SP WHERE (SELECT MIN(SP.QTY) FROM SP) > 50 ;
Но, к сожалению, эта формулировка не является логически эквивалентной предыдущей. Чтобы убедиться в этом, предположим, что минимальный объем поставки должен быть больше 500. Тогда при выполнении запроса в первой формулировке будет произведена таблица с одним столбцом и без единой строки. В отличие от этого, при выполнении запроса без раздела HAVING результирующая таблица будет состоять из одного столбца и одной строки (содержащей неопределенное значение), поскольку условие раздела WHERE вычисляется в false для каждой строки SP, и поэтому раздел SELECT будет вычисляться для пустой таблицы.
Замечание: неопределенное значение с строке результата появляется в результате некорректной спецификации SQL, в соответствии с которой значение SUM для пустого множества есть NULL (а должно было бы быть нулевым).
Но формулировка, эквивалентная исходной и не включающая HAVING, все- таки существует. Она немного более хитрая:
SELECT DISTINCT ( SELECT SUM(SP.QTY) FROM SP) AS TQY FROM SP WHERE (SELECT MIN(SP.QTY) FROM SP) > 50 ;
При выполнении запроса в этой формулировке, если (внешние) разделы FROM и WHERE совместно производят пустую таблицу, то таким будет и результат всего запроса. Причина состоит в том, что единственный элемент, указанный в разделе SELECT является не ссылкой на агрегатную функцию SUM, а скалярным выражением, содержащим такую ссылку. Мощность окончательного результата (т.е. число строк в результирующей таблице) не зависит от вида этого скалярного выражения; можно было бы заменить (SELECT SUM ...) на SP.P#, на SP.QTY или даже на литерал. Более детально, происходит следующее:
- Предположим, что условие раздела WHERE вычисляется в false для каждой строки SP.
- Тогда разделы FROM и WHERE совместно производят пустую таблицу (без строк).
- Подзапрос в разделе SELECT, конечно, возвращает значение 3100. (Более точно, он вырабатывает таблицу с одним столбцом и одной строкой, содержащей численное значение, но SQL извлекает это значение из таблицы. Здесь для нас это обстоятельство не слишком важно, но в SQL вообще оно вызывает проблемы.)
- Итак, обсуждаемая формулировка запроса логически эквивалентна следующей:
SELECT 3100 AS TQY FROM empty ;
(empty именует пустую таблицу.) Очевидно, что результатом такого запроса является таблица с одним столбцом TQY и без строк.
Теперь мы можем сформулировать еще одно правило преобразования: Пусть имеется таблица R{A,B}, и agg1 и agg2 - агрегатные функции, применимые к R.A и R.B соответственно. Тогда выражение
SELECT agg1(R.A) AS C FROM R HAVING agg2(R.B) comp scalar ;
может быть логически преобразовано в эквивалентное выражение
SELECT DISTINCT ( SELECT agg1(R.A) FROM R ) AS C FROM R WHERE ( SELECT agg2(R.B) FROM R ) comp scalar ;
(Здесь comp - некоторый скалярный оператор сравнения, а scalar - некоторое скалярное выражение.)
Будем называть такие преобразования "преобразованиями Типа 3". Читателям рекомендуется самостоятельно разобрать случай, когда формулировка с HAVING включает раздел WHERE.