Каждый день одно и то же. Открываешь клиент базы данных, чтобы что-то проверить, посчитать или найти. И снова пишешь почти тот же 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. Оконные функции

Финальный аккорд — мощные оконные функции (OVERPARTITION 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)


  1. Sau
    16.09.2025 11:06

    Мои запросы, которые хоть и не решают 90 % всех проблем на работе, а всего лишь нужны для случаев которые я не помню так сразу.

    Синтаксис UPDATE JOIN для MySQL:

    UPDATE tableA a

    JOIN tableB b ON a.a_id = b.a_id

    JOIN tableC c ON b.b_id = c.b_id

    SET b.val = a.val+c.val

    WHERE a.val > 10 AND c.val > 10;

    SELECT без таблицы для Firebird

    SELECT 'abc' AS abc, 123 AS def, '20110101' AS dt FROM RDB$DATABASE

    Выбор строк с максимальной датой

    SELECT * FROM ddd

    JOIN (SELECT ddd2.da_id, MAX(ddd2.timestamp) dts, count(*) c

    FROM 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.


    1. Akina
      16.09.2025 11:06

      Синтаксис UPDATE JOIN для MySQL

      Только если поля связывания по факту NOT NULL, а записи в таблицах опять же по факту 1:1, а не 1:(0..1).

      Выбор строк с максимальной датой

      RANK() в помощь.


  1. Akina
    16.09.2025 11:06

    Без указания конкретной СУБД (а ещё лучше - и её версии) такая статья ещё до прочтения теряет половину применимости.

    Ежедневный мониторинг

    Такой запрос пригоден только в том случае, если имеется индекс по created_at (или индекс, где это поле является префиксом). Иначе можно неплохо нагрузить сервер, тем более что таблица транзакций редко бывает компактной. Если в таблице имеется первичный индекс по полю автоинкремента или UUIDv7, сортировку лучше выполнять по нему.

    Их всего 3 (речь о различных JOIN)

    Огорчу. Их немножко больше. FULL, CROSS... и даже LATERAL.

    Запросы в запросах

    CTE для этой цели подходят лучше.

    Модификация данных с умом

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

    Чтобы вероятность ошибиться была меньше, можно идти более аккуратным путём. Пишется представление, а не SELECT-запрос. Потом SELECT-ом к представлению проверяем, что собираемся модифицировать. И затем пишем запрос на модификацию/удаление, выполняя операцию из другой копии таблицы и связывая её с представлением по первичному ключу.

    -----

    А вообще, как по мне, то столь простые запросы лучше как раз писать "на лету", а не заниматься сохранением шаблонов. Но это моё личное мнение...


    1. NightKiro
      16.09.2025 11:06

      Если они нужны раз в N времени, или в том числе манагерам, то можно и сохранить

      А если это происходит регулярно, то можно и скриптов прикрутить, с мордой чтобы тыкать было удобно

      Правда обычно это называется админкой

      Но по идее автор пытается сделать шпаргалку для лёгкого запоминания?..


      1. Akina
        16.09.2025 11:06

        Но по идее автор пытается сделать шпаргалку для лёгкого запоминания?

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

        Нет, вот правда, я уже этот этап проходил, когда был в начинающих. Месяц, максимум два - и вроде бы поначалу удобный инструмент превращается в гирю на ноге, потом в хлам, хранимый "на всякий случай, вдруг понадобится", а потом просто удаляется, дабы не засорял каталог.


    1. Borelli
      16.09.2025 11:06

      CTE для этой цели подходят лучше.

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

      С подзапросами нужно быть аккуратными. В случае с JOIN - скорее всего подзапрос будет вычислен один раз, дальше пойдет объединение. А вот в WHERE можно попасть на то, что сколько будет сравнений = просмотрено строк в исходной таблице, столько же раз будет выполнен подзапрос, даже если он статический, и возвращает одинаковые данные не зависимо от текущей строки, проверяемой в WHERE.


  1. vbdv
    16.09.2025 11:06

    Спасибо автору.

    Очень полезно, что напомнил об:

    Begin Update -> Validation -> Commit Or Rollback

    Очевидная же вещь, но всегда забываю или создаю сначала временную.

    В общем - намотал на ус ;-)


  1. Pusk1
    16.09.2025 11:06

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

    Это позволит откатить изменение, если что то пошло не так.


  1. Gbor
    16.09.2025 11:06

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


    1. Akina
      16.09.2025 11:06

      SELECT выводит отдельные поля, и соответственно требует обязательного указания того, какие именно поля следует вывести.

      DELETE же работает не с отдельными полями, а с всей записью как единым объектом. Потому указание полей в списке вывода бессмысленно.

      Синтаксис DELETE предусматривает возможность удаления не из всех таблиц, входящих в источник данных, а только из некоторых. В этом случае после DELETE указывается список таблиц, из которых следует удалять записи, отвечающие условиям отбора (DELETE t1, t2 FROM t1 JOIN t2 JOIN t3 ...).

      Некоторые серверы допускают в этом списке и формальное указание шаблона "все поля" для всех или некоторых таблиц (DELETE t1.*, t2 FROM t1 JOIN t2 JOIN t3 ...), но этот "довесок" просто игнорируется. Попытка же указать в списке отдельные поля (даже если указан полный список полей таблицы, но по отдельности) вызовет ошибку синтаксиса.


      1. Gbor
        16.09.2025 11:06

        Да просто сказано замени просто селект на делет и всё. А вот и не всё, оказывается...


  1. suburg
    16.09.2025 11:06

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


  1. Pavel1114
    16.09.2025 11:06

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


  1. AKimovd
    16.09.2025 11:06

    Без ORDER BY и LIMIT можно случайно выгрузить всю таблицу и подвесить клиент, чего явно нам не хотелось бы.

    Сначала в любом случае будут выбраны все нужные стоки, потом все отсортированы и только потом будет фильтрация на limit 10.


    1. Akina
      16.09.2025 11:06

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


  1. AKimovd
    16.09.2025 11:06

    Количество видов join несколько больше.


    1. Naf2000
      16.09.2025 11:06

      Вижу четыре INNER, LEFT, RIGHT, FULL

      Все остальное это доп условия. На схеме показаны все комбинации из трех упорядоченных подмножеств (кроме пустого набора), которые возможно реализовать этими JOIN


      1. AKimovd
        16.09.2025 11:06

        Есть ещё CROSS JOIN


        1. Naf2000
          16.09.2025 11:06

          это INNER JOIN ON TRUE

          Но тут много можно заменить )))


          1. Akina
            16.09.2025 11:06

            Скорее наоборот - INNER JOIN ON исторически есть CROSS JOIN WHERE.


  1. AKimovd
    16.09.2025 11:06

    Оконные функции

    Есть ещё иерархические и аналитические


  1. Shvarzne2ger
    16.09.2025 11:06

    В примере 1 надо пояснить, что ограничение по количеству записей SELECT ... LIMIT 10 это для PostgreeSQL или MySQL.

    Для MS SQL ограничение по количеству записей - это SELECT TOP 10 ...


    1. Naf2000
      16.09.2025 11:06

      сути это не меняет. Речь была об ограничении, а не синтаксисе


  1. 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|