Главная страница

лекция. Учебник по sql если вы хотите узнать, что такое sql этот сайт для вас


Скачать 7.88 Mb.
НазваниеУчебник по sql если вы хотите узнать, что такое sql этот сайт для вас
Анкорлекция
Дата21.12.2022
Размер7.88 Mb.
Формат файлаpdf
Имя файлаMoiseenko_SQL.pdf
ТипУчебник
#857633
страница13 из 47
1   ...   9   10   11   12   13   14   15   16   ...   47
Упражнение 11
(подсказки и
решения)
Наличие стандартной агрегатной функции AVG решает все проблемы:
1.
SELECT
AVG
(
speed
)
2.
FROM
PC;
Однако на форуме сайта были замечены попытки выделиться из общей
«серой» массы. Вот вполне эквивалентное решение, которое, правда, добавляет лишнюю операцию в процедурный план:
1.
SELECT
SUM
(
speed
)
/
COUNT
(
speed
)
2.
FROM
PC;
Справедливость следующего решения зависит от имеющихся ограничений:
1.
SELECT
SUM
(
speed
)
/
SUM
(
1
)
2.
FROM
PC;
А именно, если SUM(speed) игнорирует строки с NULL-значением скорости, то SUM(1) подсчитает общее количество строк в таблице PC, что, по существу, эквивалентно использованию COUNT(*). В результате в числителе будет подсчитана сумма скоростей всех ПК за исключением строк с неизвестной скоростью, которая будет делиться на общее число строк.
Поэтому все приведенные решения будут эквивалентны, только если NULL- значения недопустимы, то есть имеется ограничение NOT NULL на столбце speed.

Упражнение 15
(подсказки и
решения)
Неверное решение
1.11.1
этой задачи легко поправить, если различать ПК не по номеру модели, а, как и положено, по первичному ключу — столбцу code:
1.
SELECT
DISTINCT
t.hd
2.
FROM
PC t
3.
WHERE
EXISTS
(
SELECT
*
4.
FROM
PC
5.
WHERE
pc.hd = t.hd
AND
6. pc.code <> t.code
7.
)
;
Поскольку достаточно лишь двух ПК с одинаковыми жесткими дисками, можно использовать самосоединение таблицы PC по аналогичным условиям:
1.
SELECT
DISTINCT
pc1.hd
2.
FROM
PC pc1, PC pc2 3.
WHERE
pc1.hd = pc2.hd
AND
4. pc1.code <> pc2.code;
Однако наиболее оптимальным будет решение с группировкой и условиями отбора в предложении HAVING:
1.
SELECT
2. PC.hd
FROM
PC
3.
GROUP
BY
hd
4.
HAVING
COUNT
(
hd
)
>
1
;

Для полноты картины приведем еще одно решение, которое использует подзапрос с группировкой и которое по эффективности также уступает вышеприведенному.
1.
SELECT
DISTINCT
hd
2.
FROM
PC
3.
WHERE
(
SELECT
COUNT
(
hd
)
4.
FROM
PC pc2 5.
WHERE
pc2.hd = pc.hd
6.
)
>
1
;
Причина низкой эффективности рассмотренных решений с подзапросами заключается в том, что все они используют коррелирующий подзапрос, то есть подзапрос, который будет выполняться для каждой строки основного запроса.
Запрос с соединением имеет самую низкую производительность. Это вполне понятно, так как операции соединения весьма затратные, несмотря на достаточно эффективные алгоритмы их реализации.
Упражнение 16
(подсказки и
решения)
Избыточность решения
1.12.2
можно устранить, если вообще убрать подзапрос, а соединение выполнить между таблицами P и L. При этом запрос получится не только компактным, но и легко читаемым и, что не менее важно, более эффективным.
Еще один пример тяжело читаемого запроса, который был бы правильным, будь номер модели
(model) числовым.
1.
SELECT
MAX
(
model1
)
,
MIN
(
model2
)
,
MAX
(
speed
)
,
MAX
(
ram
)

2.
FROM
(
SELECT
pc1.model
AS
model1, pc2.model
AS
model2, pc1.speed, pc2.ram,
3.
CASE
WHEN
CAST
(
pc1.model
AS
NUMERIC
(
6
,
2
))
>
4.
CAST
(
pc2.model
AS
NUMERIC
(
6
,
2
))
5.
THEN
pc1.model+pc2.model
6.
ELSE
pc2.model+pc1.model
7.
END
AS
sm
8.
FROM
PC pc1, PC pc2 9.
WHERE
pc1.speed = pc2.speed
AND
10. pc1.ram = pc2.ram
AND
11. pc1.model <> pc2.model
12.
)
a
13.
GROUP
BY
a.sm;
Однако тип данных VARCHAR(50) подразумевает наличие произвольных символов, наличие которых имеет место в проверочной базе данных (скажем,
T-64). На этих данных преобразование типа
1.
CAST
(
pc1.model
AS
NUMERIC
(
6
,
2
))
будет вызывать ошибку.
Я считаю, что это хороший пример того, как не следует писать запросы. Как же следует их писать? Загляните на форум задачи, когда её решите, там вы найдете лучшие образцы.
Упражнение 17
(подсказки и
решения)
Итак, избавляемся от декартового произведения
Для этого убираем таблицу
PC из предложения FROM, а таблицы Product и Laptop соединяем по столбцу model:

