Девять из десяти запросов, которые вы напишете в жизни, — это чтение данных: найти заказ, посмотреть статус, проверить, что запись появилась. Всё чтение делается одной командой — SELECT. Она безопасна: сколько ни выполняй, данные не изменятся. Продолжаем на базе магазина с таблицами customer и orders.
WHERE решает, какие строки останутся, SELECT — какие колонки показать. Это два разных сита.
SELECT и FROM: что и откуда
Минимальный запрос — «покажи всё из таблицы»:
живой пример
SELECT * FROM orders;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
FROM называет таблицу, SELECT — какие столбцы показать; звёздочка значит «все». На практике чаще перечисляют нужные столбцы явно:
живой пример
SELECT id, status, total_amount FROM orders;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Так результат читается легче, а на широких таблицах (по 40 столбцов) это единственный способ не утонуть. Точка с запятой завершает запрос; регистр не важен (select = SELECT), но команды принято писать заглавными — глазу проще отделять их от имён.
Звёздочка удобна, когда смотрят глазами, и вредна в коде приложения. Колонки в таблице меняются: добавили internal_comment — и он поехал в ответ ручки вместе с остальным; добавили длинное description — и запрос стал тащить мегабайты, которых никто не просил; переставили колонки местами — и код, читающий их по номеру, стал читать не то. Плюс по SELECT * не видно, что коду действительно нужно, а значит, нельзя понять, можно ли удалить колонку. Правило простое: звёздочка в ручном запросе, явный список — везде, где запрос сохраняют.
А чтобы узнать, из чего вообще выбирать, спрашивают базу. В psql это \d orders — колонки, типы и обязательность одним экраном. Из любого клиента то же показывает запрос к служебному каталогу:
живой пример
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'orders'
ORDER BY ordinal_position;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
is_nullable = NO означает «поле всегда заполнено», YES — «бывает пустым», и это первое, что стоит знать про незнакомую таблицу: пустые значения ведут себя иначе, чем все ожидают.
WHERE: оставить только нужное
Вся сила запросов — в фильтре WHERE: он оставляет строки, для которых условие истинно.
живой пример
SELECT id, total_amount FROM orders WHERE status = 'PAID';
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Сравнения: =, <> (не равно), >, <, >=, <=. Текстовые значения — в одинарных кавычках: 'PAID'. Числа — без кавычек: total_amount > 1000.
Условия соединяются через AND и OR:
живой пример
SELECT id FROM orders
WHERE status = 'PAID' AND total_amount > 1000;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
С OR внимательнее: AND склеивает сильнее, чем OR, и без скобок смешанное условие читается не так, как написано:
-- ожидают оплаты ИЛИ (оплачены И дороже 500): дешёвые неоплаченные тоже попадут
WHERE status = 'PENDING_PAYMENT' OR status = 'PAID' AND total_amount > 500
-- то, что обычно имели в виду: любой из двух статусов, и все дороже 500
WHERE (status = 'PENDING_PAYMENT' OR status = 'PAID') AND total_amount > 500
Смешали AND и OR — ставьте скобки, даже если знаете приоритет: их поставят и за читателя.
Три полезных фильтра сверх сравнений:
IN— значение из списка:WHERE status IN ('PENDING_PAYMENT', 'PAID')— короче, чем цепочка OR.BETWEEN— диапазон включительно:WHERE total_amount BETWEEN 500 AND 1500. С колонками времени так писать нельзя:created_at BETWEEN '2026-08-01' AND '2026-08-31'обрежет весь последний день, кроме его полуночи, потому что верхняя граница читается как2026-08-31 00:00:00. Для времени берут пару сравнений:created_at >= '2026-08-01' AND created_at < '2026-09-01'.IS NULL— пустое значение:WHERE paid_at IS NULLнаходит неоплаченные заказы. Через= NULLпустоту не ищут: такой запрос всегда возвращает ноль строк и молчит об этом; почему — в статье NULL и типы данных.LIKE— поиск по шаблону в строках:WHERE email LIKE '%@gmail.com'. Процент означает «любые символы»:'Anna%'— начинается с «Anna»,'%ova%'— содержит «ova» (имена в учебной базе записаны латиницей, поэтому и шаблоны такие). В PostgreSQL есть регистронезависимый вариантILIKE— для поиска по именам и почтам обычно нужен именно он. Оговорка про нашу песочницу: почта там объявлена особым типомCITEXT, который сам не различает регистр, иLIKE '%@EXAMPLE.COM'находит те же девять строк, что и запись строчными. С обычным текстовым столбцом так не будет — вот тамILIKEи понадобится.
Отработать здесь: Неоплаченные заказы · Заказы Анны в двух статусах · Товары в диапазоне цен · Покупатели с фамилией на «-ova»
ORDER BY и LIMIT: порядок и порция
Без явной сортировки база возвращает строки в непредсказуемом порядке — сегодня так, завтра иначе. Нужен порядок — скажите об этом:
живой пример
SELECT id, total_amount FROM orders
ORDER BY total_amount DESC
LIMIT 10;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
ORDER BY total_amount DESC — по убыванию суммы (ASC — по возрастанию, он по умолчанию). LIMIT 10 — только первые десять строк. Вместе это «топ-10 самых дорогих заказов» — и одновременно защита: если в таблице миллионы строк, запрос без LIMIT будет тащить их все.
Сортировать можно по нескольким столбцам: ORDER BY status, total_amount DESC — сначала по статусу, а внутри каждого статуса — по убыванию суммы. Слово «группировка» здесь не к месту: строки только раскладываются по порядку, ни одна из них никуда не схлопывается.
Рядом с LIMIT живёт OFFSET — «пропусти первые N строк». Так листают страницы:
живой пример
SELECT id, total_amount FROM orders
ORDER BY total_amount DESC, id
LIMIT 10 OFFSET 20;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Это третья страница по десять строк. Две оговорки, из-за которых постраничный вывод ломается чаще всего. Первая: без ORDER BY страницы бессмысленны, а при сортировке по неуникальной колонке — почти бессмысленны. Если у двух заказов одинаковая сумма, их взаимный порядок база не обещает, и одна и та же строка может приехать и на второй странице, и на третьей, а другая не приехать вовсе. Поэтому вторым ключом сортировки ставят что-то уникальное, как id в примере. Вторая: OFFSET не бесплатен. Чтобы пропустить двадцать тысяч строк, база их всё равно прочитает и отсортирует, поэтому глубокие страницы тормозят тем сильнее, чем дальше листаешь. В приложениях вместо номера страницы запоминают последнюю показанную строку и просят «следующие десять после неё» — условие вида WHERE (total_amount, id) < (последняя_сумма, последний_id) читает ровно нужное.
Отработать здесь: Витрина от дешёвого к дорогому · Лента заказов покупателя: по статусу, свежие сверху
DISTINCT: без повторов
Вопрос «какие вообще бывают статусы заказов» кажется вопросом про справочник, но справочника может не быть, а статусы лежат прямо в колонке orders.status, по одному на строку. SELECT status FROM orders вернёт столько строк, сколько заказов, и большинство повторяются. DISTINCT оставляет каждую комбинацию значений один раз:
живой пример
SELECT DISTINCT status
FROM orders
ORDER BY status;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
DISTINCT относится ко всем колонкам в SELECT сразу: SELECT DISTINCT customer_id, status уберёт повторы пар, а не отдельных значений. Выполняется он после WHERE и до ORDER BY, поэтому сортировать можно только по тому, что осталось в выдаче. Считать уникальные, а не показывать, помогает его пара из следующей статьи: COUNT(DISTINCT customer_id). Одно предупреждение: если DISTINCT понадобился, чтобы убрать задвоение после JOIN, это лечит симптом, а причина в соединении, о ней в статье про JOIN.
В каком порядке на самом деле выполняется запросспросят на собеседовании
Псевдоним из SELECT в WHERE не работает, а в ORDER BY работает; COUNT в WHERE написать нельзя, а в HAVING можно. Это не капризы базы: запрос пишется в порядке SELECT … FROM … WHERE … ORDER BY, а выполняется в другом. Сначала FROM собирает строки, потом WHERE их фильтрует, и только потом SELECT вычисляет колонки и псевдонимы: на момент фильтрации псевдонима ещё нет, а ORDER BY идёт после SELECT, и там он уже есть. Агрегаты считаются по группам, а группы появляются после WHERE, поэтому фильтр по COUNT живёт в HAVING, который стоит после группировки.
Полный порядок такой: FROM/JOIN → WHERE → GROUP BY → HAVING → оконные функции → SELECT → DISTINCT → ORDER BY → LIMIT. Группы, окна и вынесенные вперёд запросы через WITH встретятся дальше в цикле, начиная со статьи про подзапросы. И LIMIT не спасает от тяжёлой сортировки: чтобы отдать первые 20 строк по ORDER BY, база обязана сначала упорядочить всё, если нет подходящего индекса.
Смотрите, на каком месте стоит SELECT: он считает псевдонимы уже после WHERE, поэтому в фильтре псевдонима ещё нет, а в ORDER BY он есть.
Где это применяется
SELECT … FROM … WHERE … — рабочая лошадка на каждый день: проверить, что заказ из интерфейса действительно создался; убедиться, что после отмены статус сменился; найти пользователя по кусочку почты через LIKE; глянуть последние записи через ORDER BY created_at DESC LIMIT 20. Как только вы перестаёте просить разработчика «посмотри в базе, а?» и смотрите сами — скорость вашей работы меняется скачком.
Где спотыкаются начинающие:
- Двойные кавычки вместо одинарных.
WHERE status = "PAID"во многих СУБД — ошибка: двойные кавычки — для имён столбцов, одинарные — для значений. - Сравнение без учёта регистра и пробелов.
'Paid','PAID'и'PAID '— три разных значения. Не находится очевидное — проверьте регистр и хвостовые пробелы. - Полагаются на «естественный» порядок строк. Без ORDER BY порядок не гарантирован — «вчера первая строка была другой» не баг базы, а отсутствие сортировки в запросе.
Коротко
SELECTназывает колонки,FROMтаблицу,WHEREоставляет строки по условию (=,LIKE,IN,BETWEEN),ORDER BYзадаёт порядок,LIMITпорцию.- База выполняет запрос не в порядке написания:
FROM→WHERE→SELECT→DISTINCT→ORDER BY→LIMIT; поэтому псевдоним изSELECTвWHEREне виден. DISTINCTубирает повторы по всем колонкам выдачи сразу; для подсчёта уникальных естьCOUNT(DISTINCT …).LIMITбезORDER BYотдаёт случайную порцию: порядок строк без сортировки база не обещает.OFFSETлистает страницы, но читает и пропущенное; сортировать надо с уникальным ключом, иначе строки перескакивают между страницами.BETWEENвключает обе границы: для колонок времени берут>=и<, иначе последний день диапазона теряется.
Что почитать дальше
- JOIN: собрать данные из нескольких таблиц — когда нужного нет в одной таблице.
- Агрегаты: COUNT, GROUP BY и HAVING — считать, а не перечислять.
- Почему запрос медленный — что база делает с
WHEREвнутри.