Уважаемые читатели!
Журнал Database Programming and Design уже второй месяц предоставляет возможность электронного доступа к колонке Кристофера Дейта "According to Date". В майском выпуске господин Дейт начал публиковать интересную заметку по поводу избыточности средств языка SQL. Мне показалось полезным предложить вашему вниманию первую часть этой заметки. Прошу обратить внимание, что в примерах используется SQL-92. Думаю, что многие из вас не обращали внимание на соответствующие возможности этого языка. Если вас интересуют другие работы К.Дейта, обращайтесь в электронный книжный магазин www.mistral.ru.
С уважением, Сергей Кузнецов
Greevous Bodily Harm (Part 1 of 2)
C.J. Date
оригинал статьи можно найти по адресу
http://www.dbpd.com/9805date.htm
Кристофер Дейт является независимым автором, лектором, исследователем и консультантом, специализирующимся в области систем реляционных баз данных. Корресподенцию ему можно послать по почте по адресу: Database Programming & Design, 411 Borel Ave., Ste. 100, San Mateo, CA 94402.
Известно ли вам, что разделы GROUP BY и HAVING (для их совместного названия далее используется аббревиатура GBH) в языке SQL избыточны? Другими словами, любой осмысленный запрос, который можен быть выражен с использованием одного из этих разделов или их обоих может быть выражен и без их применения. (Ниже поясняется значение слова "осмысленный".)
В качестве основы примеров используется известная по книгам К. Дейта база данных "поставщики и детали":
S ( S#, SNAME, STATUS, CITY ) PRIMARY KEY ( S# ) P ( P#, PNAME, COLOR, WEIGHT, CITY ) PRIMARY KEY ( P# ) SP ( S#, P#, QTY ) PRIMARY KEY ( S#, P# ) FOREIGN KEY ( S#) REFERENCES S FOREIGN KEY ( P#) REFERENCES P
Вот запрос к этой базе данных, для которой люди "естественно" используют раздел GROUP BY:
Q1: Для каждой поставляемой детали выдать номер детали, максимальное и минимальное число поставок.
"Естественной" (с применением GROUP BY) формулировкой запрса является следующая:
SELECT SP.P#, MAX(SP.QTY) AS MXQ, MIN(SP.QTY) AS MNQ FROM SP GROUP BY SP.P# ;Предположим, что база данных содержит следующие значения:
S SP S# SNAME STATUS CITY S# P# QTY ------------------------------- -------------- S1 SMITH 20 LONDON S1 P1 300 S2 JONES 10 PARIS S1 P2 200 S3 BLAKE 30 PARIS S1 P3 400 S4 CLARK 20 LONDON S1 P4 200 S5 ADAMS 30 ATHENS S1 P5 100 S1 P6 100 P S2 P1 300 S2 P2 400 P# PNAME COLOR WEIGHT CITY S3 P2 200 ------------------------------------ S4 P2 200 P1 Nut Red 12 London S4 P4 300 P2 Bolt Green 17 Paris S4 P5 400 P3 Screw Blue 17 Rome P4 Screw Red 14 London P5 Cam Blue 12 Paris P6 Cog Red 19 RomeТогда результатом запроса будет следующая таблица:
P# MXQ MNQ --------------- P1 300 300 P2 400 200 P3 400 400 P4 300 200 P5 400 100 P6 100 100Вот другая формулировка того же самого запроса без использования GROUP BY:
SELECT DISTINCT SP.P#, (SELECT MAX(SPX.QTY) FROM SP AS SPX WHERE SPX.P# = SP.P#) AS MXQ, (SELECT MIN(SPX.QTY) FROM SP AS SPX WHERE SPX.P# = SP.P#) AS MXQ FROM SP ;
Конечно, эта формулировка немного дленнее предыдущей, но логически они эквивалентны. Обобщая этот пример, можно вывести следующее заключение:
Пусть имеется таблица R { A, B, ... } и пусть agg - это агрегатная функция (например, SUM, MAX или MIN), применимая к столбцу R.B. Тогда выражение
SELECT R.A, agg(R.B) AS C FROM R GROUP BY R.A ;может быть логически преобразовано в эквивалентное выражение
SELECT DISTINCT R.A (SELECT agg(RX.B) FROM R AS RX WHERE RX.A = R.A) AS C) FROM R) ;Будем далее называть это преобразование преобразованием Типа 1.
Теперь рассмотрим, что произойдет, если в исходной формулировке с GROUP BY будет присутствовать раздел WHERE. Расширим запрос Q1:
Q2: Для каждой поставляемой детали выдать номер детали, максимальное и минимальное число поставок, но при этом не принимать во внимание поставки поставщика S1.Вот формулировка с GROUP BY:
SELECT SP.P#, MAX(SP.QTY) AS MXQ, MIN(SP.QTY) AS MNQ FROM SP WHERE SP.S# <> 'S1' GROUP BY SP.P# ;
Эквивалентная формулировка запроса без GROUP BY (не единственная из числа возможных) не намного хитрее:
SELECT DISTINCT SP.P#, (SELECT MAX(SPX.QTY) FROM SP AS SPX WHERE SPX.P# = SP.P# AND SPX.S# <> 'S1') AS MXQ, (SELECT MIN(SPX.QTY) FROM SP AS SPX WHERE SPX.P# = SP.P# AND SPX.S# <> 'S1') AS MNQ, FROM SP WHERE SP.S# <> 'S1' ;
Как видно, раздел WHERE из исходной формулировки с GROUP BY размножился в двух вложенных выражениях раздела SELECT. В исходной формулировке раздел WHERE управляет как разделом SELECT, так и разделом GROUP BY. Последовательность записи разделов в языке SQL несколько нелогична. В общем случае выражение, включающее разделы SELECT-FROM-WHERE-GROUP BY вычисляется в последовательности FROM-WHERE-GROUP BY-SELECT, и имело бы смысл писать именно в таком порядке. Но язык SQL этого не позволяет.
Как видно из приведенного выше примера, преобразование Типа 1 нуждается лишь в незначительных расширениях, чтобы включать возможность использования раздела WHERE. Детали очевидны. Еще раз изменим наш пример:
Q3: Для каждой поставляемой детали выдать максимальное число поставок, но без номера детали.Формулировка с GROUP BY:
SELECT MAX(SP.QTY) AS MXQ FROM SP GROUP BY SP.P# ;С использованием преобразования Типа 1 мы получим следующую формулировку:
SELECT DISTINCT (SELECT MAX(SPX.QTY) FROM SP AS SPX WHERE SPX.P# = SP.P# AS MXQ FROM SP ;Вот результаты выполнения этих двух запросов:
С GROUP BY Без GROUP BY MXQ MXQ ----- ----- 300 300 400 400 400 100 300 400 100
Как видно, результаты разные, т.е. запросы не совсем эквивалентны, и преобразование Типа 1 не работает в этом частном случае. Но действительной причиной отсутствия эквивалентности является то, что результат выполнения запроса с GROUP BY не есть отношение, поскольку содержит строки-дубликаты. Более существенно то, что дубликаты осмысленны. Например, у двух строк "300" разный смысл: одна из них означает, что у некоторой детали максимальный объем поставок равен 300, а другая - что имеется некоторая другая деталь с тем же самым максимальным объемом поставок. Эти "осмысленные дубликаты" представляют собой очень существенный отход от базовых принципов реляционной модели данных. Возможность их наличия говорит о том, что SQL не является и никогда не был истинно реляционным языком.
Заметим, что при использовании SQL "осмысленные дубликаты" могут появляться в ряде других случаев. Например, даже такое простое выражение как
SELECT CITY FROM S ;в общем случае производит результат с "осмысленными дубликатами".
Еще раз осмыслим запрос Q3. Что он на самом деле означает? Похоже, что нас интересовало множество максимальных поставок из SP. Формулировка без GROUP BY корректно производит эту информацию. Конечно, в результате не показывается, для каких конкретных деталей производились максимальные поставки, но требуемая информация предоставляется.
Формулировка с GROUP BY дает ту же самую информацию. Поэтому она допустима. Но некоторые люди (к числу которых не относится господин Дейт) могли бы сказать, что поскольку поставляются данные о шести деталях, то эта формулировка предпочтительнее той, которая без GROUP BY. Следует заметить, что эта информация представлена в нереляционной форме. Было бы корректнее получать ее по-другому, например, с использованием запроса Q1.
По мнению автора, запросы вида Q3, хотя и являются допустимыми, не слишком осмысленны. Такие запросы игнорируют существенную информацию. Обычно это связано с тем, что в раздел SELECT входят не все столбцы, используемые в разделе GROUP BY. Для подобных формулировок преобразование Типа 1 "работает некорректно". Но это преобразование работает правильно для всех "осмысленных" запросов.
Вот запрос, для формулировки которого большинство людей использовало бы раздел HAVING:
Q4: Для каждой детали, поставляемой более чем одним поставщиком, выдать номер детали.Возможной формулировкой с использованием GHB могла бы быть следующая:
SELECT SP.P# FROM SP GROUP BY SP.P# HAVING COUNT(*) > 1 ;Результатом такого запроса является таблица
P# ----- P1 P2 P4 P5Вот формулировка без использования разделов GROUP BY и HAVING:
SELECT DISTINCT SP.P# FROM SP WHERE (SELECT COUNT(*) FROM SP AS SPX WHERE SPX.P# = SP.P#) > 1 ;
Снова эта формулировка длиннее варианта с GBH, но логически они эквивалентны. И опять легко обобщить пример. Если использовать обозначения из предыдущего раздела, то выражение
SELECT R.A FROM R GROUP BY R.A HAVING agg(R.B) comp scalar ;
(где comp является некоторой операцией скалярного сравнения, а scalar - некоторое скалярное выражение) может быть логически преобразовано к эквивалентному выражению
SELECT DISTINCT R.A FROM R WHERE (SELECT agg(R.B) FROM R AS RX WHERE RX.A = R.A) comp scalar ;
Будем называть такого типа преобразования преобразованиями Типа 2. Посмотрим, что произойдет, если исходная формулировка будет включать раздел WHERE.
Q5: Для каждой детали, поставляемой более чем одним поставщиком (кроме поставщика S1), выдать номер детали.Формулировка с GBH:
SELECT SP.P# FROM SP WHERE SP.P# <> 'S1' GROUP BY SP.P# HAVING COUNT(*) > 1 ;Формулировка без GBH лишь немного хитрее:
SELECT DISTINCT SP.P# FROM SP WHERE (SELECT COUNT(*) FROM SP AS SPX WHERE SPX.P# = SP.P# AND SPX.S# <> 'S1') > 1 ;
Пример показывает, что преобразование Типа 2 может быть легко обобщено для случая наличия раздела WHERE. Вот несколько более сложный пример:
Q6: Для каждой детали, поставляемой более чем одним поставщиком, выдать номер детали и общее число поставок этой детали.Формулировка запроса с использованием GBH:
SELECT SP.P#, SUM(SP.QTY) AS TQY FROM SP GROUP BY SP.P# HAVING COUNT(*) > 1 ;Применяя правила преобразований Типа 1 и 2, получим следующее:
SELECT DISTINCT SP.P#, (SELECT SUM(SPX.QTY) FROM SP AS SPX WHERE SPX.P# = SP.P#) AS TQY FROM SP WHERE (SELECT COUNT(*) FROM SP AS SPX WHERE SPX.P# = SP.P#) > 1 ;И еще один пример:
Q7: Для каждой детали, поставляемой более чем одним поставщиком, выдать общее число поставок этой детали, но без номера детали.Вот формулировка с применением GBH:
SELECT SUM(SP.QTY) AS TQY FROM SP GROUP BY SP.P# HAVING COUNT(*) > 1 ;Преобразованный вариант:
SELECT (SELECT SUM(SPX.QTY) FROM SP AS SPX WHERE SPX.P# = SP.P#) AS TQY FROM SP WHERE (SELECT COUNT(*) FROM SP AS SPX WHERE SPX.P# = SP.P#) > 1 ;
В результате выполнения этих запросов были бы получены следующие результаты:
С GROUP BY Без GROUP BY и HAVING и HAVING TQY TQY ----- ----- 600 600 1000 1000 500 500 500
Снова запросы производят разные результаты и потому не эквивалентны. Но эта ситуация аналогична той, которая обсуждалась по поводу примера Q3, и не требует дополнительных комментариев.