1.
SELECT
DISTINCT
type, Laptop.model, speed
2.
FROM
Laptop, Product
3.
WHERE
Product.model = Laptop.model
AND
4. Laptop.speed <
(
SELECT
MIN
(
speed
)
FROM
PC
)
;
Условие p.type = 'laptop' уже излишне, так как внутреннее соединение будет содержать модели только данного типа. Оказывается, что можно избавиться и от соединения, поскольку таблица Product используется только для того, чтобы в результирующем наборе вывести тип продукта. Но тип продукта заранее известен — это портативный компьютер, поэтому мы может просто применить выражение (константу) для указания типа, убрав соединение:
1.
SELECT
DISTINCT
'Laptop'
AS
type, model, speed
2.
FROM
Laptop
3.
WHERE
speed <
(
SELECT
MIN
(
speed
)
FROM
PC
)
;
Заметим, что данное решение будет справедливо только в том случае, если в таблице Laptop будут находиться изделия с типом Laptop. Для нашей базы данных это условие выполняется, т. к. имеется всего три типа продукции и соответственно три таблицы. Поэтому нарушение данного условия может быть связано только с изменением структуры, что, впрочем, тоже следует иметь в виду при разработке приложений со встроенными SQL-запросами.
Упражнение 18
(подсказки и
решения)
В следующем решении:
1.
SELECT
c.maker, a.priceA price
2.
FROM
(
SELECT
MIN
(
price
)
priceA
3.
FROM
Printer
4.
WHERE
Color =
'y'
5.
)
a
INNER
JOIN

6. Printer b
ON
a.priceA = b.price
INNER
JOIN
7.
Product c
ON
b.model = c.model;
в подзапросе определяются минимальная цена на цветные принтеры, затем выполняется соединение по этой цене с таблицей принтеров, чтобы найти все принтеры с такой ценой. Наконец, соединение с таблицей Product дает производителей найденных принтеров.
Конечно, соединение по цене можно заменить простым сравнением
1.
WHERE
price =
(
SELECT
MIN
(
price
)
priceA
2.
FROM
Printer
3.
WHERE
Color =
'y'
4.
)
Однако ошибка не в этом, а в том, что отыскиваются любые принтеры, у которых цена совпадает с минимальной ценой на цветные принтеры, что, собственно говоря, и имеет место. В результате получаем:
Maker
Price
D
270.0
A
270.0
Правильный же ответ дает только одну строку:
Maker
Price
D
270.0

Вторая строка не является цветным принтером, в этом легко убедиться, если добавить в предложение SELECT дополнительные столбцы:
1.
SELECT
c.maker, a.priceA price, color, b.type что дает
maker
price
color
type
D
270.0 y
Jet
A
270.0 n
Matrix
Вторая ошибка заключается в возможном наличии дубликатов, т. к. для одного и того же производителя может иметься несколько принтеров по одной и той же цене.
Упражнение 23
(подсказки и
решения)
Для решения этой задачи используется обычно два подхода: соединение с последующим выбором нужной строки и проверка на попадание производителя в два списка.
Следующее неправильное решение, реализует первый подход:
Решение 4.11.1
1.
SELECT
DISTINCT
a.maker
2.
FROM
Product a
LEFT
JOIN
3. PC b
ON
a.model = b.model
AND

