Команда SELECT в SQL: синтаксис, WHERE, ORDER BY, LIMIT и порядок выполнения

Команда SELECT в SQL: синтаксис, WHERE, ORDER BY, LIMIT и порядок выполнения Полезное

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

Ниже — рабочий пример, синтаксис по частям, логический порядок выполнения запроса и типовые ошибки. Все запросы проверены 24.09.2026 в PostgreSQL 16.14 и SQLite 3.51; где СУБД ведут себя по-разному, это сказано рядом с примером.

Три термина, которые помогут дальше:

  • Команда (оператор) — весь запрос SELECT ... ; целиком.
  • Предложение (clause) — его часть, начинающаяся с ключевого слова: FROM, WHERE, ORDER BY.
  • Результирующий набор — то, что вернул запрос. Это новая временная таблица, а не исходная.

Минимальный рабочий пример

Создадим таблицу пользователей. Обратите внимание на NULL: у Глеба не указан город, у Веры — email. Эти строки понадобятся, чтобы показать ловушки.

CREATE TABLE users (
    id             INTEGER PRIMARY KEY,
    name           VARCHAR(50) NOT NULL,
    city           VARCHAR(50),
    login_attempts INTEGER NOT NULL DEFAULT 0,
    email          VARCHAR(100)
);

INSERT INTO users (id, name, city, login_attempts, email) VALUES
    (1, 'Анна',  'Москва', 0, 'anna@example.com'),
    (2, 'Борис', 'Казань', 3, 'boris@example.com'),
    (3, 'Вера',  'Москва', 5, NULL),
    (4, 'Глеб',  NULL,     1, 'gleb@example.com'),
    (5, 'Дина',  'Самара', 4, 'dina@example.com'),
    (6, 'Егор',  'Казань', 2, 'egor@example.com');

Задача: найти двух пользователей с наибольшим числом неудачных входов, если попыток больше двух.

SELECT name, city, login_attempts
FROM users
WHERE login_attempts > 2
ORDER BY login_attempts DESC
LIMIT 2;
 name |  city  | login_attempts
------+--------+----------------
 Вера | Москва |              5
 Дина | Самара |              4
(2 rows)

Разбор по строкам: SELECT перечисляет нужные столбцы, FROM говорит, из какой таблицы брать строки, WHERE оставляет только строки с login_attempts > 2 (их три: Борис, Вера, Дина), ORDER BY ... DESC сортирует по убыванию, LIMIT 2 отрезает первые две.

Синтаксис SELECT

Общая форма с предложениями в том порядке, в котором их пишут:

SELECT [DISTINCT] столбец_или_выражение [AS псевдоним], ...
FROM таблица [AS псевдоним]
[WHERE условие_для_строк]
[GROUP BY столбцы]
[HAVING условие_для_групп]
[ORDER BY столбец [ASC | DESC], ...]
[LIMIT число [OFFSET сдвиг]];

Обязателен только SELECT. FROM нужен почти всегда, но в PostgreSQL, SQLite и MySQL можно вычислить выражение и без таблицы: SELECT 2 + 2;. Порядок предложений строгий: WHERE после ORDER BY — синтаксическая ошибка.

SELECT * возвращает все столбцы. Для просмотра это удобно, а в коде приложения столбцы лучше перечислять явно: запрос не потащит лишние данные, когда в таблицу добавят новое поле.

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

Пишем запрос сверху вниз, а СУБД логически обрабатывает его в другом порядке. Это упрощенная модель: оптимизатор может переставлять физические шаги (например, сначала использовать индекс), но результат обязан быть таким, как если бы порядок был этим.

Шаг Предложение Что происходит
1 FROM берется исходная таблица (или соединение таблиц)
2 WHERE отбрасываются строки, не подходящие под условие
3 GROUP BY строки собираются в группы
4 HAVING отбрасываются группы
5 SELECT вычисляются столбцы и выражения, назначаются псевдонимы
6 DISTINCT удаляются повторяющиеся строки
7 ORDER BY результат сортируется
8 LIMIT / OFFSET берется нужный кусок

Главное практическое следствие: псевдоним из SELECT еще не существует на шаге WHERE.

SELECT name, login_attempts * 10 AS score
FROM users
WHERE score > 20;

PostgreSQL отвечает ошибкой:

ERROR:  column "score" does not exist
LINE 3: WHERE score > 20;
              ^

Исправление — повторить выражение в WHERE. В ORDER BY псевдоним уже можно использовать, потому что сортировка идет после SELECT:

SELECT name, login_attempts * 10 AS score
FROM users
WHERE login_attempts * 10 > 20
ORDER BY score DESC;
 name  | score
-------+-------
 Вера  |    50
 Дина  |    40
 Борис |    30

SQLite здесь снисходителен и первый вариант выполняет без ошибки. Рассчитывать на это не стоит: запрос перестанет работать при переносе на PostgreSQL.

WHERE: условия отбора строк

В WHERE работают сравнения =, <> (или !=), >, <, >=, <=, логические AND, OR, NOT и специальные операторы.

