Каждый день одно и то же. Открываешь клиент базы данных, чтобы что-то проверить, посчитать или найти. И снова пишешь почти тот же SELECT, что и вчера, с тем же WHERE и JOIN. Знакомо?
SQL в большинстве случаях не требует сложные 100-строчные запросы с вложенными подзапросами на три уровня глубины. Чаще всего нам нужны простые, отточенные и, главное, эффективные конструкции.
В этой статье я собрал 7 таких запросов-«рабочих лошадок». Это не какой-то там справочник, а готовая шпаргалка для реальных задач.
Прежде чем перейти к запросам, запомните главное правило любого UPDATE или DELETE: всегда сначала пишите SELECT с теми же условиями WHERE.
Серьёзно, всегда. Это убережёт вас от случайного изменения или удаления не тех данных. Один мой знакомый в начале карьеры однажды чуть не отправил в архив несколько гигабайт production-данных из-за неверного условия. С тех пор он, да и я тоже делаем так:
-- Шаг 1: Сначала СМОТРИМ, что будем обновлять/удалять
SELECT * FROM orders WHERE status = 'test';
-- Шаг 2: Убедившись, что выборка корректна, заменяем на UPDATE/DELETE
DELETE FROM orders WHERE status = 'test';
Теперь, когда этот критически важный ритуал усвоен, перейдём к тем самым «рабочим лошадкам».
❯ 1. Ежедневный мониторинг
Как обычно, первым делом с утра нам нужно проверить, что система жива, данные поступают, ничего не сломалось ночью. Без ORDER BY и LIMIT можно случайно выгрузить всю таблицу и подвесить клиент, чего явно нам не хотелось бы.
SELECT id, status, created_at, amount
FROM latest_transactions
WHERE status = 'processed'
ORDER BY created_at DESC
LIMIT 10;
Ключевая привычка — всегда использовать ORDER BY для детерминированности и LIMIT для безопасности.
Допустим, вы увидели, что данные поступают. Что дальше? Обычно следом просят свестить итоги и подготовить отчет.
❯ 2. Агрегация данных
Самый частый запрос для любого отчёта. Посчитать количество, сумму, среднее значение по группам. Здесь на первый план выходят GROUP BY и агрегатные функции.
SELECT
user_id,
COUNT(*) AS total_orders,
SUM(amount) AS total_revenue
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY user_id
HAVING SUM(amount) > 1000
ORDER BY total_revenue DESC;
Считает выручку по каждому пользователю с начала года, но показывает только «VIP-клиентов», принесших больше 1000 единиц. Запомните: WHERE фильтрует строки догруппировки, а HAVING — после.
Готовя отчеты, но вы вдруг замечаете аномалии: некоторые цифры не сходятся. Есть подозрение на дубликаты.
❯ 3. Охота за дубликатами
Дубликаты — вечная головная боль. Они появляются из-за ошибок в логике приложения, сбоев и багов. Их нужно уметь быстро находить. Спасет GROUP BY и HAVING.
SELECT
email,
COUNT(*) AS duplicate_count
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
Находит все email-адреса, которые встречаются в таблице пользователей больше одного раза. Это отправная точка для «зачистки».
Дубликаты найдены. Чтобы принять осмысленное решение по ним, нужно больше информации. Пора заглянуть в связанные таблицы.
❯ 4. Объединение данных из нескольких таблиц
Данные в нормализованной базе всегда разбросаны. Чтобы получить полную картину, нужны JOIN-ы.
SELECT
u.first_name,
u.last_name,
o.order_date,
o.total_amount,
p.product_name
FROM users u
INNER JOIN orders o ON u.id = o.user_id
LEFT JOIN products p ON o.product_id = p.id
WHERE u.country = 'Germany'
ORDER BY o.order_date DESC;
Главное — не запутаться в их типах.
Их всего 3:INNER JOIN - только совпадающие записи с обеих сторон
LEFT JOIN - все записи из левой таблицы + совпадения справа
RIGHT JOIN - все записи из правой таблицы + совпадения слева
Объединив данные, вы обнаруживаете сложную закономерность: нужно найти записи, которые соответствуют условию на основе других данных.
❯ 5. Запросы в запросах
Когда простых условий WHERE уже недостаточно, на помощь приходят подзапросы. Они позволяют вложить один запрос в другой для сложной фильтрации.
SELECT *
FROM products
WHERE price > (
SELECT AVG(price)
FROM products
WHERE category_id = 5
)
AND category_id = 5;
Находит все товары в категории 5, цена которых выше средней по этой же категории. Идеально для поиска аномалий и выбросов.
Проанализировав данные, вы понимаете, что пора не просто смотреть, а что-то менять. Самое опасное — модификация данных.
❯ 6. Модификация данных с умом
Вот мы и дошли до самого опасного — UPDATE и DELETE. Здесь правило только одно: семь раз отмерь, один раз выполни. Всегда начинай с SELECT.
-- ШАГ 1: Сначала смотрим, что будем менять
SELECT *
FROM users
WHERE last_login < '2024-01-01'
AND active = true;
-- ШАГ 2: Только затем выполняем UPDATE (и лучше в транзакции!)
BEGIN;
UPDATE users
SET active = false
WHERE last_login < '2024-01-01'
AND active = true;
-- Смотрим, что получилось, и только потом COMMIT или ROLLBACK
COMMIT;
Данные обновлены. Теперь нужно проанализировать результат в разрезе времени или групп, не схлопывая строки. Обычного GROUP BY уже мало.
❯ 7. Оконные функции
Финальный аккорд — мощные оконные функции (OVER, PARTITION BY). Они позволяют делать агрегацию без группировки, сохраняя все исходные строки.
SELECT
product_id,
sale_date,
daily_revenue,
SUM(daily_revenue) OVER (
PARTITION BY product_id
ORDER BY sale_date
) AS cumulative_revenue
FROM sales
ORDER BY product_id, sale_date;
Для каждого товара рассчитывает нарастающий итог выручки по дням. В отличие от GROUP BY, мы видим каждую продажу, а не только итог.
Вот и все. Этих 7 запросов хватит, чтобы уверенно чувствовать себя в 90% рабочих ситуаций с SQL. Они покрывают весь цикл работы с данными: от ежедневного мониторинга до сложной аналитики и аккуратной модификации.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
? Читайте также:
➤ Технологии из прошлого: телетайп. Разбираемся в устройстве, воскрешаем старый аппарат
➤ Болезнь Крона, осы-паразиты и «больной нытик из Цюриха»: что стоит за главной сценой «Чужого»
Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.
Комментарии (0)