4. b.speed> =
750
LEFT
JOIN
5. Laptop c
ON
a.model = c.model
AND
c.speed> =
750 6.
WHERE
NOT
(
b.model
IS
NULL
AND
7. c.model
IS
NULL
8.
)
;
Это еще один вариант на тему «чего-то одного». Действительно, модель уникальна, т. е. она представляет собой либо ПК, либо портативный компьютер. В результате внешних соединений получаются строки типа:
1. maker model
(
PC
)
NULL
или
1. maker
NULL
model
(
laptop
)
Однако здесь не может быть строк:
1. maker model
(
PC
)
model
(
laptop
)
поскольку соединение выполняется по номеру модели.
В результате в списке имеются производители, которые выпускают только один вид продукции с требуемыми характеристиками. Однако подправить это решение просто, добавив группировку по производителю и посчитав модели.
Предлагаем вам сделать это самостоятельно.
Решение 4.11.2
Для демонстрации второго подхода рассмотрим следующий вариант:
1.
SELECT
DISTINCT
t.maker
2.
FROM
Product t
3.
WHERE
(
t.model
IN
(
SELECT
model
4.
FROM
PC
5.
WHERE
speed >=
750 6.
)
OR
7. t.model
IN
(
SELECT
model

8.
FROM
Laptop
9.
WHERE
speed >=
750 10.
)
11.
)
AND
12.
EXISTS
(
SELECT
*
13.
FROM
Product
14.
WHERE
Product.maker = t.maker
AND
15.
Product.type=
'PC'
16.
)
AND
17.
EXISTS
(
SELECT
*
18.
FROM
Product
19.
WHERE
Product.maker = t.maker
AND
20.
Product.type=
'Laptop'
21.
)
;
который можно прочитать следующим образом: найти производителя, который выпускает ПК со скоростью не менее 750 МГц или портативный компьютер со скоростью не менее 750 МГц; при этом данный производитель должен производить ПК и портативные компьютеры. Несомненный прогресс этого решения по сравнению с решением
1.15.1
состоит в том, что выводятся производители, как ПК, так и портативных компьютеров. Однако это решение допускает вариант, когда производитель выпускает только ПК со скоростью более 750 МГц, в то время как все его портативные компьютеры имеют скорость менее 750 МГц, и наоборот.
Упражнение 26
(подсказки и
решения)
Ниже приводится еще пара неверных решений этой задачи, которые содержат легко исправимую ошибку.
Решение 4.12.1
1.
SELECT
AVG
(
price
)
2.
FROM
(
SELECT
price
3.
FROM
PC

4.
WHERE
model
IN
(
SELECT
model
5.
FROM
product
6.
WHERE
maker=
'A'
AND
7. type=
'PC'
8.
)
9.
UNION
10.
SELECT
price
11.
FROM
Laptop
12.
WHERE
model
IN
(
SELECT
model
13.
FROM
product
14.
WHERE
maker=
'A'
AND
15. type=
'Laptop'
16.
)
17.
)
AS
prod;
Решение 4.12.2
1.
SELECT
AVG
(
price
)
2.
FROM
(
SELECT
price, model
3.
FROM
pc
4.
WHERE
model
IN
(
SELECT
model
5.
FROM
product
6.
WHERE
maker=
'A'
AND
7. type=
'PC'
8.
)
9.
UNION
10.
SELECT
price, model
11.
FROM
Laptop
12.
WHERE
model
IN
(
SELECT
model
13.
FROM
product
14.
WHERE
maker=
'A'
AND
15. type=
'Laptop'
16.
)
17.
)
AS
prod;
Первое из этих решений дает на основной базе результат 772.5, а второе —
773.0 при правильном значении 734.5454545454545.
В запросе 4.12.1 выбираются цены на все модели производителя А из таблицы PC. Затем они объединяются с ценами на все модели производителя
А из таблицы Laptop. Наконец, вычисляется среднее значение. Что же тут
неправильного? Ошибка состоит в том, как объединяются цены.
Оператор UNION исключает дубликаты, поэтому из нескольких одинаковых цен (если таковые имеются) будет оставаться только одна. Как результат, среднее будет посчитано по неверному количеству.
В запросе 4.12.2 выбирается не только цена, но и номер модели. То есть объединение выполняется по паре атрибутов. Это решение было бы правильным, если бы в соответствующей таблице не было одинаковых моделей с одинаковыми ценами. Последнее было бы гарантировано, если бы пара атрибутов {price, model} являлась первичным ключом. Однако согласно нашей схеме это не так. Сама по себе такая ситуация не является нереальной.
Представим себе, что одна модель комплектуется большим диском, чем другая модель с тем же номером, а памяти, наоборот, имеет меньше. Тогда цены у них вполне может быть одинаковы. Естественно, может быть несколько идентичных моделей.
В результате объединения будут исключены дубликаты пар {price, model} и, как следствие, получен неверный результат.
Надеемся, теперь вполне очевидно, как следует решать эту задачу.
Упражнение 27
(подсказки и
решения)
Смотрите упражнение номер 2
Упражнение 30
(подсказки и
решения)
Чтобы решить проблему удвоения, утроения результатов, достаточно сначала выполнить соответствующую группировку по каждой таблице, а уже затем их соединять. Тогда эта задача сводится
к задаче 29, решение которой не вызывало проблем и потому не приводится в книге.
Это не единственный способ решить задачу.
Загляните на форум сайта
, чтобы познакомиться с другими вариантами.
Упражнение 46
(подсказки и
решения)
Обещанный в пункте 3.1
пример автоматического соединения двух решений, каждое из которых правильно учитывает один из двух моментов, вызывающих характерные ошибки, может выглядеть так (смотрите комментарии)
1.
-- Корабли, участвующие в битве при Гвадалканале и которые
есть в Ships
2.
-- Обратите внимание на использование коррелирующего
подзапроса в
3.
-- предложении WHERE, который решает проблему устранения
дубликатов при
4.
-- декартовом произведения
5.
SELECT
a.ship, displacement, numGuns
6.
FROM
(
SELECT
ship
7.
FROM
Outcomes
8.
WHERE
battle =
'Guadalcanal'
9.
)
AS
a, Classes
10.
WHERE
class
IN
(
SELECT
class
11.
FROM
Ships
12.
WHERE
name = a.ship
13.
)
14.
UNION
15.
-- Аналогичный по логике запрос, который выбирает
те головные корабли из
16.
-- Outcomes, которые сражались при Гвадалканале.
17.
SELECT
a.ship, displacement, numGuns
18.
FROM
(
SELECT
ship
19.
FROM
Outcomes
20.
WHERE
battle =
'Guadalcanal'
21.
)
AS
a, Classes
22.
WHERE
class
IN
(
SELECT
ship

23.
FROM
Outcomes
24.
WHERE
ship = a.ship
25.
)
26.
UNION
27.
--По сути, это решение 3.1.1
28.
SELECT
a.ship, displacement, numGuns
29.
FROM
(
SELECT
ship
30.
FROM
Outcomes
31.
WHERE
battle =
'Guadalcanal'
32.
)
AS
a
LEFT
JOIN
33.
Classes
ON
a.ship = class;
В результате получим лишние строки, характерным примером которых являются такие:
ship
displacement
numGuns
California 32000 12
California NULL
NULL
Можно еще утяжелить этот запрос (и сделать его менее эффективным), добавив код для исключения ошибочной строки. Критерием здесь может служить присутствие NULL-значения, например, в столбце displacement, если есть другая строка с тем же именем корабля. Однако мы советуем обойтись без этого и решить задачу иначе. Тем более, что это возможно, в чем легко убедиться, зайдя на форум сайта, посвященный этой задаче.
В заключение приведем почти правильное решение:
1.
SELECT
name, displacement, numGuns
2.
FROM
Classes, Ships
3.
WHERE
Classes.class = Ships.class
AND
4. name
IN
(
SELECT
Ship
5.
FROM
Outcomes
6.
WHERE
battle =
'Guadalcanal'

7.
)
8.
UNION
9.
SELECT
class, displacement, numGuns
10.
FROM
Classes
11.
WHERE
class
IN
(
SELECT
ship
12.
FROM
Outcomes
13.
WHERE
battle =
'Guadalcanal'
14.
)
;
Первый запрос из объединения в этом решении находит информацию о кораблях, которые есть в таблице Ships и которые принимали участие в сражение при Гвадалканале.
Второй запрос находит нужные нам головные корабли в Outcomes.
Возможные дубликаты (когда головной корабль имеется также и в таблице
Ships) исключаются использованием предложения UNION.
Так что же здесь неверно? Если до сих пор непонятно, вернитесь к обсуждению задачи в пункте 3.1
Упражнение 37
(подсказки и
решения)
Рассмотрим следующее решение задачи, которое свободно от ошибок, проанализированных в пункте
3.2
:
1.
SELECT
t1.class
2.
FROM
(
SELECT
a.class
AS
class,
COUNT
(
b.name
)
AS
coun
3.
FROM
Classes a
LEFT
JOIN
4. Ships b
ON
b.class = a.class
5.
GROUP
BY
a.class
6.
UNION
ALL
7.
SELECT
a1.class
AS
class,
COUNT
(
ship
)
AS
coun
8.
FROM
Classes a1
LEFT
JOIN
9. Outcomes d
ON
d.ship = a1.class
10.
WHERE
d.ship
NOT
IN
(
SELECT
b.name
11.
FROM
Ships b

12.
)
13.
GROUP
BY
a1.class
14.
)
t1 15.
GROUP
BY
t1.class
16.
HAVING
SUM
(
t1.coun
)
=
1
;
Действительно, в подзапросе объединяются два запроса, первый из которых подсчитывает для каждого класса корабли из таблицы Ships, а второй подсчитывает только те головные корабли, которых нет в Ships. Затем основной запрос для каждого класса эти количества суммирует и оставляет только те классы, в которых содержится только один корабль.
Обратите внимание на использование
UNION ALL
. Это необходимо, т. к. в противном случае будут устранены дубликаты пары {класс, количество кораблей}, в результате чего будет выводиться класс, для которого имеется один неголовной корабль в Ships и головной в Outcomes. Как раз эта характерная ошибка отмечалась нами в пункте 3.2
Что же осталось исправить, раз это решение не принимается системой?
Причина состоит в том, что головной корабль класса может принимать участие в нескольких сражениях, и тогда второй из объединяемых запросов столько раз учтет один и тот же головной корабль, сколько раз тот участвовал в сражениях.
Упражнение 39
(подсказки и
решения)
Рассмотрим решение, которое учитывает даты сражений, но все же является не вполне верным:
1.
SELECT
t.name
2.
FROM
(
SELECT
o.ship
AS
name, battle
3.
FROM
Outcomes o
4.
)
t, Battles b
5.
WHERE
t.battle = b.name
6.
GROUP
BY
t.name
7.
HAVING
(
SELECT
result

8.
FROM
Outcomes, Battles
9.
WHERE
ship = t.name
AND
10. battle = name
AND
11. date =
MIN
(
b.date
)
12.
)
=
'damaged'
AND
13.
(
SELECT
result
14.
FROM
Outcomes, Battles
15.
WHERE
ship = t.name
AND
16. battle = name
AND
17. date =
MAX
(
b.date
)
18.
)
IN
(
'damaged'
,
'ok'
,
'sunk'
)
AND
19.
COUNT
(
t.name
)
>
1
;
В этом решении участвовавшие в сражениях корабли группируются по именам, после чего остаются только те которые отвечают следующим условиям:

в сражении с минимальной датой корабль должен быть поврежден;

в сражении с максимальной датой результат сражения может быть любым;

число сражений должно быть больше одного.
Логическая ошибка, допущенная в этом запросе, заключается в том, что если корабль участвовал более чем в двух сражениях, то в первом своем сражении
(сражении с минимальной датой) корабль может и не быть поврежден. Более точно, результат его сражения должен быть ok, чтобы представленное выше решение перестало давать правильный результат. Действительно, потоплен корабль быть не может, иначе бы он не участвовал в более поздних сражениях.
Если бы он был поврежден, то запрос его бы справедливо учитывал. А вот если последовательность результатов будет следующей: ok, damaged и любой из трех возможных, то представленное решение его бы не выводило, хотя такой корабль и отвечает условиям задачи.
Упражнение 51
(подсказки и
решения)
Здесь мы хотим привести одно интересное решение, которое использует только соединения:

1.
SELECT
DISTINCT
CASE
2.
WHEN
sh.name
IS
NOT
NULL
3.
THEN
sh.name
4.
ELSE
ou.ship
5.
END
name
6.
FROM
Ships sh
FULL
JOIN
7. Outcomes ou
ON
ou.ship = sh.name
LEFT
OUTER
JOIN
8. Classes cl
ON
ou.ship = cl.class
OR
9. sh.class = cl.class
LEFT
OUTER
JOIN
10.
Classes cl2
ON
cl.displacement = cl2.displacement
AND
11. cl.numGuns < cl2.numGuns
12.
WHERE
cl.class
IS
NOT
NULL
AND
13. cl2.class
IS
NULL
;
Достаточно сложная логика этого решения будет, безусловно, полезна для обучения на данном этапе. Начнем, как обычно, с предложения FROM.
Полное соединение (FULL JOIN) дает нам все корабли из базы данных. При этом возможны следующие варианты:

корабль присутствует только в таблице Ships;

корабль присутствует в обеих таблицах;

корабль присутствует только в таблице Outcomes.
Этим трем случаям соответствуют следующие строки результирующего набора (показаны только значимые в данном случае столбцы):
Ship
name
NULL
ship_1
ship_2 ship_2
ship_3 NULL
Затем выполняется левое соединение с таблицей классов по предикату:

1. ou.ship = cl.class
OR
2. sh.class = cl.class
То есть либо имя корабля из Outcomes должно совпадать с именем класса
(висящие головные корабли), либо класс корабля из таблицы Ships. Результат соединения можно проиллюстрировать следующей таблицей:
ship
name
sh.class
cl.class
NULL
ship_1 class_1 class_1
ship_2
ship_2 class_1 class_1
ship_3
NULL
NULL
NULL
class_1 NULL class_1 class_1
Третья строка таблицы соответствует случаю, когда класс корабля неизвестен (он не является головным!), а четвертая описывает случай головного корабля, отсутствующего в таблице Ships. Забегая немного вперед, заметим, что третья строка не может нам дать никакой информации о водоизмещении и числе орудий такого корабля, поэтому она отфильтровывается следующим предикатом в предложении WHERE рассматриваемого решения:
1. cl.class
IS
NOT
NULL
Наконец, последнее левое соединение опять выполняется с таблицей классов, но уже по другому предикату:
1. cl.displacement = cl2.displacement
AND
2. cl.numGuns < cl2.numGuns