Оператор Пример Что отбирает
IN city IN ('Москва', 'Казань') значение из списка
BETWEEN login_attempts BETWEEN 2 AND 4 диапазон, обе границы включительно (2, 3, 4)
LIKE email LIKE '_ina%' шаблон: % — любое число символов, _ — ровно один
IS NULL email IS NULL отсутствующее значение

Ловушка 1: приоритет AND и OR. AND выполняется раньше OR. Хотели «москвичи и казанцы, у которых больше двух попыток»:

SELECT name, city, login_attempts
FROM users
WHERE city = 'Москва' OR city = 'Казань' AND login_attempts > 2;

Результат — три строки, включая Анну с нулем попыток: условие прочиталось как Москва OR (Казань AND > 2). Исправление — скобки:

SELECT name, city, login_attempts
FROM users
WHERE (city = 'Москва' OR city = 'Казань') AND login_attempts > 2;
 name  |  city  | login_attempts
-------+--------+----------------
 Борис | Казань |              3
 Вера  | Москва |              5

Ловушка 2: сравнение с NULL. NULL означает «значение неизвестно», и любое сравнение с ним дает не «истину», а неизвестность. Поэтому WHERE email = NULL в PostgreSQL и SQLite возвращает 0 строк, а правильный запрос — WHERE email IS NULL (вернет Веру). Отсюда же вторая ловушка: city NOT IN ('Москва', NULL) возвращает пустой результат, хотя в таблице есть Казань и Самара. Если в списке или подзапросе для NOT IN может оказаться NULL, отфильтруйте его или используйте NOT EXISTS.

Ловушка 3: регистр в LIKE зависит от СУБД. Запрос email LIKE 'A%' с заглавной латинской A в SQLite нашел Анну, а в PostgreSQL — ни одной строки. В PostgreSQL LIKE чувствителен к регистру, для поиска без учета регистра есть ILIKE (SQLite его не знает). В SQLite LIKE по умолчанию не различает регистр только для латиницы, в MySQL поведение задает collation столбца.

ORDER BY: сортировка

По умолчанию сортировка по возрастанию (ASC), DESC — по убыванию. Можно указать несколько столбцов: второй работает, когда значения первого равны.

Без ORDER BY порядок строк не гарантирован ни одной СУБД, даже если «всегда выходит по id». И даже с ORDER BY порядок одинаковых значений не определен. Пример с группировкой: у Казани и Москвы по 5 попыток.

SELECT city, COUNT(*) AS users_cnt, SUM(login_attempts) AS attempts
FROM users
WHERE city IS NOT NULL
GROUP BY city
HAVING COUNT(*) > 1
ORDER BY attempts DESC, city;
  city  | users_cnt | attempts
--------+-----------+----------
 Казань |         2 |        5
 Москва |         2 |        5

Без второго ключа city в наших прогонах PostgreSQL выдал Казань первой, а SQLite — Москву. Оба ответа корректны, поэтому для стабильного результата нужен ключ, который разрешает ничьи.

Место NULL при сортировке тоже зависит от СУБД: при ORDER BY city PostgreSQL ставит Глеба без города последним, SQLite и MySQL — первым. В PostgreSQL и SQLite (с версии 3.30) порядок можно задать явно: ORDER BY city NULLS LAST.

Здесь же видно различие WHERE и HAVING: WHERE фильтрует строки до группировки, HAVING — готовые группы, поэтому условие на COUNT(*) пишется только в HAVING.

LIMIT и OFFSET: сколько строк вернуть

LIMIT n возвращает не больше n строк, OFFSET m пропускает первые m:

SELECT id, name FROM users ORDER BY id LIMIT 2 OFFSET 2;

Вернутся строки с id 3 и 4 (Вера и Глеб). LIMIT без ORDER BY дает случайные с точки зрения читателя строки. Синтаксис различается по СУБД:

СУБД Первые 2 строки после пропуска 2
PostgreSQL, SQLite LIMIT 2 OFFSET 2
MySQL LIMIT 2 OFFSET 2 или LIMIT 2, 2 (сначала сдвиг, потом число)
SQL Server ORDER BY id OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY; первые n строк — SELECT TOP (n)
Стандарт SQL, PostgreSQL, Oracle 12c+ OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY

Для постраничного вывода больших таблиц OFFSET со временем замедляется: СУБД все равно перебирает пропускаемые строки. На глубоких страницах обычно переходят к выборке «после последнего показанного id» (WHERE id > 40 ORDER BY id LIMIT 20).

DISTINCT: уникальные строки

DISTINCT убирает дубликаты во всей строке результата, а не в одном столбце: SELECT DISTINCT city, name вернет уникальные пары.

SELECT DISTINCT city FROM users ORDER BY city;
SELECT COUNT(city) AS with_city,
       COUNT(DISTINCT city) AS unique_cities,
       COUNT(*) AS all_rows
FROM users;

