Все материалы
Продуктовая аналитикагайдстарт

SQL с нуля: как работают SELECT, FROM и WHERE

Разбираем первый SQL-запрос на понятном примере: как выбрать колонки, указать таблицу, отфильтровать строки и не перепутать результат с данными продукта.

КейсПрактика13 июля 2026 г.обновлено 2 октября 2026 г.21 мин
Содержание статьи

Первый SQL-запрос лучше учить не на абстрактных цифрах, а на вопросе из реальной работы: «Покажите пользователей, которые пришли из organic в июне». В нём уже есть всё необходимое — какие поля нужны, из какой таблицы их взять и какие строки оставить. Разберём эту логику на простом примере и закончим запросом, который можно выполнить в бесплатной песочнице симулятора SQL-аналитика.

Коротко

У базового запроса есть три главные детали: SELECT выбирает столбцы в результате, FROM указывает источник данных, а WHERE отбрасывает строки, которые не подходят под условие. Порядок написания всегда такой: сначала что показать, затем откуда взять, затем что оставить.

  • SELECT — какие поля или расчёты вернуть.
  • FROM — из какой таблицы или представления читать данные.
  • WHERE — какие строки подходят под вопрос.
  • Перед запросом нужно понять, что означает одна строка в таблице.
  • Для предсказуемой выборки добавляйте ORDER BY, а для короткого примера — LIMIT.

Порядок выполнения SQL-запроса

Запрос пишут с SELECT, а выполняется он с FROM. Логический порядок выполнения SQL-запроса — семь шагов: FROM и JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. На пятом шаге вместе с выражениями SELECT считаются оконные функции, после них DISTINCT убирает повторы. Первые запросы обходятся тремя из этих шагов, но список стоит увидеть целиком: каждый следующий оператор встанет в нём на своё место.

Отсюда же ответ на частый вопрос, почему алиас (псевдоним) из SELECT нельзя использовать в WHERE: фильтр выполняется раньше, чем SELECT даёт колонке новое имя. Запрос SELECT signup_date AS registered FROM users WHERE registered >= DATE '2026-08-29' PostgreSQL отклоняет с ошибкой column "registered" does not exist; с исходным именем signup_date он возвращает 38 строк. DuckDB, на котором работает песочница, такой алиас допускает, поэтому запрос, проверенный там, может упасть после переноса в PostgreSQL.

Порядок выполнения SQL-запроса: от FROM до LIMIT
ШагЧто делаетЧто из этого следует
1. FROM и JOINсобирает строки из таблицостальные шаги работают с этим набором
2. WHEREоставляет строки, подходящие под условиеалиасов из SELECT и агрегатов здесь ещё нет
3. GROUP BYсобирает строки в группыдальше доступны поля группировки и агрегаты
4. HAVINGоставляет группы, подходящие под условиеусловие на count или sum пишут здесь
5. SELECTсчитает оконные функции и выражения, даёт колонкам алиасы, затем DISTINCT убирает повторыокна видят строки, прошедшие шаги 1–4; DISTINCT сравнивает строки результата целиком
6. ORDER BYсортирует результаталиасы из SELECT уже видны
7. LIMITобрезает выдачубез ORDER BY неизвестно, какие строки останутся

Сначала разберитесь со структурой данных

Представим учебную таблицу users. В ней одна строка — один пользователь. Это важно зафиксировать до запроса: если одна строка уже описывает пользователя, можно читать список пользователей напрямую. В таблице events одна строка, наоборот, обычно означает одно событие, поэтому подсчёт строк там не равен подсчёту людей.

В песочнице симулятора SQL-аналитика схема открыта рядом с редактором: можно посмотреть типы колонок и сразу проверить запросы на полной учебной базе. Такой цикл полезнее, чем запоминать синтаксис отдельно от смысла таблиц.

Учебная таблица users
КолонкаТипЧто означает
user_idintegerуникальный идентификатор пользователя
signup_datedateдата регистрации
channeltextканал привлечения: organic, paid_search, partner или referral

SELECT: что показать в результате

SELECT задаёт форму результата. Если нужно проверить пользователей и их каналы, перечислите только эти поля. Явный список колонок помогает читать запрос и не тащит в ответ случайные технические поля.

В таблице 4 613 строк, поэтому запрос заканчивается двумя строками для просмотра: order by user_id сортирует пользователей по номеру, limit 10 показывает первые десять.