Первое условие предиката очевидно — мы соединяем строки по равенству значений водоизмещения, так как нам нужно находить максимум в группе кораблей, имеющих одинаковое водоизмещение. Чтобы понять смысл второго условия, опять обратимся к примеру. Дополним нашу таблицу необходимыми столбцами и рассмотрим результат соединения по данному предикату на примере, скажем, первой строки предыдущей таблицы при следующих значениях числа орудий у классов кораблей водоизмещением 30 000 тонн:
class_1
16
class_2
10
class_3
14
shi
p
Nam
e
sh.clas
s
cl.clas
s
displacemen
t
cl2.clas
s
cl.numGun
s
cl2.numGun
s
NUL
L
ship_1 class_1 class_1 30000
NULL
16
NULL
NUL
L
ship_2 class_2 class_2 30000 class_1 10 16
NUL
L
ship_2 class_2 class_2 30000 class_3 10 14
NUL
L
ship_3 class_3 class_3 30000 class_1 14 16
Таким образом, корабли класса, имеющего максимальное число орудий в свой группе по водоизмещению, будут иметь NULL-значение в столбцах, относящихся к таблице cl2 (при левом соединении!), в том числе в столбце cl2.class, так как предикат не удовлетворяется. Именно этот критерий и используется в предложении WHERE для отбора записей, отвечающих условиям задачи (cl2.class IS NULL).
Наконец, оператор CASE формирует имя корабля в окончательном результирующем наборе, выбирая из двух вариантов — столбцы ship или name
— тот, в котором находится не NULL-значение.

Если изложенное выше вам не вполне понятно, почитайте о внешних соединениях в главе 5 (
пункт 5.6.1
).
Интересное решение, но учитывающее не все возможные варианты данных.
Оно не будет давать верный результат, если имеется класс, число орудий на кораблях которого, нам не известно. Обратите внимание на схему данных: столбец numGuns допускает NULL-значения! Предлагаем вам самостоятельно проанализировать причину ошибки и исправить рассмотренное решение.
Упражнение 53
(подсказки и
решения)
Вот решение, в котором сделана попытка выполнить округление:
1.
SELECT
CAST
(
ROUND
(
AVG
(
numguns
)
,
2
)
AS
DECIMAL
(
10
,
2
))
Avg_numGuns
2.
FROM
Classes
3.
WHERE
type =
'bb'
;
Попытка неудачная, так как округление
, как и последующее приведение к типу DECIMAL (впрочем, совершенно излишнее в данном контексте), уже применяется к значению целого типа. В результате получить удается не более чем два нуля после десятичной точки.
Проблема заключается в том, что AVG(numguns) применяется к аргументу целого типа, поэтому и результат приводится к целому, причем дробная часть не округляется, а отбрасывается, что является особенностью
SQL
Server, и не является общим правилом.
Упражнение 54
(подсказки и
решения)