Первый запрос вернет четыре строки: три города и NULL, потому что для DISTINCT все NULL считаются одинаковыми. Второй даст 5 | 3 | 6: COUNT(столбец) пропускает NULL, COUNT(*) считает все строки.

Псевдонимы: AS для столбцов и таблиц

Псевдоним столбца меняет заголовок в результате, псевдоним таблицы сокращает запись и нужен при соединении таблиц. Имя с пробелами или кириллицей в PostgreSQL и SQLite берут в двойные кавычки (в MySQL — в обратные).

SELECT u.name AS "Имя", u.city AS "Город"
FROM users AS u
WHERE u.city = 'Самара';

Одинарные кавычки — для строковых значений ('Самара'), двойные — для имен. Перепутать их в PostgreSQL значит получить ошибку «столбец не существует». SQLite ради совместимости молча примет WHERE city = "Самара" как строку и вернет Дину, поэтому такая ошибка проявится только при переносе запроса.

Значения из формы — только параметрами

Когда значение для WHERE приходит от пользователя, его нельзя вклеивать в текст запроса. Пример на Python 3.14 с модулем sqlite3 (база с таблицей выше):

import sqlite3

conn = sqlite3.connect("t.db")
cur = conn.cursor()

user_input = "x' OR '1'='1"   # злой ввод из формы

# Плохо: значение вклеено в текст запроса
bad = f"SELECT name FROM users WHERE email = '{user_input}'"
print(len(cur.execute(bad).fetchall()))

# Хорошо: значение передано параметром
good = "SELECT name FROM users WHERE email = ?"
print(len(cur.execute(good, (user_input,)).fetchall()))
conn.close()
6
0

Первый вариант вернул все 6 пользователей: ввод превратил условие в «всегда истина» (SQL-инъекция). Второй ищет email, буквально равный строке, и не находит ничего. Параметризация защищает от этой инъекции, но не заменяет проверку входных данных и права пользователя БД: учетной записи приложения, которая только читает, достаточно права SELECT.

Если запрос не работает

Симптом Частая причина Что сделать
column "x" does not exist псевдоним из SELECT в WHERE или строка в двойных кавычках повторить выражение; строки — в одинарных кавычках
0 строк при = NULL сравнение с NULL IS NULL / IS NOT NULL
лишние строки при OR приоритет AND над OR скобки вокруг OR
порядок строк «прыгает» нет ORDER BY или ничьи добавить сортировку и второй ключ
LIKE не находит в PostgreSQL регистр ILIKE или lower(столбец) LIKE
ошибка у GROUP BY в PostgreSQL в SELECT столбец не из группы и не в агрегате (исключение — столбцы таблицы, чей первичный ключ есть в GROUP BY) добавить его в GROUP BY или обернуть в агрегат

Выводы

  • SELECT только читает данные и возвращает результирующий набор; обязательна лишь сама команда, остальные предложения добавляют по задаче.
  • Логический порядок выполнения — FROM, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, LIMIT; поэтому псевдоним нельзя использовать в WHERE, но можно в ORDER BY.
  • Три частые ловушки WHERE: приоритет AND над OR, сравнение с NULL через = и NOT IN со списком, где есть NULL.
  • Порядок строк гарантирует только ORDER BY с ключом, разрешающим ничьи; синтаксис LIMIT, место NULL при сортировке и регистр в LIKE различаются по СУБД.
  • Значения от пользователя передают параметрами, а не вклеивают в текст запроса.

Где применяется / связь с практикой

SELECT — самая частая команда в работе с базой: на ней строятся отчеты аналитика, API бэкенд-разработчика и проверки данных тестировщика. Следующий шаг после одиночной таблицы — соединения (JOIN), подзапросы, оконные функции и чтение плана запроса. Если нужна общая картина языка до деталей SELECT, есть обзор SQL для начинающих.

Освойте тему на практике

Системно разобрать выборки, соединения и оптимизацию запросов на задачах с проверкой можно на курсе «SQL для разработчиков и аналитиков». Попробовать формат занятий бесплатно можно на открытых уроках Otus.

FAQ

Чем SELECT отличается от SELECT DISTINCT?
SELECT возвращает все подходящие строки, включая повторы; DISTINCT оставляет по одной копии каждой одинаковой строки результата. На больших таблицах DISTINCT требует сортировки или хеширования и может заметно замедлить запрос.

Можно ли выбрать данные сразу из двух таблиц?
Да, для этого в FROM таблицы соединяют через JOIN ... ON по связующему столбцу. Результаты двух отдельных запросов с одинаковым набором столбцов объединяет UNION (без дубликатов) или UNION ALL (с дубликатами, быстрее).

Меняет ли SELECT данные в таблице?
Обычный SELECT строки таблиц не меняет. Исключения: SELECT ... INTO в SQL Server и PostgreSQL создает новую таблицу, вызов функции с побочным эффектом (например, SELECT nextval('seq') в PostgreSQL) меняет состояние последовательности, а SELECT ... FOR UPDATE данные не меняет, но блокирует строки до конца транзакции.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)