Первые строки таблицы пользователей
select
  user_id,
  signup_date,
  channel
from users
order by user_id
limit 10;
  • Не используйте select * в итоговом отчёте: результат становится шире, а смысл — менее очевидным.
  • Порядок колонок в SELECT — это порядок колонок в ответе.
  • Вместо поля можно вернуть выражение или вычисляемое значение, но сначала закрепите базовую выборку.

FROM: откуда читать строки

FROM users говорит базе, что источником результата является таблица users. Если у таблицы есть короткий псевдоним, запрос становится удобнее расширять: from users as u, а колонку можно написать как u.user_id.

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

Проверка зерна

Спросите себя: если я возьму одну строку из users, она описывает пользователя, регистрацию, сессию или событие? Ответ определит, можно ли считать строки напрямую или понадобится distinct и агрегация.

WHERE: как оставить нужные строки

WHERE превращает общий список в ответ на конкретный вопрос. Например, так можно оставить только пользователей из organic, зарегистрированных в июне. Строковые значения пишутся в одинарных кавычках, а дату лучше сравнивать с датой, а не с произвольным текстом.

Пользователи из organic, зарегистрированные в июне
select
  user_id,
  signup_date,
  channel
from users
where channel = 'organic'
  and signup_date >= date '2026-06-01'
  and signup_date < date '2026-07-01'
order by signup_date, user_id
limit 10;   -- под условие подходят 478 строк, показаны первые десять
  • = проверяет точное совпадение, например channel = 'organic'.
  • and требует, чтобы совпали оба условия.
  • or оставляет строку, если подходит хотя бы одно условие; группируйте такие условия скобками.
  • Для диапазона дат обычно удобно использовать левую включённую и правую исключённую границу.

Соберите запрос по шагам

Теперь объединим базовые части. Задача: посмотреть пять первых organic-пользователей, зарегистрированных в июне, в порядке даты регистрации. ORDER BY делает порядок явным, а LIMIT сокращает результат для быстрой проверки.

Читать его удобно сверху вниз как последовательность решений: какие поля вернуть → из какой таблицы → какие строки оставить → в каком порядке показать → сколько строк вывести. Логика выполнения внутри базы сложнее, но для аналитика такой порядок чтения хорошо сохраняет смысл запроса.

Полный запрос для первой проверки
select
  user_id,
  signup_date,
  channel
from users
where channel = 'organic'
  and signup_date >= date '2026-06-01'
  and signup_date < date '2026-07-01'
order by signup_date, user_id
limit 5;

Частые ошибки в первых запросах

На старте проблема обычно не в том, что запрос не запускается. Гораздо опаснее запрос, который запускается и даёт ответ не на тот вопрос.

Мини-проверка перед отправкой результата

Назовите единицу строки, период, фильтры и ожидаемый размер ответа. Если не можете объяснить эти четыре пункта словами, SQL ещё рано отправлять в дашборд.

  • Считать строки events как пользователей: одно и то же действие может повторяться много раз.
  • Писать select * и не замечать, что в отчёт попали технические поля или персональные данные.
  • Забывать кавычки вокруг текста: organic — значение, а не название колонки.
  • Использовать limit без order by, а потом считать полученную пятёрку «первыми» пользователями.
  • Сравнивать дату регистрации со строкой в непонятном формате и надеяться на неявное приведение типов.
  • Не проверять null: условие channel = 'organic' не включает пользователей с пустым каналом.

Что проверить перед первым запуском

Проверяйте базовый запрос как маленькое исследование. Сначала выполните select count(*) from users, затем посмотрите диапазон signup_date и распределение channel. Это поможет понять, что фильтр не возвращает пустой результат из-за опечатки, другой кодировки или периода вне учебного набора.

Для выборки добавьте order by, иначе база не обязана возвращать строки в одинаковом порядке. limit полезен для осмотра, но не должен использоваться как часть метрики. Если нужно узнать число пользователей, уберите LIMIT и добавьте явную агрегацию.

Сравните несколько строк вручную с условием. Найдите одного organic-пользователя, зарегистрированного в июне, и одного, который должен быть исключён. Такой контроль связывает SQL с данными, а не только с зелёным статусом выполнения.

Минимальный smoke-test для SELECT
ПроверкаЧто ловит
count(*)пустой или неожиданно большой результат
min/max dateне тот период или формат даты
distinct channelопечатку в значении фильтра
order by + limitстабильный просмотр примера

