Уважаемые читатели!
Журнал Database Programming and Design уже второй месяц
предоставляет возможность электронного доступа к колонке
Кристофера Дейта "According to Date". В майском выпуске господин
Дейт начал публиковать интересную заметку по поводу избыточности
средств языка SQL. Мне показалось полезным предложить вашему
вниманию первую часть этой заметки. Прошу обратить внимание, что
в примерах используется SQL-92. Думаю, что многие из вас не
обращали внимание на соответствующие возможности этого языка.
Если вас интересуют другие работы К.Дейта, обращайтесь в
электронный книжный магазин www.mistral.ru.
С уважением, Сергей Кузнецов
DataBase Programming & Design OnLine, May 1998
A discussion of some redundances in SQL
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 избыточны? Другими словами, любой осмысленный запрос, который
можен быть выражен с использованием одного из этих разделов или
их обоих может быть выражен и без их применения. (Ниже поясняется
значение слова "осмысленный".)
Раздел GROUP BY
В качестве основы примеров используется известная по книгам К.
Дейта база данных "поставщики и детали":
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
Вот запрос, для формулировки которого большинство людей
использовало бы раздел 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, и не требует дополнительных комментариев.