Чтобы написать UNION в решении 3.10.2, недостаточно выполнять объединение по одному столбцу numGuns. Список столбцов должен быть таким, чтобы он однозначно определял корабль.
Тогда действительно исключаться будут дубликаты, а не полезная информация.
Ниже представлено решение, которое использует этот прием, но содержит незначительную ошибку, поиск которой предоставляем читателям.
1.
SELECT
CAST
(
AVG
(
numguns*
1.0
)
AS
NUMERIC
(
6
,
2
))
2.
FROM
(
SELECT
ship, type, numguns
3.
FROM
Outcomes
RIGHT
JOIN
4. Classes
ON
ship = class
5.
UNION
6.
SELECT
name, type, numguns
7.
FROM
Ships s
JOIN
8. Classes c
ON
c.class = s.class
9.
)
AS
al_sh
10.
WHERE
type =
'bb'
;
Упражнение 55
(подсказки и
решения)
Здесь мы приводим решение, которое верно (хотя и излишне) учитывает корабли из таблицы
Outcomes.
1.
SELECT
t1.class,
MIN
(
b.launched
)
2.
FROM
(
SELECT
name, class
3.
FROM
Ships
4.
UNION
5.
SELECT
ship, class
6.
FROM
Outcomes
JOIN

7. Classes
ON
ship = class
AND
8. ship
NOT
IN
(
SELECT
name
9.
FROM
Ships
10.
)
11.
)
t1
LEFT
JOIN
12.
Ships b
ON
t1.class = b.class
13.
GROUP
BY
t1.class;
Единственная ошибка этого решения заключается в том, что не учтена ситуация, когда имеется класс, для которого нет кораблей в текущем состоянии базы данных. Заметим, что это допускается схемой, имеющей связь
«один-ко-многим» между таблицами Classes и Ships.
Упражнение 56
(подсказки и
решения)
Ниже приведено решение, в котором правильно проведен учет потопленных кораблей.
Незначительную ошибку в этом решении предлагаем найти самостоятельно. В случае затруднения вернитесь к решению 3.12.3 1.
SELECT
class,
SUM
(
r
)
sunks
2.
FROM
(
SELECT
name, class,
CASE
3.
WHEN
result =
'sunk'
4.
THEN
1
ELSE
0 5.
END
r
6.
FROM
Ships
AS
s
LEFT
JOIN
7. Outcomes
AS
o
ON
o.ship = s.name
8.
UNION
9.
SELECT
ship, class,
CASE
10.
WHEN
result =
'sunk'
11.
THEN
1
ELSE
0 12.
END
r
13.
FROM
Classes c
JOIN
14.
(
SELECT
*
15.
FROM
Outcomes
16.
WHERE
NOT
Ship
IN
(
SELECT
name

17.
FROM
Ships
)
18.
)
AS
ot
ON
ot.ship = c.class
19.
)
AS
b
GROUP
BY
class;
Упражнение 57
(подсказки и
решения)
Ниже представлено решение, в котором правильно определяется число потопленных кораблей (хотя, на наш взгляд, весьма громоздко), однако неправильно подсчитывается общее число кораблей в классе.
1.
SELECT
f.class,
SUM
(
count_out
)
AS
cnt
2.
FROM
(
SELECT
t.class,
SUM
(
cnt
)
AS
count_out
3.
FROM
(
SELECT
c.class, ship,
COUNT
(
*
)
CNT
4.
FROM
Classes c
LEFT
JOIN
5. Ships s
ON
c.class = s.class
INNER
JOIN
6. Outcomes o
ON
o.ship = s.name
AND
7. result =
'sunk'
8.
GROUP
BY
c.class,ship
9.
)
AS
t
10.
GROUP
BY
t.class
11.
UNION
ALL
12.
SELECT
t.class,
SUM
(
cnt
)
AS
count_out
13.
FROM
(
SELECT
c.class, ship,
COUNT
(
*
)
cnt
14.
FROM
Classes c
INNER
JOIN
15.
Outcomes o
ON
c.class = o.ship
AND
16. o.result =
'sunk'
AND
17.
NOT
EXISTS
(
SELECT
*
18.
FROM
Ships
19.
WHERE
o.ship = name
20.
)
21.
GROUP
BY
c.class,ship
22.
)
AS
t
23.
GROUP
BY
t.class
24.
)
AS
f
25.
GROUP
BY
f.class
26.
HAVING
2
<
(
SELECT
SUM
(
cnt
)
27.
FROM
(
SELECT
COUNT
(
c.class
)
AS
cnt
28.
FROM
Classes c, Ships s
29.
WHERE
c.class = s.class
AND
30. c.class = f.class

31.
UNION
32.
SELECT
COUNT
(
c.class
)
AS
cnt
33.
FROM
Classes c, Outcomes o
34.
WHERE
c.class = o.ship
AND
35. c.class = f.class
AND
36.
NOT
EXISTS
(
SELECT
*
37.
FROM
Ships
38.
WHERE
o.ship = name
39.
)
40.
)
AS
k
41.
)
;
Подсчет общего числа кораблей в классе выполняется здесь в предложении HAVING основного запроса. В подзапросе этого предложения для каждого класса из основного запроса выполняется объединение числа кораблей из таблицы Ships с числом кораблей (головных) из таблицы
Outcomes при условии, что такие корабли не были учтены ранее (их нет в таблице Ships).
Видно, что поскольку объединяются одноатрибутные отношения посредством оператора UNION, то если у нас имеется по одному кораблю и в одном, и другом наборе, мы заведомо получаем неверный результат в результате устранения дубликатов. Однако здесь это как бы не должно являться ошибкой, так как мы отбираем классы, имеющие в сумме более двух кораблей. А других возможных вариантов быть не должно, поскольку головной корабль если и есть, то он только один (несмотря на излишнее использование COUNT во втором запросе). И все же ошибка кроется именно здесь. Дело в том, что если головной корабль принимал участие более чем в одном сражении, то мы его учитываем по числу сражений, разумеется, если его нет в таблице Ships.
Исправить это решение несложно, предлагаем вам сделать это самостоятельно. Однако можно написать и более простой (а также более эффективный) запрос.
Упражнение 59
(подсказки и
решения)
В решении 2.2.1
не учитывается тот факт, что возможна ситуация, когда на некотором пункте
приема может быть только приход, но не быть расхода, то есть в таблице Outcome_o может не быть ни одной строки, относящейся к данному пункту приема. С точки зрения предметной области эта ситуация возможна для вновь открытого пункта приема, когда факт приема денежных средств уже зафиксирован, а факт расхода — еще нет. Тогда выражение в предложении SELECT
1. ss.inc - dd.out для такой точки приема станет эквивалентно выражению
1. ss.inc -
NULL
что даст в результате NULL, а не ss.inc, как это должно быть по условиям задачи.
Исправить решение очень легко, переписав ошибочное выражение в виде
1.
(
COALESCE
(
ss.inc,
0
)
- COALESCE
(
dd.out,
0
)
)
что соответствует стандарту, или с помощью функции
SQL
Server ISNULL:
1.
(
ISNULL
(
ss.inc,
0
)
- ISNULL
(
dd.out,
0
)
)
Упражнение 60
(подсказки и
решения)
В решении 2.3.1
используется полное внешнее соединение (FULL JOIN) подзапросов, чтобы учесть возможные варианты, когда в результате выполнения этих подзапросов для какого-нибудь пункта приема либо сумма прихода, либо сумма расхода будет NULL-значением (другими словами, не было расхода и/или прихода). Если, скажем, полученный приход составляет 1000, а расход —
800, то будут учтены все возможные варианты:

Приход
Расход
1000
800
NULL
800
1000
NULL
Варианта NULL NULL быть не может, так как это бы означало, что пункта приема просто не существовало (на данный момент времени).
В предложении SELECT используется конструкция, которая должна заменить NULL нулем в выражении вычисления остатка. Логика совершенно правильная, однако конструкция применена неверно:
1.
CASE
inc
2.
WHEN
NULL
3.
THEN
0 4.
ELSE
inc
5.
END
Ошибка заключается в том, что здесь фактически задействована простая операция сравнения с NULL-значением, а именно,
1.
CASE
2.
WHEN
inc =
NULL
3.
THEN
0 4.
ELSE
inc
5.
END