SELECT отвечает на вопрос, но не объясняет причину

Первая выборка — это наблюдение, а не вывод. Если вы нашли 478 organic-пользователей за июнь, следующий вопрос может быть про их активность, retention или конверсию в оплату. Не добавляйте JOIN и расчёты сразу: сначала убедитесь, что базовая аудитория определена правильно.

Сохраняйте запросы как маленькие шаги. Рабочий путь обычно выглядит так: посмотреть строки, проверить фильтр, посчитать группу, присоединить факт и сравнить сегменты. Такой порядок легче отлаживать и проще объяснить человеку, который только начинает SQL.

Когда результат станет частью отчёта, добавьте период, размер базы и ссылку на определение события. Тогда даже простой SELECT будет полезен команде, а не останется одноразовым упражнением.

Мини-практика

Попробуйте изменить последний запрос: оставьте только пользователей из referral, выведите десять строк и отсортируйте их по user_id. Затем замените диапазон дат на первые три дня июня. После этого можно переходить к агрегатам и считать, сколько пользователей пришло из каждого канала.

NULL: отсутствие значения — это не пустая строка

NULL означает, что значения нет или оно неизвестно. Это не то же самое, что пустая строка '', ноль или слово unknown. Сравнение channel = NULL не вернёт пользователей: для проверки отсутствия значения нужен IS NULL, а для наличия — IS NOT NULL.

В аналитике NULL часто имеет смысл. У пользователя ещё не определён канал, событие пришло без user_id, платёж не имеет даты возврата. Не удаляйте такие строки автоматически. Сначала решите, должны ли они входить в метрику, а затем явно назовите правило в описании расчёта.

Как читать разные виды пустых значений
ЗначениеЧто означаетПроверка
NULLзначение отсутствует или неизвестноis null / is not null
''строка есть, но она пустаяchannel = ''
0числовое значение равно нулюamount = 0
unknownявно записанная категорияchannel = 'unknown'

Даты: границы периода должны быть явными

Для месяца безопаснее использовать включённую нижнюю границу и исключённую верхнюю: signup_date >= date '2026-06-01' and signup_date < date '2026-07-01'. Так в запрос не нужно подставлять последний день месяца, а время 2026-06-30 23:59:59 не потеряется из-за округления.

Если поле хранит timestamp, уточните таймзону. «День» в UTC и день в часовом поясе продукта могут начинаться в разные моменты. Для DAU это способно менять число активных пользователей между датами. Правило преобразования должно быть одинаковым в отчёте, SQL и визуализации.

Полный июнь с временной границей
select
  user_id,
  signup_date
from users
where signup_date >= timestamp '2026-06-01 00:00:00'
  and signup_date < timestamp '2026-07-01 00:00:00'
order by signup_date, user_id
limit 10;   -- в июне 1042 регистрации, показаны первые десять
Период нужно назвать словами

Перед публикацией напишите: «полные календарные дни в таймзоне продукта» или другое точное правило. Одного названия поля недостаточно.

AND, OR и скобки меняют аудиторию

Условие с AND оставляет строки, которые подходят под оба правила. OR оставляет строку, если совпало хотя бы одно. В выражении channel = 'organic' or channel = 'referral' and signup_date >= ... база сначала применит приоритет операторов, и результат может отличаться от того, что имел в виду аналитик.

Группируйте альтернативы скобками: (channel = 'organic' or channel = 'referral') and signup_date >= .... Скобки полезны не только для базы, но и для человека, который будет поддерживать запрос через месяц.

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

Как отлаживать запрос, который возвращает не то

Не переписывайте весь SQL, если результат странный. Уберите фильтры и проверьте базовый SELECT. Затем добавляйте условия по одному, каждый раз фиксируя размер выборки. Если строки исчезли после конкретного условия, проблема находится в значении, диапазоне, NULL или типе данных.

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

Пошаговая отладка SELECT
ШагКонтрольный вопрос
1. FROMтаблица существует и содержит ожидаемый период?
2. SELECTколонки имеют нужный тип и смысл?
3. первый WHEREфильтр возвращает хотя бы известный пример?
4. следующий WHEREсколько строк исчезло и почему?
5. ORDER BY/LIMITпросмотр стабилен и не влияет на метрику?