Akina
16.09.2025 11:06Без указания конкретной СУБД (а ещё лучше - и её версии) такая статья ещё до прочтения теряет половину применимости.
Ежедневный мониторинг
Такой запрос пригоден только в том случае, если имеется индекс по
created_at(или индекс, где это поле является префиксом). Иначе можно неплохо нагрузить сервер, тем более что таблица транзакций редко бывает компактной. Если в таблице имеется первичный индекс по полю автоинкремента или UUIDv7, сортировку лучше выполнять по нему.Их всего 3 (речь о различных JOIN)
Огорчу. Их немножко больше. FULL, CROSS... и даже LATERAL.
Запросы в запросах
CTE для этой цели подходят лучше.
Модификация данных с умом
Это модификация с небольшим количеством ума. Малейший ляп при составлении второго запроса - и привет, проблемы. Особенно если запрос многотабличный, а условий много. Тут надо не только проверять все условия, но ещё и следить за стопроцентным наличием, и при этом за отсутствием интерференции, у алиасов - от таких ляпов, между прочим, даже копипаст не спасает.
Чтобы вероятность ошибиться была меньше, можно идти более аккуратным путём. Пишется представление, а не SELECT-запрос. Потом SELECT-ом к представлению проверяем, что собираемся модифицировать. И затем пишем запрос на модификацию/удаление, выполняя операцию из другой копии таблицы и связывая её с представлением по первичному ключу.
-----
А вообще, как по мне, то столь простые запросы лучше как раз писать "на лету", а не заниматься сохранением шаблонов. Но это моё личное мнение...

NightKiro
16.09.2025 11:06Если они нужны раз в N времени, или в том числе манагерам, то можно и сохранить
А если это происходит регулярно, то можно и скриптов прикрутить, с мордой чтобы тыкать было удобно
Правда обычно это называется админкой
Но по идее автор пытается сделать шпаргалку для лёгкого запоминания?..

