← Все курсы · Курс SQL · Все руководства
Подзапрос против CTE в SQL: та же логика, которую вы можете проверить
К концу этой страницы вы сможете написать запрос, который использует ответ на другой запрос, распознать четыре места, куда можно пойти, определить коррелированный тип, который выполняется один раз для каждой строки, избежать NOT IN который молча ничего не возвращает, и преобразует любое гнездо в именованные шаги, которые вы можете считать по одному. Прошло около двадцати пяти минут, и каждый запрос и результат, представленные ниже, были выполнены.
Вот что нужно сделать сегодня с самым вложенным запросом, который у вас есть. Возьмите самый внутренний подзапрос, поднимите его наверх как CTE с настоящим именем и запустите SELECT COUNT(*) против этого имени. Теперь вы знаете, со сколькими строками работает середина вашего запроса, и это единственное число, которое вам не даст вложенная версия.
Краткая версия: подзапрос — это запрос, используемый в виде значения, списка или таблицы. CTE — это тот же запрос, которому заранее присвоено имя. Логика идентична и только один из них можно проверить по частям.
Эта разница и есть идея, поэтому она и отражает картину.
- Что такое подзапрос и в каких четырех местах он находится
- Скалярный подзапрос: одно значение
- IN и NOT IN, которые ничего не возвращают
- Коррелированные подзапросы: один запуск на строку
- Производная таблица и места, где она перестает быть читаемой
- CTE: тот же запрос с именем
- Считая каждый шаг
- Когда оконная функция превосходит обе
- Полное до и после
- Краевые случаи, которые нарушают подзапрос
- Почему это работает
- Использование этого в вашем собственном проекте
- Все на одном экране
1. Что такое подзапрос и четыре места, в которых он находится
Подзапрос – это SELECT написано внутри другого оператора, заключенного в скобки. Что это такое для зависит от того, куда вы его поставите, а мест всего четыре.
| где | Что он должен вернуть | Что он делает |
|---|---|---|
в WHERE, против = или > | одно значение | Указывая номер, который в противном случае вы бы сначала искали |
в WHERE, против IN | Один столбец, любое количество строк | Предоставление списка для сопоставления |
в SELECT | одно значение | Размещение суммы или ставки рядом с каждой строкой |
в FROM | Целый стол | Запрос результата запроса |
Все остальное на этой странице — одно из этих четырех с вариациями. Зная, какой из четырех вы пишете, вы сразу же поймете, какую форму должен вернуть подзапрос, а большинство ошибок подзапроса — это несоответствие формы: список, в котором ожидалось одно значение, или один столбец, в котором была таблица.
2. Скалярный подзапрос: одно значение
Прежде чем объяснить: вы хотите, чтобы заказы стоили больше среднего заказа. Скажи, почему ты не можешь писать WHERE revenue > AVG(revenue).
Потому что WHERE выполняется перед любым агрегированием, поэтому на момент его оценки еще нет среднего значения. Подзапрос решает эту проблему, сначала вычисляя среднее значение в виде отдельного полного запроса.
SELECT COUNT(*) AS orders, SUM(units*unit_price) AS revenue
FROM orders
WHERE units*unit_price > (SELECT AVG(units*unit_price) FROM orders);
8 | 6475
Восемь из шестнадцати заказов общей стоимостью 6475 долларов. Само среднее составляет 9890 ÷ 16, что составляет 618,125, а восьмёрки — те, что выше него.
Подзапрос, который возвращает ровно одно значение, называется скаляр, и его можно использовать везде, где может находиться число. Сюда входит список выбора, в котором рядом с каждой строкой помещается цифра всей таблицы:
SELECT order_id,
units*unit_price AS revenue,
ROUND(100.0*(units*unit_price)
/ (SELECT SUM(units*unit_price) FROM orders), 1) AS pct
FROM orders ORDER BY revenue DESC LIMIT 3;
1010 | 1100 | 11.1
1001 | 880 | 8.9
1015 | 880 | 8.9
Самый крупный заказ составляет 11,1% от всего. Проверьте это: 1100 ÷ 9890 равно 0,1112. Подзапрос выполнялся один раз, а не один раз для каждой строки, поскольку он не зависит от строки, рядом с которой он находится. Это различие составляет весь четвертый раздел.
3. IN и NOT IN, которые ничего не возвращают.
IN принимает список, а не значение, поэтому подзапрос возвращает один столбец и столько строк, сколько ему нужно.
SELECT COUNT(*) FROM orders
WHERE order_id IN (SELECT order_id FROM cancelled);
2
Два из шестнадцати заказов находятся в списке отмененных. Теперь очевидный следующий шаг, и именно ради него нужно притормозить.
Перед пояснением: отмененная таблица содержит три строки, 1004, 1009 и один NULL. Предскажите, сколько из шестнадцати заказов вернутся не отмененными.
SELECT COUNT(*) FROM orders
WHERE order_id NOT IN (SELECT order_id FROM cancelled);
0
Ноль. Не четырнадцать. Каждая строка исчезла, и ничего не произошло.
Причина в том, что x NOT IN (a, b, NULL) расширяется до x <> a AND x <> b AND x <> NULLи сравнение с NULL никогда не является правдой, это неизвестно. Ан AND цепочка, содержащая неизвестное, никогда не может оказаться истинной, поэтому ни одна строка не подходит. Полный отчет о том, почему неизвестный ведет себя таким образом, находится в NULL в SQL, и это самое дорогое его последствие.
Два исправления, оба проверены на одних и тех же данных:
-- exclude the NULLs from the list
WHERE order_id NOT IN (SELECT order_id FROM cancelled WHERE order_id IS NOT NULL)
14
-- or use NOT EXISTS, which is immune by construction
WHERE NOT EXISTS (SELECT 1 FROM cancelled c WHERE c.order_id = o.order_id)
14
Предпочитать NOT EXISTS. Его нельзя нарушить, если в следующем месяце в списке появится нуль, обычно он также планирует, по крайней мере, и читается так, как вы имеете в виду: нет соответствующей строки. Держать IN для положительного случая, когда ноль в списке безвреден.
4. Коррелирующие подзапросы: один запуск на строку.
Подзапрос, который ссылается на столбец из внешнего запроса, не может быть вычислен один раз, поскольку его ответ различен для каждой внешней строки. Это коррелированный подзапрос.
Вопрос: какие заказы превосходят средние для своего региона? Вот средние показатели по региону, так что есть с чем сравнить.
East | 4 orders | 3040 | 760.00
North | 5 orders | 2495 | 499.00
South | 4 orders | 2670 | 667.50
West | 3 orders | 1685 | 561.67
SELECT o.order_id, o.region, o.units*o.unit_price AS revenue
FROM orders o
WHERE o.units*o.unit_price > (
SELECT AVG(i.units*i.unit_price)
FROM orders i
WHERE i.region = o.region)
ORDER BY o.region, revenue DESC;
1010 | East | 1100
1001 | North | 880
1014 | North | 510
1015 | South | 880
1002 | South | 850
1012 | West | 765
Шесть заказов. Проверьте первый: среднее значение Норта равно 499, а заказы Норта равны 880, 510, 440, 425 и 240, поэтому ровно два его очищают. Внутренний запрос выполнялся шестнадцать раз, по одному разу для каждой внешней строки, каждый раз фильтруясь по региону этой строки.
Далее следуют две вещи. Читается хорошо, потому что WHERE i.region = o.region линия говорит вслух, что означает «свой регион». И это самая медленная фигура на этой странице, потому что это петля: на шестнадцати строках никто не замечает, на шестнадцати миллионах это разница между секундой и перерывом на кофе. В восьмом разделе есть версия, выполняющая ту же работу за один проход.
5. Производная таблица и места, где она перестает читаться
Подзапрос в FROM возвращает целую таблицу, которую внешний запрос затем обрабатывает так, как если бы она была единой. Ему должен быть присвоен псевдоним, и именно эта форма позволяет фильтровать только что вычисленный агрегат.
SELECT region, revenue FROM (
SELECT region, SUM(units*unit_price) AS revenue
FROM orders GROUP BY 1
) t
WHERE revenue > 2500
ORDER BY revenue DESC;
East | 3040
South | 2670
Это работает, и это нормально на одном уровне. Проблемы начинаются с двух, потому что порядок чтения инвертируется: запрос выполняется изнутри наружу и записывается снаружи внутрь, поэтому вы прокручиваете вниз, чтобы найти начало. На трёх уровнях, с объединением посередине, его вообще никто не читает, включая того, кто его написал.
Существует также практическое ограничение, о котором стоит знать, прежде чем оно укусит. Производную таблицу можно использовать только один раз. Если двум частям вашего запроса нужен один и тот же промежуточный результат, вы пишете его дважды, а затем кто-то редактирует одну копию.
6. CTE: тот же запрос с именем
Общее табличное выражение — это тот же подзапрос, перемещенный наверх и получивший имя с помощью WITH. Вот производная таблица из пятого раздела, не изменившаяся по смыслу:
WITH by_region AS (
SELECT region, SUM(units*unit_price) AS revenue
FROM orders GROUP BY 1
)
SELECT region, revenue
FROM by_region
WHERE revenue > 2500
ORDER BY revenue DESC;
East | 3040
South | 2670
Идентичный выход. Что изменилось, так это то, что шаги теперь расположены в том порядке, в котором они происходят, промежуточный результат имеет имя, выбранное пользователем, а конечный результат SELECT достаточно короток, чтобы прочитать его за один раз.
Они складываются друг в друга, разделенные запятыми, и каждый из них может использовать те, что расположены над ним:
WITH big AS (
SELECT * FROM orders WHERE units*unit_price > 600
), by_rep AS (
SELECT rep, COUNT(*) AS orders, SUM(units*unit_price) AS revenue
FROM big GROUP BY 1
)
SELECT rep, orders, revenue FROM by_rep ORDER BY revenue DESC;
Priya Shah | 3 | 2440
Owen Park | 3 | 2390
Dana Reyes | 1 | 880
Sam Okafor | 1 | 765
Восемь крупных заказов, распределенных в четырех направлениях. При чтении сверху вниз анализ описывается в том порядке, в котором кто-то объяснил бы его вслух: сначала возьмите большие заказы, затем сгруппируйте их по представителям, затем отсортируйте. Вложенная версия того же самого говорит, что сначала последний шаг.
Именованный шаг также можно использовать несколько раз, чего не может сделать производная таблица. Руководство по CTE идет дальше в синтаксис и строит запрос на проверяемых этапах; на этой странице рассказывается о том, когда использовать его вместо подзапроса.
7. Считаем каждый шаг
Это практическая причина предпочитать CTE, и она важнее, чем просто читаемость.
Когда вложенный запрос возвращает неправильный ответ, у вас есть одно число, последнее, и нет возможности определить, какой слой пошёл не так. С помощью именованных шагов вы можете указать SELECT COUNT(*) в любом из них, изменив последнюю строку:
WITH big AS (
SELECT * FROM orders WHERE units*unit_price > 600
), by_rep AS (
SELECT rep, COUNT(*) AS orders, SUM(units*unit_price) AS revenue
FROM big GROUP BY 1
)
SELECT COUNT(*) FROM big; -- 8
-- SELECT COUNT(*) FROM by_rep; -- 4
Пришло шестнадцать заказов, восемь прошли фильтр, четыре строки вышли из группировки. Запишите эти три числа, прежде чем запускать реальный запрос, а затем сравните. Фильтр, который должен был удалить несколько и большую часть таблицы, сразу же объявляет о себе, а группировка, которая создает больше строк, чем категорий, означает, что соединение что-то умножило.
Произнесите вслух, как выглядит эквивалентная отладка для трехуровневого вложенного запроса. Вы комментируете внешние слои по одному, редактируя запрос для его проверки, а это означает, что то, что вы тестируете, больше не является тем, что было неправильным. Именованные шаги позволяют просматривать, не меняя ничего выше последней строки.
8. Когда оконная функция превосходит обе
Связанный подзапрос из четвертого раздела спрашивал: превышает ли этот порядок средний показатель по региону? Вот тот же вопрос без подзапроса.
SELECT order_id, region, revenue FROM (
SELECT order_id, region,
units*unit_price AS revenue,
AVG(units*unit_price) OVER (PARTITION BY region) AS region_avg
FROM orders)
WHERE revenue > region_avg
ORDER BY region, revenue DESC;
1010 | East | 1100
1001 | North | 880
1014 | North | 510
1015 | South | 880
1002 | South | 850
1012 | West | 765
Те же шесть заказов. AVG(...) OVER (PARTITION BY region) вычисляет среднее значение для каждого региона один раз и присоединяет его к каждой строке этого региона за один проход вместо выполнения отдельного запроса для каждой строки.
Итак, практическое правило: всякий раз, когда коррелированный подзапрос вычисляет агрегат по группе, к которой принадлежит внешняя строка, оконная функция делает это быстрее и обычно более четко. Оконные функции охватывает все остальное, что OVER могу сделать. Связанные подзапросы сохраняют свое место для проверок существования и логики, которую окно не может выразить.
Представьте себе самый медленный запрос, за который вы отвечаете. Есть ли внутри него подзапрос, в котором упоминается столбец из внешнего запроса? Эта линия показывает, куда движется время.
Полное до и после
Тот же вопрос: какие представители выполняют крупные заказы?
До
SELECT rep, COUNT(*), SUM(revenue) FROM (
SELECT rep, units*unit_price AS revenue FROM orders
WHERE units*unit_price > (SELECT AVG(units*unit_price) FROM orders)
) x GROUP BY 1 ORDER BY 3 DESC;
Правильно, и читать его нужно от середины к краям. Порог заглублен двумя уровнями ниже. Невозможно узнать, сколько строк выдержало фильтр, не редактируя запрос, и производная таблица называется x, что ничего не говорит следующему читателю.
после
WITH avg_order AS (
SELECT AVG(units*unit_price) AS v FROM orders -- 618.125
), big AS (
SELECT * FROM orders
WHERE units*unit_price > (SELECT v FROM avg_order) -- 8 rows
), by_rep AS (
SELECT rep, COUNT(*) AS orders, SUM(units*unit_price) AS revenue
FROM big GROUP BY 1 -- 4 rows
)
SELECT rep, orders, revenue FROM by_rep ORDER BY revenue DESC;
Priya Shah | 3 | 2440
Owen Park | 3 | 2390
Dana Reyes | 1 | 880
Sam Okafor | 1 | 765
Тот же ответ, читаем сверху вниз, с указанным пороговым значением и тремя промежуточными значениями, которые вы можете проверить отдельно. 2440 плюс 2390 плюс 880 плюс 765 — это 6475, то есть вторая часть фигуры, полученная из всей таблицы, поэтому между шагами ничего не потерялось.
Претензия, и она является причиной конвертации: вложенный запрос дает вам одно число для проверки, а именованный запрос дает вам число за шаг, что является разницей между знанием того, что ответ неправильный, и знанием того, где.
Краевые случаи, которые нарушают подзапрос
Шесть, которые стоит знать.
Скалярный подзапрос, возвращающий более одной строки. Хорошо, пока данные не изменятся, а затем ошибки запроса. Если вы ожидаете, что он вернет одну строку, скажите об этом с помощью LIMIT 1 и ORDER BYили агрегировать его.
Скалярный подзапрос, который не возвращает строк. Это дает NULL, это не ошибка, а NULL при сравнении все условие становится неизвестным, поэтому строки незаметно исчезают, а не появляются сообщения.
NOT IN против всего, что можно обнулить. Раздел третий. Предположим, что каждый столбец имеет значение NULL, если в схеме не указано иное, и используйте NOT EXISTS.
Связанный подзапрос к большой таблице. Это петля. Если запрос стал медленным после того, как кто-то добавил фильтр, ссылающийся на внешнюю таблицу, вот почему.
Один и тот же CTE использовался дважды. Некоторые движки пересчитывают его каждый раз, когда к нему обращаются, а некоторые материализуют его один раз, и то, какой из них делает ваш, влияет как на скорость, так и, если CTE содержит что-то недетерминированное, на ответ. Проверьте поведение вашего двигателя, прежде чем полагаться на него.
Псевдоним, который затеняет таблицу. Если в коррелированном подзапросе внутренние и внешние таблицы не имеют явного псевдонима, WHERE region = region сравнивает столбец сам с собой и всегда имеет значение Верно, поэтому фильтр ничего не делает. Всегда используйте псевдонимы обоих, т.к. o и i выше.
Почему это работает
Вложенность — это не случайность языка, это замысел. Предок SQL был построен на идее блока запроса, который мог появиться везде, где только может быть значение или таблица, так что сложный вопрос можно было составить из более простых, не изобретая новый синтаксис для каждой комбинации (Чемберлен & Бойс, 1974, Материалы семинара ACM SIGFIDET 1974 г. по описанию данных, доступу и контролю., 249–264). Вот почему существует ровно четыре места, куда может попасть подзапрос: это четыре места, где может появиться значение или таблица. И это объясняет, почему CTE не является отдельной функцией. Это тот же блок, которому присвоено имя и он перемещен вперед, поэтому преобразование между ними никогда не меняет ответ.
тот NOT IN сбой также является следствием конструкции, а не ошибкой. Пропущенное значение явно не совпадает с любым другим значением, включая само себя, поэтому сравнение с его участием дает неизвестное, а не истинное или ложное значение. AND цепочка, содержащая неизвестное, не может оказаться верной (Кодд, 1979, Транзакции ACM в системах баз данных, 4(4), 397–434). Как только вы примете это правило, обнулите строки из NOT IN неудивительно, это вынужденно. Причина, по которой это всех привлекает, заключается в том, что IN ведет себя совершенно нормально в том же списке, поэтому ошибка появляется только при отрицании.
Одно замечание в шпаргалке ниже. Он создан для того, чтобы его просматривали и вспоминали, а не перечитывали, поскольку самотестирование превосходит повторное обучение достаточно надежно, чтобы проявляться в широком диапазоне реальных условий в классе и на рабочем месте, а не только в лаборатории (Агарвал, Нуньес & Тупой, 2021, Обзор педагогической психологии, 33(4), 1409–1453).
Использование этого в вашем собственном проекте
Переписывать каждый запрос в репозитории — это жалко и бессмысленно. Вместо этого сделайте это по порядку.
- Найдите по своим запросам
NOT IN (SELECTи измените каждый наNOT EXISTS. Это исправление правильности, а не стиля, и оно механическое. - Найдите любой запрос, вложенный более чем в один уровень и конвертировать только этот. Два уровня — это то место, где читаемость падает с обрыва.
- Назовите каждый CTE в честь одной из его строк.:
big_orders,by_rep,first_purchase. Нетt,xилиcte1. - Напишите ожидаемое количество строк в качестве комментария к каждому шагу. прежде чем запускать, проверьте. Эта привычка находит больше ошибок, чем чтение SQL.
- Ищите коррелированные подзапросы во всем медленноми замените агрегатные функции оконной функцией.
- Оставьте рабочие одноуровневые подзапросы в покое. Скалярный подзапрос в
WHEREПункт ясен и не нуждается в церемониях.
Если у вас под рукой есть бумага, один дополнительный эскиз стоит пяти минут. Нарисуйте запрос в виде блоков, по одному на шаг, со стрелкой между ними и напишите рядом с каждым блоком, сколько строк вы ожидаете получить из него. Этот рисунок является версией CTE, и его последующий ввод в основном представляет собой транскрипцию.
Все на одном экране
Это лист поиска. Закройте правую колонку, проработайте левую и произнесите каждый ответ вслух, прежде чем проверять его.
| вещь | Что он делает |
|---|---|
| Подзапрос | SELECT, используемый как значение, список или таблица. |
| Четыре места | ГДЕ со сравнением, ГДЕ с IN, SELECT, FROM. |
| Скалярный подзапрос | Возвращает одно значение. Можно использовать везде, где может быть номер. |
Почему нет WHERE x > AVG(x) | WHERE выполняется перед агрегацией. Среднего пока нет. |
IN (SELECT ...) | Один столбец, любое количество строк. |
NOT IN с НУЛЕМ | Возвращает ноль строк. Никакой ошибки. |
| Исправление | NOT EXISTSили отфильтруйте NULL-значения из списка. |
| Связанный подзапрос | Упоминает внешний столбец, поэтому он выполняется один раз для каждой внешней строки. |
| Его стоимость | Петля. Незаметен на маленьких столах, дорогой на больших. |
| Производная таблица | Подзапрос в FROM. Нужен псевдоним. Можно использовать один раз. |
| CTE | WITH name AS (...). Тот же запрос с именем вверху. |
| Укладка | Через запятую. Каждый может использовать те, что выше него. |
| Почему стоит выбрать CTE | Каждый шаг получает имя и количество строк, которые вы можете проверить. |
| Отладка CTE | Измените последнюю строку на SELECT COUNT(*) FROM step. |
| Коррелированный агрегат | Заменить на AVG(...) OVER (PARTITION BY ...). |
| Скаляр не возвращает строк | NULL, поэтому сравнение неизвестно и строки исчезают. |
| Псевдоним обеих таблиц | или WHERE region = region всегда верно. |
Еще одна мысль, и мне бы искренне хотелось услышать ответы других людей. Больше всего мне стоило NOT IN против списка поставщиков, который был в порядке в течение года, пока одна запись о поставщике не была создана с пустым кодом, и в отчете об исключении в одночасье количество строк уменьшилось с нескольких сотен до нуля, и никто не заметил, что пустой отчет является необычным. Что с вами незаметно сделал подзапрос?
Ссылки
- Чемберлин Д.Д. и Бойс Р.Ф. (1974). ПРОДОЛЖЕНИЕ: структурированный английский язык запросов. Материалы семинара ACM SIGFIDET 1974 г. по описанию данных, доступу и контролю., 249–264.
- Кодд, EF (1979). Расширение реляционной модели базы данных для более глубокого понимания. Транзакции ACM в системах баз данных, 4(4), 397–434.
- Агарвал П.К., Нуньес Л.Д. и Блант Дж.Р. (2021). Практика поиска неизменно приносит пользу обучению студентов: систематический обзор прикладных исследований. Обзор педагогической психологии, 33(4), 1409–1453.
SQL для аналитиков составляет 458 страниц, сравниваются подзапросы и CTE, включая то, что каждый может видеть, а что нет.
SQL для аналитиков, 19 $ →тот Курс SQL дает вам повторы: подзапросы во всех четырех позициях, коррелированные для преобразования и НЕ В ловушках, в которые нужно попасть намеренно, а также флеш-карточки именно той формы, которая требуется для каждой позиции. Теперь вы владеете методом; комплект - это практика. Ничего устанавливать, учетная запись не требуется.
Откройте набор SQL →