База читает запрос не в том порядке, в каком вы его пишете

Запрос пишется как SELECT ... FROM ... WHERE ..., но логически всё происходит иначе: сначала база берёт строки из источника, затем отбрасывает не подходящие под условие, и только потом формирует колонки результата. Сортировка и ограничение выдачи применяются в самом конце, к уже готовому набору.

Из этого следует практическое правило, о которое спотыкаются почти все в первую неделю: псевдоним, придуманный в SELECT, обычно нельзя использовать в WHERE того же запроса. На момент фильтрации его ещё не существует. Если выражение нужно и для отбора, и для вывода, его либо повторяют, либо выносят на отдельный слой запроса.

Ещё одно следствие: LIMIT не делает запрос дешёвым. Он ограничивает то, что вы увидите, а не то, что база успела прочитать. На учебной базе это незаметно, на рабочей таблице событий — вполне ощутимо.

Из того же порядка растёт следующая тема курса. Когда появится группировка, окажется, что WHERE фильтрует отдельные строки до сборки групп, а условие по уже посчитанной сумме или количеству живёт в HAVING. Пока запрос не агрегирует, это различие не мешает — но помнить о нём стоит с первого дня.

ORDER BY видит псевдоним из SELECT, а WHERE — нет: это один список шагов

Посмотрите на два почти одинаковых запроса к events. Первый считает действия по дням: select date(event_time) as day, count(*) from events group by day order by day — выполняется. Во втором добавлено where day >= date '2026-06-01', и PostgreSQL отвечает column "day" does not exist. Имя одно и то же, запрос один и тот же, а group by и order by его находят, where и having — нет.

Объясняется это не списком исключений, а одним вопросом к каждому шагу: какие имена к этому моменту уже существуют. Выражения из select вычисляются на пятом шаге из семи, поэтому where и having про псевдоним ещё не знают, а сортировка на шестом шаге работает с готовым результатом и видит его спокойно. Отступление от порядка одно: group by выполняется раньше, но PostgreSQL и DuckDB разрешают писать в нём имя колонки результата.

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

Что доступно по имени на каждом шаге
ШагЧто делаетКакие имена уже существуют
1. FROM / JOINберёт строки источниковколонки таблиц и их псевдонимы
2. WHEREвыбрасывает строкитолько колонки таблиц
3. GROUP BYсобирает группыколонки таблиц; PostgreSQL и DuckDB принимают и псевдонимы SELECT
4. HAVINGвыбрасывает группыагрегаты и колонки группировки, но не псевдонимы
5. SELECTсчитает оконные функции поверх групп, вычисляет выражения, затем DISTINCT убирает повторыздесь псевдонимы и появляются; окна видят результат шагов 1–4
6. ORDER BYсортирует результатколонки результата, псевдонимы, номера позиций
7. LIMITобрезает выдачуимена не нужны: только число строк
Способ 1: повторить выражение там, где оно нужно
-- в WHERE стоит колонка таблицы: с псевдонимом (where day >= ...) PostgreSQL
-- не запустится, а DuckDB в песочнице такой псевдоним допускает
select date(event_time) as day, count(*) as actions
from events
where event_time >= date '2026-06-01'
group by day
order by day;
sqlСпособ 2: дать выражению имя слоем раньше
with daily as (
  select user_id, date(event_time) as day
  from events
)
select day, count(distinct user_id) as active_users
from daily
where day >= date '2026-06-01'
group by day
order by day;
  • Оконные функции считаются после группировки, поэтому where row_number() over (...) = 1 не запустится, а перенос условия в having не спасёт: там живут агрегаты групп, а не номера строк. Нужен отдельный слой.
  • select distinct date(event_time) as day from events order by event_time в PostgreSQL даёт ошибку: после удаления повторов строки с разным временем слиплись в одну, и сортировать стало не по чему.
  • having без group by разрешён и считает всю выборку одной группой, так что having count(*) > 100 вернёт либо весь результат, либо пустоту.

Одинаковые даты делают выдачу нестабильной, а OFFSET дорожает с каждой страницей

В собранном выше запросе сортировка записана как order by signup_date, user_id, и вторая колонка там не для симметрии. За 1–3 июня в базе 98 регистраций на три дня: 32, 33 и 33. Значит у десятков строк signup_date совпадает, и order by signup_date задаёт не порядок строк, а три группы; внутри группы база расставит строки как ей удобнее и на следующем запуске может расставить иначе.