Akina
16.09.2025 11:06Но по идее автор пытается сделать шпаргалку для лёгкого запоминания?
Вот положа руку хоть куда-нибудь... вы лично правда считаете такую форму именно "шпаргалкой для лёгкого запоминания"? Как по мне, поиск нужного шаблона в подобного рода шпаргалке (а она, вот сто пудов, быстро распухнет) гораздо более затратен, чем написание запроса "на лету" по требуемой логике. Вы сами пишете, что задача регулярна - а, значит, эти постоянно выполняемые запросы должны просто в мозгу сидеть и набираться чуть ли не с закрытыми глазами. А если ещё учесть, что в большинстве случаев взятый шаблон нужно корректировать, подставляя в критерии актуальные значения для условий отбора...
Нет, вот правда, я уже этот этап проходил, когда был в начинающих. Месяц, максимум два - и вроде бы поначалу удобный инструмент превращается в гирю на ноге, потом в хлам, хранимый "на всякий случай, вдруг понадобится", а потом просто удаляется, дабы не засорял каталог.

Borelli
16.09.2025 11:06CTE для этой цели подходят лучше.
Чем они лучше? Кроме возможности рекурсии и повышения читабельности запроса за счёт сокращения объема текста бонусов не так уж много. Однажды я переписал многостраничный SQL на 7 или 8 уровней CTE со связями между таблицами, семантически выделил полезные опорные сущности. Стало красиво. Запустил с показом плана выполнения (в MSSQL было дело), и был поражен гигантскому графу где были разбросаны во множестве одинаковые ветки. Вдобавок, каждая из них вычислялась отдельно, и остальные ждали результата каждого кусочка. Хотя казалось бы, вычисли один раз CTE и подставь в остальных местах, но нет...
С подзапросами нужно быть аккуратными. В случае с JOIN - скорее всего подзапрос будет вычислен один раз, дальше пойдет объединение. А вот в WHERE можно попасть на то, что сколько будет сравнений = просмотрено строк в исходной таблице, столько же раз будет выполнен подзапрос, даже если он статический, и возвращает одинаковые данные не зависимо от текущей строки, проверяемой в WHERE.

vbdv
16.09.2025 11:06Спасибо автору.
Очень полезно, что напомнил об:
Begin Update -> Validation -> Commit Or Rollback
Очевидная же вещь, но всегда забываю или создаю сначала временную.
В общем - намотал на ус ;-)

Pusk1
16.09.2025 11:06Для хотфиксов есть универсальный поход с созданием таблицы, куда складываются оригинальные данные, а потом уже запрос на удаление. Лучше с условием по этой таблице, но не обязательно. Обычно достаточно копипаста из выюорки для новой таблицы.
Это позволит откатить изменение, если что то пошло не так.

Gbor
16.09.2025 11:06Не понимаю, почему селект звёздочка, а делет без звездочки.

Akina
16.09.2025 11:06SELECT выводит отдельные поля, и соответственно требует обязательного указания того, какие именно поля следует вывести.
DELETE же работает не с отдельными полями, а с всей записью как единым объектом. Потому указание полей в списке вывода бессмысленно.
Синтаксис DELETE предусматривает возможность удаления не из всех таблиц, входящих в источник данных, а только из некоторых. В этом случае после DELETE указывается список таблиц, из которых следует удалять записи, отвечающие условиям отбора (
DELETE t1, t2 FROM t1 JOIN t2 JOIN t3 ...).Некоторые серверы допускают в этом списке и формальное указание шаблона "все поля" для всех или некоторых таблиц (
DELETE t1.*, t2 FROM t1 JOIN t2 JOIN t3 ...), но этот "довесок" просто игнорируется. Попытка же указать в списке отдельные поля (даже если указан полный список полей таблицы, но по отдельности) вызовет ошибку синтаксиса.
Gbor
16.09.2025 11:06Да просто сказано замени просто селект на делет и всё. А вот и не всё, оказывается...

suburg
16.09.2025 11:06Запрос №1 очень опасный. Если нет индекса по полю created_at, то будет sequence scan, что на большой таблице создаст нагрузку на систему. ORDER BY на больших таблицах надо с осторожностью использовать.

Pavel1114
16.09.2025 11:06Спорная ценность статьи. Вроде и запросы норм. Но тем кто их сам не может написать они не помогут - не смогут под себя адаптировать. А те кто может сам быстрее напишет.
А вообще мне нравится концепция реализованная в pycharm (может и в других IDE есть подобное) когда консоль подключения к базе храниться и редактируются как обычный файл с возможностью запуска отдельных команд. Там у меня и хранятся какие то сложные запросы.

AKimovd
16.09.2025 11:06Без
ORDER BYиLIMITможно случайно выгрузить всю таблицу и подвесить клиент, чего явно нам не хотелось бы.Сначала в любом случае будут выбраны все нужные стоки, потом все отсортированы и только потом будет фильтрация на limit 10.