Сравнение же с NULL-значением всегда дает UNKNOWN. Поэтому условие WHEN не выполняется, в результате чего выполняется ветвь ELSE, всегда возвращая значение inc, даже в том случае, когда inc есть NULL.
Упражнение 70
(подсказки и
решения)
Здесь мы хотим рассмотреть вполне прозрачное решение, содержащее две незначительные ошибки.
Вот оно (см. комментарии):
1.
SELECT
q.battle
2.
FROM
(
3.
--Определяем корабли из Ships участвовавшие в битвах
4.
SELECT
Outcomes.battle, Outcomes.ship, Classes.country
5.
FROM
Classes
INNER
JOIN
6. Ships
ON
Classes.class = Ships.class
INNER
JOIN
7. Outcomes
ON
Ships.name = Outcomes.ship
8.
UNION
9.
--Определяем головные корабли из Outcomes
10.
SELECT
Outcomes.battle,
Outcomes.ship,
Classes.country
11.
FROM
Outcomes
INNER
JOIN
12.
Classes
ON
Outcomes.ship = Classes.class
13.
)
AS
q
14.
GROUP
BY
q.battle
15.
HAVING
COUNT
(
q.country
)
>=
3
;
Надеемся, что вы без труда их найдете и исправите.
Упражнение 121
(подсказки и
решения)

Беда с этими головными кораблями! Вот как иногда неправильно решается вопрос о головных кораблях, спущенных на воду ранее 1941 года:
1.
SELECT
class
2.
FROM
Classes
3.
WHERE
EXISTS
(
SELECT
1 4.
FROM
Ships
5.
WHERE
launched <
1941
AND
6. Ships.class = Classes.class
7.
)
;
То есть здесь класс отождествляется с наличием головного корабля в БД, а именно, разыскивается класс, который имеет в БД корабль, спущенный на воду ранее 1941 года. Однако из описания предметной области следует, что не всегда есть корабль, имя которого совпадает с именем класса. Поэтому искать неучтенные в Ships головные корабли следует исключительно в таблице
Outcomes.
Наконец, о неучтенном варианте в решении 5.3.3
. Итак, возможна следующая ситуация. Имеется головной корабль с неизвестным годом спуска на воду.
Более того, он может участвовать только в сражениях после 1941 года. Пусть для всех других корабли того же класса год спуска на воду тоже неизвестен
(это допускается схемой данных). Однако, если хотя бы один из этих кораблей участвовал в сражении до 1941 года, то нам следует такой корабль включить в результирующий набор вместе с головным, так как головной корабль (если он есть!) должен быть спущен на воду ранее любого другого корабля аналогичного класса.
Вот решение, которое, казалось бы, учитывает все оговоренные моменты:
1.
-- Корабли, спущенные на воду до 1941 года
2.
SELECT
name
3.
FROM
Ships
4.
WHERE
launched <
1941 5.
6.
UNION
7.
-- Корабли, принимавшие участие в сражениях до 1941 года
8.
SELECT
ship
9.
FROM
Outcomes
JOIN

10.
Battles
ON
Battles.name = Outcomes.battle
11.
WHERE
date <
'19410101'
12.
13.
UNION
14.
-- Головные корабли из Outcomes, в классе которых
есть другие корабли,
15.
-- спущенные на воду до 1941 года
16.
SELECT
ship
17.
FROM
Outcomes
18.
WHERE
ship
IN
(
SELECT
class
19.
FROM
Ships
20.
WHERE
launched <
1941 21.
)
22.
23.
UNION
24.
-- Головные корабли из Outcomes при условии, что
хотя бы один из кораблей
25.
-- того же класса, участвовал в сражении до 1941
года
26.
SELECT
ship
27.
FROM
Outcomes
28.
WHERE
Ship
IN
(
SELECT
class
29.
FROM
Ships
JOIN
30.
Outcomes
ON
Ships.name = Outcomes.ship
JOIN
31.
Battles
ON
Battles.name = Outcomes.battle
32.
WHERE
date <
'19410101'
33.
)
;
Однако система все равно сообщает об ошибке…
Как уже было отмечено в пункте 3.5
, головные корабли с неизвестным годом спуска на воду могут находиться не только в таблице Outcomes, но и в таблице
Ships. Более того, такие корабли не будут учтены рассматриваемым запросом, если их нет в таблице Outcomes, то есть они либо не участвовали в сражениях, либо информация об их участии неизвестна.
Так что здесь чистая логика, и никаких подвохов.

