SELECT — команда SQL, которая читает данные из таблиц и возвращает их в виде результирующего набора: таблицы из нуля или более строк. Данные в базе она не меняет. Минимальный запрос — SELECT столбцы FROM таблица, а условия, сортировку и ограничение числа строк добавляют предложениями WHERE, ORDER BY и LIMIT.
Содержание
- Минимальный рабочий пример
- Синтаксис SELECT
- Порядок выполнения запроса
- WHERE: условия отбора строк
- ORDER BY: сортировка
- LIMIT и OFFSET: сколько строк вернуть
- DISTINCT: уникальные строки
- Псевдонимы: AS для столбцов и таблиц
- Значения из формы — только параметрами
- Если запрос не работает
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — рабочий пример, синтаксис по частям, логический порядок выполнения запроса и типовые ошибки. Все запросы проверены 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 данные не меняет, но блокирует строки до конца транзакции.