Akina
16.09.2025 11:06Думаю, автор имел в виду именно выгрузку, то есть передачу массива выбранных данных на клиента. Что и вправду может оказаться процессом небыстрым и изрядно нагружающим сеть. Что же до выборки на сервере, то там скорее всего будут сортироваться не сами записи, а значения ключей сортировки с паре с уникальными идентификаторами, а затем уже выбираться сами записи из тела таблицы.

Shvarzne2ger
16.09.2025 11:06В примере 1 надо пояснить, что ограничение по количеству записей SELECT ... LIMIT 10 это для PostgreeSQL или MySQL.
Для MS SQL ограничение по количеству записей - это SELECT TOP 10 ...

olegtk55
16.09.2025 11:06Запрос с оконной функцией содержит серьезную и распространенную ошибку.
Если в один и тот же день (
sale_date) для одногоproduct_idбудет несколько записей, они будут считаться одной строкой внутри окна. Это может привести к результатам, когда несколько строк с одной датой будут иметь одинаковое значение нарастающего итогаПример
Без указания границ фрейма (Неправильно)
SELECT product_id,order_date,unit_price*quantity AS revenue,
SUM(unit_price*quantity)
OVER (PARTITION BY product_id ORDER BY order_date) AS sum_revenue
FROM Orders JOIN order_items oi USING (order_id)
WHERE product_id IN(19,20,21)
ORDER BY product_id, order_date
product_id|order_date|revenue |sum_revenue|
----------+----------+---------+-----------+
19|21-02-2017|270100.00| 270100.00|
19|09-03-2018|129500.00| 523550.00|
19|09-03-2018|123950.00| 523550.00|
19|21-12-2019|170200.00| 821400.00|
19|21-12-2019|127650.00| 821400.00|
20|17-07-2018|209760.00| 209760.00|
20|05-09-2020| 82800.00| 292560.00|
21|16-08-2017|190050.00| 190050.00|
21|02-11-2019| 61540.00| 467860.00|
21|02-11-2019|102240.00| 467860.00|
21|02-11-2019|114030.00| 467860.00|
С указанием границ фрейма (Правильно)
SELECT product_id,order_date,unit_price*quantity AS revenue,
SUM(unit_price*quantity)
OVER (PARTITION BY product_id ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS sum_revenue
FROM Orders JOIN order_items oi USING (order_id)
WHERE product_id IN(19,20,21)
ORDER BY product_id, order_date
product_id|order_date|revenue |sum_revenue|
----------+----------+---------+-----------+
19|21-02-2017|270100.00| 270100.00|
19|09-03-2018|129500.00| 399600.00|
19|09-03-2018|123950.00| 523550.00|
19|21-12-2019|170200.00| 693750.00|
19|21-12-2019|127650.00| 821400.00|
20|17-07-2018|209760.00| 209760.00|
20|05-09-2020| 82800.00| 292560.00|
21|16-08-2017|190050.00| 190050.00|
21|02-11-2019| 61540.00| 251590.00|
21|02-11-2019|102240.00| 353830.00|
21|02-11-2019|114030.00| 467860.00|

Sau
Мои запросы, которые хоть и не решают 90 % всех проблем на работе, а всего лишь нужны для случаев которые я не помню так сразу.
Синтаксис UPDATE JOIN для MySQL:
UPDATE tableA aJOIN tableB b ON a.a_id = b.a_idJOIN tableC c ON b.b_id = c.b_idSET b.val = a.val+c.valWHERE a.val > 10 AND c.val > 10;SELECT без таблицы для Firebird
SELECT 'abc' AS abc, 123 AS def, '20110101' AS dt FROM RDB$DATABASEВыбор строк с максимальной датой
SELECT * FROM dddJOIN (SELECT ddd2.da_id, MAX(ddd2.timestamp) dts, count(*) cFROM ddd ddd2 GROUP BY ddd2.da_id ) ddd3 ON ddd3.da_id = ddd.da_id AND ddd3.dts = ddd.timestampУдаление временной таблицы в MS SQL
IF OBJECT_ID('tempdb..#temp_table') is not null DROP TABLE #temp_tableОписание полей временной таблицы в MS SQL
SELECT * FROM Tempdb.Sys.Columns WHERE Object_ID = Object_ID('tempdb..#temp_table')А также синтаксис WITH RECURSIVE.
Akina
Только если поля связывания по факту NOT NULL, а записи в таблицах опять же по факту 1:1, а не 1:(0..1).
RANK() в помощь.