Для просмотра это неважно, для выдачи по частям — ломает результат. limit 4 возьмёт четыре строки из 98, limit 4 offset 4 — следующие четыре. Если внутри дня порядок изменился, пользователь, стоявший четвёртым, уезжает на вторую страницу и появляется там второй раз, а кто-то другой не попадает ни в одну из выдач. Обе страницы выглядят нормально: ошибку видно только при сверке с общим числом строк. Лечится это тай-брейкером — колонкой, которая не повторяется: user_id подходит, channel или country нет, они тоже дают ничьи.

Вторая часть той же темы — цена листания. offset не пролистывает строки, а выбрасывает уже прочитанные: offset 100000 limit 20 обходится базе как выборка 100 020 строк, из которых сто тысяч отправляются в мусор. Устойчивая альтернатива — keyset: следующая страница просит не «пропусти сорок», а «дай то, что идёт после этой пары значений». Кортежное сравнение делает это одним условием и заодно защищает от сдвига выдачи, когда между запросами пришли новые регистрации; для сортировки по убыванию знак меняется на <, а набор колонок в условии должен совпадать с набором в order by.

Ещё две детали. В PostgreSQL order by signup_date desc ставит NULL первыми — пустое значение считается «большим», — поэтому для незаполненных дат пишут явное nulls last. А если задача звучит как «первая десятка», а на десятом месте ничья, limit 10 обрежет список произвольно: fetch first 10 rows with ties вернёт всех, кто делит последнее место, и строк станет больше десяти, зато результат перестанет зависеть от того, какую из равных строк база увидела раньше.

Что выбрать под задачу выдачи
ЗадачаПриёмНа что смотреть
посмотреть пример строкorder by + limitпорядок задан явно, число не уходит в отчёт
первые страницы спискаlimit + offsetстоимость растёт с номером страницы
глубокое листаниеkeyset по кортежуколонки условия совпадают с order by
топ-N с честными ничьимиfetch first N rows with tiesстрок может быть больше N
топ-N внутри каждой группыROW_NUMBER или RANKранжируется агрегат, а не сырая строка
Страница 1: пользователи 1–4, последняя пара — (2026-06-01, 4)
select user_id, signup_date, channel
from users
where signup_date >= date '2026-06-01'
  and signup_date <  date '2026-06-04'
order by signup_date, user_id
limit 4;
sqlСтраница 2 без OFFSET: по последней паре предыдущей, пользователи 5–8
-- продолжаем с последней пары, а не пропускаем четыре строки
select user_id, signup_date, channel
from users
where signup_date >= date '2026-06-01'
  and signup_date <  date '2026-06-04'
  and (signup_date, user_id) > (date '2026-06-01', 4)
order by signup_date, user_id
limit 4;

Сквозной пример: три таблицы, на которых можно всё повторить

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

Первое, что стоит сделать с незнакомой базой, — назвать зерно каждой таблицы одной фразой. Это занимает минуту и снимает половину будущих вопросов: становится видно, где строка означает человека, где действие, а где деньги.

Учебная база «Маяк»
ТаблицаОдна строка — этоКлючевые колонки
usersодин зарегистрированный пользовательuser_id, signup_date, channel, country
eventsодно действие пользователя в продуктеevent_id, user_id, event_time, event_name
paymentsодин платёжpayment_id, user_id, paid_at, amount, plan

Шаг 1. Уточнить вопрос раньше, чем открыть редактор

Менеджер продукта пишет в чат: «Пришли список пользователей из organic за начало июня». Звучит однозначно, но в этой фразе спрятаны как минимум три решения, и каждое меняет результат.

Что такое «начало июня» — первые три дня, первая неделя или весь месяц? Что такое «пользователи из organic» — те, у кого канал записан как organic в момент регистрации, или те, кто пришёл без рекламной метки вообще? И что именно нужно в ответе: список людей для выгрузки или их количество для слайда? От последнего зависит, будет ли в запросе SELECT полей или COUNT.

Уточнить это дешевле до запроса. Ответ «47 пользователей» и ответ «90 пользователей» одинаково верны для разных прочтений одной просьбы, но обсуждать вы их будете как ошибку.