Упражнение (-2)
(подсказки и
решения)
При решении этой задачи часто допускается весьма характерная и, на наш взгляд, принципиальная ошибка, которая имеется и в решении 3.6.2. Однако, чтобы не приводить здесь окончательный вариант и позволить читателю самому его построить, объясним суть ошибки, упростив формулировку задачи:
Определить год, когда на воду было спущено максимальное количество кораблей. Вывод: количество кораблей, год
Определить распределение количества кораблей по годам можно так:
1.
SELECT
launched
[
year
]
,
COUNT
(
*
)
cnt
2.
FROM
Ships
3.
GROUP
BY
launched;
Примечание:
В
SQL
Server имена столбцов заключаются в квадратные скобки ([]),
чтобы избежать неоднозначности. Например, неоднозначность
возникает при использовании пробелов в именах, когда первое слово может
быть истолковано как имя столбца, а второе — как его псевдоним (alias).
Хорошим стилем признается отказ от пробелов в именах, однако, вполне
оправданным является их употребление для формирования заголовков
отчета.
В нашем случае ([year]) квадратные скобки применяются во избежание путаницы с функцией year(), которая возвращает год из аргумента, представленного типом дата-время.
Теперь нам нужно оставить из всех строк, возвращаемых этим запросом, только те, у которых количество (cnt) максимально, то есть:
1. cnt >=
ALL
(
SELECT
COUNT
(
*
)
cnt

2.
FROM
Ships
3.
GROUP
BY
launched
4.
)
Окончательно получим:
Решение 4.19.1
1.
SELECT
*
2.
FROM
(
SELECT
launched
[
year
]
,
COUNT
(
*
)
cnt
3.
FROM
Ships
4.
GROUP
BY
launched
5.
)
x
6.
WHERE
cnt >=
ALL
(
SELECT
COUNT
(
*
)
cnt
7.
FROM
Ships
8.
GROUP
BY
launched
9.
)
;
Тем не менее, здесь кроется ошибка. Эта ошибка не связана с формальным построением решения. Оно не вызывает сомнения. Как это обычно происходит при решении задач на сайте, ошибка заключается в неточном учете особенностей модели предметной области, а именно, ее ограничений. В данном случае допускается, что в базе данных могут быть корабли с неизвестным годом спуска на воду, так как, во-первых, столбец launched допускает NULL-значения и, во-вторых, для головного корабля, который присутствует только в таблице Outcomes, год спуска на воду неизвестен.
Строить корабли — это вам не кроликов разводить. Корабли строятся годами.
Поэтому, если для ряда кораблей год спуска на воду неизвестен (NULL), то велика вероятность того, что число таких кораблей будет больше, чем количество кораблей, спущенных на воду в любом реальном году.
Особенность группировки заключается в том (и это оговорено в стандарте), что NULL-значения трактуются как равные. Следовательно, все корабли с неизвестным годом спуска на воду, будут просуммированы с годом NULL.
Полагаем, что результат не должен включать такую строку по той причине, что неизвестный год не означает один и тот же. С этим можно, конечно, поспорить. Однако все споры сведутся к допустимости использования специфического значения NULL в реляционной модели. Дискуссии по этому поводу ведутся со времен создания этой модели Коддом Е.Ф., которому и принадлежит идея NULL-значения. Однако, насколько нам известно, достойной альтернативы предложено не было.

Возвращаясь к нашей задаче, мы, в знак безграничного уважения к Кодду, внесем в решение следующее изменение:
Решение 4.19.2
1.
SELECT
*
FROM
(
SELECT
launched
[
year
]
,
COUNT
(
*
)
cnt
2.
FROM
Ships
3.
WHERE
launched
IS
NOT
NULL
4.
GROUP
BY
launched
5.
)
x
6.
WHERE
cnt >=
ALL
(
SELECT
COUNT
(
*
)
cnt
7.
FROM
Ships
8.
WHERE
launched
IS
NOT
NULL
9.
GROUP
BY
launched
10.
)
;
В подзапросе предложения WHERE проверку на NULL-значения можно не выполнять, если использовать вместо функции COUNT(*) функцию
COUNT(launched), поскольку в этом случае будут подсчитаны только корабли с известным годом спуска на воду:
1.
WHERE
cnt >=
ALL
(
SELECT
COUNT
(
launched
)
cnt
2.
FROM
Ships
3.
GROUP
BY
launched
4.
)
Для всех же кораблей с неизвестным годом спуска на воду будет получена строка со значением 0, так как если в наборе нет ни одной записи, то функция
COUNT возвращает именно это значение. Последнее не должно нас смущать, поскольку количество кораблей в основном запросе больше нуля, если есть хотя бы один корабль с известным годом спуска на воду. Аналогичным образом можно поступить и в основном запросе, что позволит получить более краткую форму решения:
Решение 4.19.3

1.
SELECT
*
FROM
(
SELECT
launched
[
year
]
,
COUNT
(
launched
)
cnt
2.
FROM
Ships
3.
GROUP
BY
launched
4.
)
x
5.
WHERE
cnt >=
ALL
(
SELECT
COUNT
(
launched
)
cnt
6.
FROM
Ships
7.
GROUP
BY
launched
8.
)
;
Справедливости ради следует отметить, что стоимость решения 4.19.3 по его плану выполнения в
SQL
Server 2000 будет незначительно уступать (в третьей значащей цифре) стоимости решения 4.19.2.
Примечание:
Стоимость любого запроса к учебным базам данных, а также
процедурный план его выполнения можно посмотреть на
странице сайта
.
Язык
манипуляции
данными в SQL
…язык учителя дзэн передает идеи, а не чувства или намерения. И поэтому он играет не ту роль, какую обычно играет язык; поскольку выбор фраз исходит от учителя, то чудо свершается в той области, которая ему присуща, и ученик раскрывается сам себе, понимает себя, и таким образом обычная фраза становится ключом.
Х. Кортасар. Игра в классики.
Данная книга ориентирована на практическое применение языка
SQL
, то есть в первую очередь на использование оператора SELECT, реализующего выборку данных из реляционных
СУБД, и операторов INSERT, UPDATE и DELETE, которые служат для модификации данных. Эти операторы и составляют тот подъязык SQL, который называется языком манипуляции данными (или DML — Data

Manipulation Language). В этой «теоретической» части мы старались придерживаться тех синтаксических конструкций рассматриваемых операторов, которые, во-первых, полностью соответствуют стандарту SQL-
92, и, во-вторых, поддерживаются практически всеми коммерческими СУБД.
Естественно, нельзя было обойти вниманием и некоторые особенности реализации. Это обусловлено тем, что имеющиеся в каждой реализации расширения стандарта позволяют зачастую более компактно написать запрос, а также тем, что некоторые моменты не стандартизируются и, как правило, опускаются на уровень реализации. Поскольку
сайт
работает с MS
SQL
Server
2005, то особенности реализации, которые оговариваются отдельно, относятся по большей части именно к этому продукту. В частности, это касается функций работы со строками и значениями типа даты/времени, которые рассматриваются в
главе 7
(
1   ...   9   10   11   12   13   14   15   16   ...   47


написать администратору сайта