Предложение SQL «с» против внутреннего соединения (выберите …)

#sql #sql-server

#sql #sql-сервер

Вопрос:

Я создал простую скрипку SQL, чтобы попытаться легче проиллюстрировать мою проблему.

Учитывая следующую структуру базы данных:

 create table cities (id int, name varchar(100), state varchar(2), population int);
create table citizens (id int, name varchar(100), age int, cityId int);

insert into cities values (1, 'Dallas', 'TX', 100);
insert into cities values (2, 'Houston', 'TX', 200);

insert into citizens values (1, 'John', 20, 1); --Dallas
insert into citizens values (2, 'Peter', 30, 1); --Dallas
insert into citizens values (3, 'Paul', 40, 2); --Houston
insert into citizens values (4, 'Steven', 50, 2); --Houston
 

Если я хочу выбрать сумму всех жителей городов Техаса и средний возраст его граждан, я бы сначала сделал что-то вроде:

 select sum(cities.population), avg(citizens.age)
from cities
inner join citizens on citizens.cityId = cities.id
where cities.state = 'TX'
 

Он возвращает 600 и 35 . Средний возраст указан правильно, но сумма населения — нет. Это потому, что для каждого города он возвращает две строки, по одной для каждого гражданина, что в конечном итоге удваивает правильный результат, который был бы 300 .

Я придумал два разных решения этой проблемы, и я хочу знать «лучшее» из них или более рекомендуемое (то есть то, которое работает лучше) — или даже другое решение, если кто-нибудь может мне его предложить.

решение # 1

 select sum(cities.population), avg(citizens.age)
from cities
inner join (select cityId, avg(age) as age from citizens group by cityId) as citizens 
on citizens.cityId = cities.id
where cities.state = 'TX'
 

решение # 2

 with temp as
(select cityId, avg(age) as age from citizens group by cityId)

select sum(cities.population), avg(temp.age)
from cities
inner join temp on temp.cityId = cities.id
where cities.state = 'TX'
 

Мне кажется (и я могу в этом совершенно ошибаться), что с with помощью предложения он сначала выбирает всю citizens таблицу, создавая временную таблицу с результатом, а затем выполняет основной выбор (выше), соединяясь с этой временной таблицей. Но я не уверен inner join (select...) в решении # 1.

Примечание: это для Microsoft SQL Server 2019

Комментарии:

1. Используйте ключевое слово DISTINCT в своем первом запросе?

2. С точки зрения оптимизации, я думаю, что предложение With (то, что мы называем общим табличным выражением CTE) практически совпадает с версией inner join (Select …) с точки зрения того, что на самом деле происходит за кулисами. Синтаксис With просто более полезен, если вы собираетесь ссылаться на CTE более одного раза

3. @codeulike Это не «почти то же самое», это точно то же самое

Ответ №1:

Первый метод близок, но не дает правильных результатов — среднее значение средних значений не является общим средним значением. Вместо этого используйте sum() и count() и вручную вычислите среднее значение.

Вторая проблема заключается в том, что города без граждан (если таковые имеются) будут отфильтрованы. Итак, я бы предложил:

 select sum(c.population), sum(cz.sum_age) / sum(cz.cnt)
from cities c left join
     (select cityId, sum(age) as sum_age, count(*) as cnt
      from citizens cz
      group by cz.cityId
     ) cz 
     on cz.cityId = c.id
where c.state = 'TX';
 

Ответ №2:

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

Вы можете убедиться в этом, сравнив планы запросов. Компилятор не видит CTE как что-либо иное, кроме другого способа размещения производной таблицы / подзапроса, и оптимизирует его любым возможным способом. Вы вряд ли увидите точную настройку CTE в буфере таблицы в плане запроса.

Существует еще одна конструкция, которую вы не использовали: an APPLY :

 select sum(c.population), sum(cz.sum_age) / sum(cz.cnt)
from cities c
outer apply    -- or cross apply
     (select cityId, sum(age) as sum_age, count(*) as cnt
      from citizens cz
     where cz.cityId = c.id
      group by ()
     ) cz 
where c.state = 'TX';
 

Сформулированное таким образом, суммирование концептуально выполняется по одному запросу citizens таблицы.

Однако компилятор обычно может преобразовать формы join и apply друг в друга и выберет лучшую, поэтому обычно это не имеет значения.

Ответ №3:

Я придумал два разных решения этой проблемы, и я хочу знать «лучшее» из них или более рекомендуемое (то есть то, которое работает лучше) — или даже другое решение, если кто-нибудь может мне его предложить.

На моем месте я бы выполнил два отдельных запроса, потому что вы в основном пытаетесь вычислить два разных агрегата из двух разных областей — city и citizen. С таким же успехом может быть два запроса, их намного легче читать.

 select sum(cities.population)
from cities
where cities.state = 'TX'

select avg(citizens.age)
from cities
inner join citizens on citizens.cityId = cities.id
where cities.state = 'TX'