Одна просьба — три разных запроса
ПрочтениеУсловие периодаСколько строк вернёт
первые три дня июняsignup_date >= '2026-06-01' AND signup_date < '2026-06-04'47 пользователей
первая неделя июняsignup_date >= '2026-06-01' AND signup_date < '2026-06-08'90 пользователей
весь июньsignup_date >= '2026-06-01' AND signup_date < '2026-07-01'478 пользователей

Шаг 2. Собрать запрос и посмотреть на размер выборки

Договорились: нужен список organic-пользователей за первые три дня июня, с датой регистрации и страной. Запрос собирается ровно из тех частей, которые разобраны выше, и читается как формулировка задачи.

Обратите внимание на две детали. Верхняя граница периода записана как «меньше 4 июня», а не «меньше или равно 3 июня» — так запрос не сломается, если однажды в колонке окажется время, а не только дата. И сортировка задана явно: без ORDER BY база не обязана возвращать строки в одном порядке, а список, который меняется между запусками, невозможно обсуждать.

Список organic-пользователей за первые три дня июня
SELECT
  user_id,
  signup_date,
  country
FROM users
WHERE channel = 'organic'
  AND signup_date >= DATE '2026-06-01'
  AND signup_date <  DATE '2026-06-04'
ORDER BY signup_date, user_id;

Шаг 3. Сверить результат руками

Запрос вернул 47 строк. На учебной базе это можно проверить пересчётом по дням: из organic 1 июня зарегистрировались 15 человек, 2 и 3 июня — по 16, значит в трёхдневное окно попадают ровно 47. Совпало.

Такая сверка кажется избыточной ровно до первого случая, когда она не сходится. На реальных данных пересчитать руками нельзя, но приём остаётся: посчитайте ожидаемый порядок величины заранее и сравните. «Ожидаю примерно сотню, получил четыре» — это сигнал искать опечатку в значении канала или неверную границу периода, а не радоваться быстрому ответу.

Полезно так же проверить дополнение выборки: сколько пользователей за тот же период пришло не из organic. Если сумма двух чисел не равна общему количеству регистраций за период, где-то потерялись строки — обычно из-за NULL в колонке канала. Запрос ниже считает все четыре числа сразу, и его удобно держать под рукой как шаблон проверки для любого фильтра по категории.

Первые три строки результата из 47
user_idsignup_datecountry
42026-06-01RU
52026-06-01AM
62026-06-01RU
Контрольный запрос: сходится ли выборка с общим числом регистраций
SELECT
  count(*)                                            AS all_signups,
  count(*) FILTER (WHERE channel = 'organic')         AS organic_signups,
  count(*) FILTER (WHERE channel <> 'organic')        AS other_signups,
  count(*) FILTER (WHERE channel IS NULL)             AS unknown_channel
FROM users
WHERE signup_date >= DATE '2026-06-01'
  AND signup_date <  DATE '2026-06-04';

-- 98 | 47 | 51 | 0: 47 + 51 = 98, строк без канала нет

Шаг 4. Отдать ответ так, чтобы его можно было перепроверить

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

Формулировка может выглядеть так: «За 1–3 июня из organic зарегистрировались 47 пользователей из 98 регистраций за этот период. Канал берётся из users.channel на момент регистрации; период — по дате регистрации, верхняя граница не включена». Теперь любой коллега видит, какое именно решение вы приняли за него, и может возразить по существу.

Тот же принцип работает и дальше по курсу: чем сложнее запрос, тем важнее рядом с числом называть зерно, период и правило отбора. Метрику без этих трёх вещей нельзя ни проверить, ни повторить.

Минимальный чеклист перед первым SQL-ответом

Перед отправкой результата проверьте не только зелёный статус выполнения. Назовите зерно строки, период и таймзону, условия фильтра, отношение NULL и ожидаемый размер базы. Найдите одну строку, которая должна попасть в ответ, и одну, которая должна быть исключена.

Если запрос отвечает на вопрос про пользователей, не называйте его метрикой, пока не проверили уникальность user_id. Следующий шаг — агрегирование через GROUP BY и COUNT(DISTINCT), но базовая аудитория должна быть понятна заранее.

Что изучать дальше

После SELECT, FROM и WHERE логично перейти к GROUP BY, COUNT и COUNT(DISTINCT). Именно там список строк превращается в метрику: DAU по дням, пользователи по каналам или конверсия между шагами воронки. Но сначала закрепите базовую механику на нескольких собственных вопросах.

Продолжить чтение
Вся библиотека