Menu

GROUP BY и HAVING в SQLite: фильтрация групп

Разбираемся, как GROUP BY группирует строки в SQLite, а HAVING фильтрует уже агрегированные результаты. Заодно ставим точку в споре WHERE vs HAVING.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

GROUP BY: как сгруппировать строки в «корзины»

Агрегатные функции SQL — COUNT, SUM, AVG — сворачивают множество строк в одно число. А GROUP BY позволяет делать это по категориям: одно число на каждого клиента, на каждый месяц, на каждый статус. Каждое уникальное значение (или комбинация значений) превращается в отдельную строку результата.

Три клиента — три строки на выходе. Шесть исходных строк исчезли: они схлопнулись в корзинки по клиентам, и внутри каждой посчитались COUNT(*) и SUM(amount).

Логика тут простая. GROUP BY customer говорит: «считай все строки с одинаковым клиентом одной группой». А агрегатные функции дальше работают уже отдельно по каждой такой группе.

Что можно класть в список SELECT

Вот на этом моменте многие спотыкаются. Когда вы используете GROUP BY, каждая колонка в SELECT должна либо присутствовать в GROUP BY, либо быть обёрнута в агрегатную функцию. Иначе непонятно, какое именно значение брать — из какой строки группы его тянуть?

Если написать SELECT region, rep, SUM(amount) с GROUP BY region, SQLite спокойно это выполнит (он более снисходителен, чем другие СУБД, которые такой запрос отвергнут), но значение rep будет выбрано из группы произвольно. На каждый регион вы получите по одному имени менеджера, причём без всякой гарантии, какое именно. На это полагаться нельзя — добавляйте в GROUP BY все неагрегированные столбцы, которые показываете.

HAVING — фильтрация групп после агрегации

WHERE отсеивает строки до группировки, а HAVING фильтрует уже сами группы после неё. В этом и состоит вся разница between WHERE и HAVING, и именно поэтому условие вроде COUNT(*) > 1 нельзя засунуть в WHERE — на момент его выполнения счётчика ещё попросту не существует.

У Клео всего один заказ, поэтому её группа отсеивается. Остаются Ада и Борис. Условие проверяется по агрегированному значению каждой группы, а не по отдельным строкам.

В HAVING можно прямо обращаться к псевдонимам колонок из SELECT — SQLite это разрешает:

Так читается заметно приятнее, чем повторять SUM(amount) внутри HAVING.

WHERE и HAVING: используем вместе

WHERE и HAVING — это не «или одно, или другое». WHERE отсеивает строки до группировки, а HAVING отбирает уже сами группы, которые попадут в результат. На практике в реальных запросах почти всегда работают оба сразу.

Читаем сверху вниз в порядке выполнения:

  1. WHERE status = 'paid' — сразу выкидываем возвраты.
  2. GROUP BY customer — то, что осталось, раскладываем по клиентам.
  3. SUM(amount) считается внутри каждой группы.
  4. HAVING SUM(amount) > 75 — оставляем только те группы, что прошли порог.

В итоге остаются Борис (80 + 20 = 100) и Клео (200). У Ады единственный оплаченный заказ был на 50 — до порога не дотягивает.

Несколько условий и группировка по нескольким столбцам

В HAVING работают те же логические операторы, что и в WHEREAND, OR, NOT. А ещё можно группировать сразу по нескольким столбцам, получая подгруппы:

Каждая пара (region, quarter) — это отдельная группа. В HAVING мы требуем сразу два условия: сумма больше 100 и как минимум две сделки. Под оба условия подходят только ('North', 'Q1') и ('South', 'Q2').

Практический приём: поиск дубликатов

Связка GROUP BY ... HAVING COUNT(*) > 1 — это классический способ найти повторяющиеся значения в столбце:

Всплывают два дубликата. Дальше обычно решают, что с этим делать: объединить аккаунты, навесить ограничение UNIQUE или просто почистить данные — но сам запрос для поиска всегда одной и той же формы.

HAVING без GROUP BY

Случай редкий, но вполне рабочий. Если GROUP BY нет, вся выборка считается одной большой группой, и HAVING фильтрует её целиком — либо получаете агрегаты, либо пустой результат:

Единственная строка в результате появилась потому, что сумма равна 160. Поменяйте порог на > 200 — и запрос не вернёт ни одной строки. На практике HAVING почти всегда идёт в паре с GROUP BY, но полезно помнить, что сам язык этого не требует.

Коротко о главном

  • GROUP BY схлопывает строки в группы по ключу; агрегатные функции считаются внутри каждой группы.
  • Каждая неагрегированная колонка из SELECT должна присутствовать и в GROUP BY.
  • WHERE отсеивает строки до группировки, а HAVING — уже готовые группы после неё.
  • Агрегатам вроде COUNT(*) и SUM(...) место в HAVING, но никак не в WHERE.
  • HAVING спокойно принимает составные условия и умеет ссылаться на алиасы из SELECT.

Дальше: внешние ключи

Агрегировать одну таблицу — это здорово, но в реальных схемах данные обычно размазаны по нескольким таблицам: заказы здесь, клиенты там, товары где-то ещё. Внешние ключи как раз и нужны, чтобы связать эти таблицы между собой и удержать целостность отношений. Об этом — в следующей главе.

Часто задаваемые вопросы

В чём разница между WHERE и HAVING в SQLite?

WHERE отсеивает отдельные строки до группировки, а HAVING работает уже после агрегации и фильтрует целые группы. То есть WHERE amount > 100 оставит только строки со значением больше 100, а HAVING SUM(amount) > 100 — только те группы, у которых сумма получилась больше 100. Агрегатные функции вроде COUNT или SUM в WHERE использовать нельзя — для этого как раз и придуман HAVING.

Можно ли использовать HAVING без GROUP BY в SQLite?

Можно. Без GROUP BY SQLite считает весь результат одной большой группой, и HAVING фильтрует её целиком. На выходе вы получите либо одну строку, либо ни одной. На практике такой приём встречается редко: обычно если есть HAVING, то рядом стоит и GROUP BY.

Как отфильтровать группы по COUNT в SQLite?

Агрегат пишем в HAVING, а не в WHERE. Например, запрос SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id HAVING COUNT(*) > 1 вернёт клиентов, у которых больше одного заказа. В SQLite внутри HAVING можно сослаться и на алиас столбца из списка SELECT — это удобно.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