Упражнение 54

С точностью до двух десятичных знаков определите среднее число орудий всех линейных кораблей (учесть корабли из таблицы Outcomes).

Решение 3.10.1

SELECT ROUND(SUM(ng)/SUM(cnt), 2) res
FROM (SELECT SUM(numGuns) ng, COUNT(*) cnt
    FROM Classes c,Ships s
    where c.class = s.class 
        AND c.type = 'bb'
    UNION ALL
    SELECT SUM(numGuns) ng, count(*) cnt
    FROM Classes c, Outcomes o
    WHERE c.class = o.ship 
        AND c.type = 'BB'
        AND NOT EXISTS (SELECT name 
                        FROM Ships s
                        WHERE s.name = o.ship)
    ) x;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

В этом решении сделана попытка вручную посчитать среднее как сумму значений, деленную на их количество. Однако специфика арифметических операций в SQL Server состоит в том, что результат всегда приводится к типу аргумента. Поскольку число орудий — целое число (тип INTEGER для столбца numGuns), то дробная часть числа, полученного при делении, будет попросту отбрасываться, что заведомо даст неправильный результат.

Важно

Использование функции AVG для вычисления среднего не меняет ситуацию, так как приведение типа проводится по тем же правилам.

Это легко проверить, если выполнить запрос

SELECT AVG(a) from (values(1),(2))X(a); -- 3/2
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
который даст 1, а не 2, если бы выполнялось округление.

Если вы будете выполнять аналогичные запросы на сайте sql-ex.ru, поставьте флажок «Без проверки» на странице с упражнениями, чтобы система не выполняла бесполезного сравнения результата с эталонным решением соответствующего упражнения.

Для получения «точного» результата деления целых чисел нужно привести операнд (хотя бы один) к вещественному типу. Это можно сделать с помощью функции приведения типа CAST или простым умножением на вещественную единицу, как мы и поступим:

SELECT SUM(numGuns*1.0) ng

Теперь поговорим об округлении, которое использует функцию T-SQL ROUND. Опять обратимся к простому примеру (округление до двух цифр после десятичной точки):

SELECT ROUND(AVG(5.0/3),2);
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
который даст нам 1.670000 в качестве результата. То есть округление выполнено правильно, но сохранены незначащие нули, количество которых соответствует числу значащих цифр, используемых по умолчанию для представления вещественного числа. Это число, естественно, зависит от реализации, поэтому в данном случае мы говорим лишь об SQL Server. Здесь уместно заметить, что при сравнении результатов с «правильным» решением значения 1.67 и 1.670000 будут считаться разными, поскольку выполняется посимвольное сравнение. Поэтому нужно позаботиться еще и об удалении этих нулей. Отложим этот вопрос до анализа следующего решения, так как там эта проблема, как и проблема округления, решена верно. Там же мы рассмотрим и логическую ошибку, которую содержит решение 3.10.1.

Решение 3.10.2

SELECT CAST(AVG(numGuns*1.0) AS NUMERIC(10,2))
FROM (SELECT numguns
    FROM Classes c 
        JOIN Ships s ON c.class = s.class
    WHERE type = 'bb'
    UNION ALL
    SELECT numguns
    FROM Classes] c 
        JOIN Outcomes o ON c.class = o.ship
    WHERE type='bb' 
        AND o.ship NOT IN(SELECT name
                        FROM Ships
                        )
    ) t;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

Обратите внимание на приведение типа к числу с фиксированной точкой, которое и выполняет требуемое округление результата.

В подзапросе объединяются (UNION ALL) два запроса. Первый определяет число орудий для кораблей в таблице Ships, принадлежащих классам линейных кораблей (тип bb). Второй учитывает головные корабли соответствующих классов при условии, что их нет в таблице Ships.

Таким образом, сделана попытка учесть каждый корабль в БД только один раз. Поскольку для объединения используется UNION ALL, то дубликаты устраняться не будут. Это совершенно справедливо, так как многие корабли будут иметь одинаковое число орудий, а в предложении SELECT подзапроса выводится только этот столбец.

И все же ошибка связана именно с использованием UNION ALL. Поступим формально, то есть не будем домысливать предметную область, а обратимся к схеме. В таблице Ships первичным ключом является имя корабля, поэтому первый запрос в объединении даст нам по одной строке на каждый корабль известного класса. В таблице же Outcomes ключом является пара {ship, battle}, то есть уникальность обеспечивается для комбинации имени корабля и сражения, в котором он принимал участие. Отсюда следует, что один и тот же корабль может несколько раз упоминаться в таблице Outcomes, что соответствует участию данного корабля в нескольких сражениях.

В результате второй запрос в объединении даст дубликаты кораблей, если головной корабль участвовал в нескольких сражениях. Это и делает ошибочным представленное решение.

С другой стороны, и UNION вместо UNION ALL мы написать не можем по указанной выше причине.

ПиР

Решить задачу на SQL-EX.RU