7786
Авторский канал про Базы Данных и SQL Ресурсы, гайды, задачи, шпаргалки. Информация ежедневно пополняется! Автор: @energy_it
Почему NOT IN может вернуть пустой результат из-за NULL!
Одна из самых неприятных ловушек SQL — поведение NOT IN при наличии NULL. Запрос выглядит абсолютно корректно, но внезапно перестаёт возвращать строки.
Таблицы:
users(id)
bans(user_id)
bans. Часто пишут так:SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM bans
);
bans.user_id нет NULL — всё работает нормально. Но представим данные:bans
-------
1
2
NULL
0 rows
id <> 1
AND id <> 2
AND id <> NULL
id <> NULL
WHERE проходят только TRUE. Из-за этого всё условие целиком перестаёт выполняться.NULL = 1
NULL <> 1
NULL = NULL
NOT IN с NULL внутри подзапроса становится опасным.NOT EXISTS:SELECT *
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM bans b
WHERE b.user_id = u.id
);
EXISTS работает нормально: сравнение идёт построчно; NULL не ломает всю проверку; оптимизатор обычно хорошо превращает это в anti-join.SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM bans
WHERE user_id IS NOT NULL
);
NOT EXISTS обычно считается более безопасным и читаемым решением.NOT IN () и NOT EXISTS могут давать разные планы выполнения в зависимости от СУБД. Но логически для nullable данных: NOT EXISTS почти всегда предпочтительнее.SELECT COUNT(*)
FROM bans
WHERE user_id IS NULL;
NOT IN уже потенциально опасен.NOT IN и NULL плохо сочетаются. Если в подзапросе появляется хотя бы один NULL, условие может перестать возвращать строки вообще. Для таких проверок надёжнее использовать NOT EXISTS.
BUFFERS: как увидеть цену запроса?
Обычный EXPLAIN ANALYZE показывает время выполнения, но почти ничего не говорит о том, сколько данных было прочитано с диска и из памяти.
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...
BUFFERS добавляет статистику по страницам памяти:shared hit — страницы найдены в PostgreSQL shared buffers.shared read — страницы пришлось загрузить в shared buffers; это может быть физический диск или OS page cache.Buffers: shared hit=15000 read=2
Buffers: shared hit=10 read=15000
execution time могут иметь абсолютно разную нагрузку на систему.
Генерация пропущенных дат в отчётах: почему LEFT JOIN не помогает!
Классическая задача — построить отчёт по дням. Например, количество заказов по датам. И внезапно оказывается, что в выдаче нет некоторых дней.
Таблица:
orders(id, created_at)
SELECT
DATE(created_at) AS day,
COUNT(*) AS orders_count
FROM orders
GROUP BY DATE(created_at)
ORDER BY day;
LEFT JOIN сама по себе не помогает — без таблицы дат нечего достраивать. Правильный подход — сначала явно сгенерировать календарь, а потом присоединить данные.generate_series:WITH calendar AS (
SELECT generate_series(
DATE '2026-01-01',
DATE '2026-01-10',
INTERVAL '1 day'
)::date AS day
)
SELECT
c.day,
COUNT(o.id) AS orders_count
FROM calendar c
LEFT JOIN orders o
ON DATE(o.created_at) = c.day
GROUP BY c.day
ORDER BY c.day;
COUNT(o.id) вернёт 0.COUNT(o.id), а не COUNT(*): COUNT(*) посчитает строку календаря даже без совпадений — всегда ≥ 1; COUNT(o.id) считает только реальные заказы — корректный 0 при отсутствии данных.JOIN: ON DATE(o.created_at) = c.day. Так писать удобно, но это ломает использование индекса по created_at.ON o.created_at >= c.day
AND o.created_at < c.day + INTERVAL '1 day'
SELECT
c.day,
COUNT(o.id) AS orders_count,
SUM(COUNT(o.id)) OVER (ORDER BY c.day) AS running_total
FROM calendar c
LEFT JOIN orders o
ON o.created_at >= c.day
AND o.created_at < c.day + INTERVAL '1 day'
GROUP BY c.day
ORDER BY c.day;
WITH bounds AS (
SELECT
MIN(created_at)::date AS min_day,
MAX(created_at)::date AS max_day
FROM orders
),
calendar AS (
SELECT generate_series(min_day, max_day, INTERVAL '1 day')::date AS day
FROM bounds
)
SELECT ...
created_at — timestamptz, границы дня зависят от timezone.
LAG() в SQL — сравниваем строку с предыдущей без JOIN!
Оконная функция LAG() позволяет получить значение из предыдущей строки внутри группы. Это один из самых полезных инструментов для аналитики, логов и временных рядов.
Таблица:
payments(id, user_id, amount, created_at)
Посмотрим предыдущий платёж каждого пользователя:
SELECT
user_id,
amount,
LAG(amount) OVER (
PARTITION BY user_id
ORDER BY created_at
) AS prev_amount
FROM payments;
LAG() сдвигает значение на одну строку назад внутри окна пользователя.SELECT
user_id,
amount,
amount - LAG(amount) OVER (
PARTITION BY user_id
ORDER BY created_at
) AS diff
FROM payments;
SELECT *
FROM (
SELECT
user_id,
status,
LAG(status) OVER (
PARTITION BY user_id
ORDER BY created_at
) AS prev_status
FROM user_logs
) t
WHERE status <> prev_status;
LAG() часто используют для анализа трендов, поиска изменений, расчёта дельт и работы с временными рядами.
Исключающие ограничения: гарантия отсутствия пересечений на уровне БД!
Проверять пересекаются ли интервалы через SELECT перед INSERT — классическая ошибка, которая ломается при конкурентных транзакциях.
SELECT 1
FROM bookings
WHERE room_id = 101
AND start_at < $end
AND end_at > $start;
CREATE EXTENSION IF NOT EXISTS btree_gist;
ALTER TABLE bookings
ADD CONSTRAINT no_overlap
EXCLUDE USING gist (
room_id WITH =,
tsrange(start_at, end_at) WITH &&
);
exclusion constraint — это способ переложить сложную логику (пересечения, диапазоны, уникальность по условиям) с приложения на уровень СУБД с полной защитой от состояния гонки.
Диапазоны как тип данных: убираем двойные условия и баги!
Когда работаешь с интервалами, обычно пишут два условия, и почти всегда где-то ошибаются с границами:
WHERE created_at <= now()
AND shipped_at > now()
tstzrange(created_at, shipped_at)
@> now()
@> проверяет содержит ли диапазон значение, заменяя сразу два условия.created_at <= x AND shipped_at > x:CREATE INDEX ON orders USING GIST (tstzrange(created_at, shipped_at));
GiST-индексом такие запросы работают эффективно даже на больших объёмах.range-типы — это способ убрать класс ошибок с датами и упростить сложную логику.
😍 SQL PostgreSQL Course — информативный курс по SQL с практикой!
Это структурированное обучение по PostgreSQL: от базовых запросов до JOIN’ов, подзапросов и проектирования базы данных. Материал выстроен последовательно, поэтому легко идти шаг за шагом и не теряться в теме. Здесь есть практика с заданиями и ответами, за счёт чего обучение закрепляется.
Оставляю ссылочку: GitHub 📱
Дубликаты после JOIN — откуда берутся и как контролировать!
Одна из частых проблем — внезапное размножение строк после JOIN. Это базовое поведение, если не учтена кардинальность связей.
Таблицы:
orders(id, customer_id, amount)
payments(id, order_id, status)
SELECT
o.id,
o.amount,
p.status
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.id;
orders — payments. То есть один заказ повторится столько раз, сколько у него записей в payments.SELECT
COUNT(*) AS total_orders
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.id;
COUNT(*) считает строки уже после JOIN, а не заказы. Если у заказа 3 платежа — он попадёт в счёт 3 раза. Это одна из самых частых причин кривых метрик.SELECT
COUNT(DISTINCT o.id) AS total_orders
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.id;
DISTINCT — это дополнительная операция, и на больших объёмах она может стоить дорого.payments, JOIN здесь вообще лишний. Лучше контролировать кардинальность до JOINEXISTS:SELECT
o.id,
o.amount
FROM orders o
WHERE EXISTS (
SELECT 1
FROM payments p
WHERE p.order_id = o.id
AND p.status = 'success'
);
EXISTS работает как semi-join: он проверяет факт наличия строки, но не тянет её в результат. За счёт этого одна строка заказа остаётся одной строкой.JOIN всё-таки нужен — агрегируем заранее:SELECT
o.id,
o.amount,
COALESCE(p.has_success, 0) AS has_success
FROM orders o
LEFT JOIN (
SELECT
order_id,
MAX(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS has_success
FROM payments
GROUP BY order_id
) p ON p.order_id = o.id;
payments к одной строке на order_id, и только потом делаем JOIN. После этого результат становится понятным: одна строка на заказ, без раздувания.GROUP BY o.id, o.amount, p.status
GROUP BY просто фиксирует текущую детализацию. Если у заказа было несколько статусов — строки никуда не денутся.JOIN не создаёт дубликаты сам по себе. Он возвращает строки в соответствии с числом совпадений по условию ON. Если после JOIN строк стало больше — значит реальная связь между таблицами не 1:1, а 1:N или даже N:M.
☕️ Полезную статью нашёл на Хабре: «Ваш RAG не умеет думать. А мой умеет»!
В этой статье:• Разбирается, почему классические RAG-системы на базе векторных БД часто ограничиваются поверхностным поиском;
• Показан подход HippoRAG 2, где память и связи между фактами организованы ближе к графовой модели;
• Объясняется, как эволюционирует работа с данными;
• Рассматривается, как это влияет на SQL/NoSQL слой, архитектуру хранения и построение более осмысленных запросов к данным.
🔊 Продолжайте читать на Habr!
Проверяем дубликаты и считаем уникальные значения!
В больших таблицах важно быстро находить повторяющиеся записи и понимать, сколько уникальных элементов. Это полезно для контроля качества данных и аналитики.
Создадим таблицу пользователей:
CREATE TABLE users (
user_id INT,
email VARCHAR(100)
);
INSERT INTO users VALUES
(1, 'alice@mail.com'),
(2, 'bob@mail.com'),
(3, 'alice@mail.com'),
(4, 'carol@mail.com'),
(5, 'bob@mail.com');
SELECT email, COUNT(*) AS cnt,
CASE WHEN COUNT(*)>1 THEN 'Duplicate' ELSE 'Unique' END AS status
FROM users
GROUP BY email;
COUNT() + GROUP BY группирует одинаковые значения, а CASE сразу классифицирует их как дубликаты или уникальные.email | cnt | status
-----------------------------
alice@mail.com | 2 | Duplicate
bob@mail.com | 2 | Duplicate
carol@mail.com | 1 | Unique
❤️ Нашел вам html5css — простой и наглядный справочник по SQL на русском!
Если хочется быстро разобраться в командах — этот сайт отлично подойдёт. Конструкции объяснены на примерах, с чётким и кратким синтаксисом. Удобно использовать как справочник или экспресс‑повторение.
📌 Оставляю ссылочку: html5css.ru
➡️ SQL Ready | #ресурс
Совет на 2026 год — переходите в ML.
Пока обычные разрабы конкурируют с ИИ-копилотами, ML-инженеры эти самые нейронки создают.
В эпоху нейростей это самые востребованые люди в мире программирования. Зарплаты мидлов начинаются от 250 000 ₽, а у сеньоров в BigTech доходят до 700 000 ₽.
А чтобы освоить его всего за 4 месяца без лишней суеты — изучите канал Артема Алехина.
Его бэкграунд: Руководитель команды в Сбере, валютная удаленка. К 22 годам вышел на доход 1 000 000+ ₽ в месяц.
На канале вы найдёте:
— Всё про самые востребованные стеки(Python, ИИ-агенты, NLP) и почему математика — это не страшно, если учить только нужное.
— Как оформить резюме, чтобы оно пролетало через любые LLM-фильтры и ATS-системы прямо к тимлидам.
— Скрипты переговоров, которые помогли его ученикам прыгнуть с 0 до 360к всего за 8 месяцев.
Во времена острой нехватки ML-разработчиков, это лучшее время, чтобы перекатиться. Переходи и изучай: /channel/+qG_ihzUEkHJlOWVi
🖥 Когда система нагружена сильнее всего?
В системах с временными интервалами важно понимать не просто отдельные события, а их наложение — именно оно определяет реальную нагрузку.
Сегодня в задаче:
• Преобразуем интервалы в точки начала и конца, чтобы работать с ними как с потоком событий;
• Посчитаем текущую нагрузку через накопительную сумму по времени;
• Найдём момент максимального количества одновременных событий — пик нагрузки системы.
☕️ Годную статью нашёл на Хабре: «Как работает распределённый SQL в YDB: от запроса до выполнения»!
В этой статье:• Показано, как в YDB обрабатывается SQL-запрос — от парсинга до распределённого исполнения по узлам;
• Разбирается, как устроены планировщик, оптимизатор и механизмы шардинга при работе с большими данными;
• Объясняется, как достигаются консистентность, отказоустойчивость и масштабируемость в распределённой базе.
🔊 Продолжайте читать на Habr!
📂 Напоминалка по блокировкам и транзакциям в SQL!
Optimistic locking позволяет работать без блокировок — конфликт проверяется при записи (например, через version или updated_at). Pessimistic locking наоборот сразу ставит блокировку (SELECT ... FOR UPDATE) и заставляет другие транзакции ждать.
На картинке — как два подхода ведут себя при одновременном обновлении одной строки: в одном случае получаем conflict, в другом — очередь.
Сохрани, чтобы не потерять!
➡️ SQL Ready | #ресурс
📂 Напоминалка по Data Engineering и аналитической архитектуре!
Например, Data Warehouse хранит уже очищенные и структурированные данные для BI и отчетности, Data Lake позволяет складировать сырые данные любого типа и обрабатывать их по мере необходимости, а Data Mesh распределяет владение данными между бизнес-доменами и превращает данные в полноценные data products.
На картинке — основные отличия: как хранятся данные, кто ими управляет, как происходит обработка и для каких задач лучше подходит каждый подход.
Сохрани, чтобы не потерять!
➡️ SQL Ready | #ресурс
😍 SQL шпаргалка для начинающих — база по работе с бд!
Удобный и понятный справочник, где разобраны основные SQL-запросы и конструкции: SELECT, WHERE, JOIN, INSERT, UPDATE и другие ключевые команды. Всё объясняется простым языком с примерами, чтобы быстро понять логику работы с данными и начать применять это в реальных задачах
📌 Оставляю ссылочку: ermita.one
➡️ SQL Ready | #ресурс
📂 Напоминалка по DB Sharding!
Шардинг помогает масштабировать базу данных, распределяя данные между несколькими серверами. На картинке — основные стратегии партиционирования и шардинга.
Например, key-based sharding использует hash-функцию для равномерного распределения записей, а range-based sharding делит данные по диапазонам значений.
Сохрани, чтобы не потерять!
➡️ SQL Ready | #ресурс
🐱 Awesome Postgres — огромная база всего, что связано с PostgreSQL!
В этом репозитории собраны лучшие инструменты, библиотеки, GUI-клиенты, ORM, статьи, курсы и сервисы для работы с PostgreSQL. По сути — это подборка всего полезного для backend-разработчика и DBA в одном месте. Внутри есть материалы не только по базовой работе с БД, но и по мониторингу, репликации, производительности, миграциям и архитектуре.
Оставляю ссылочку: GitHub 📱
📂 Напоминалка по тому, как работает PostgreSQL!
Например, каждое подключение создаёт отдельный процесс, а любые изменения сначала попадают в WAL, и только потом гарантированно пишутся в данные.
На картинке — базовая архитектура: подключения и backend-процессы, shared memory, background и auxiliary процессы и физическое хранение.
Сохрани, чтобы не потерять!
➡️ SQL Ready | #ресурс
Отклики, после которых зовут на собеседования
Главный фокус команды ии-ассистента Софи:
1. Ребята переписали парсер - теперь собирают вакансии из 110+ источников: Telegram, Хабр, LinkedIn и другие.
2. Параллельно сделали умный мэтчер - Софи теперь показывает не всё подряд, а то, что подходит именно вам
(локация, стек, грейд + обучение на фидбэке юзеров).
На этом строится основа автооткликов - быстрых, точных и стабильных.
Следующий шаг - откалибровать мэтчер до точности 80%+, но это невозможно без реальных пользователей.
Поэтому в ближайшие дни они открывают доступ к бесплатному тестированию.
Мест будет немного, так как фокус на качестве.
➡️Если хочешь попасть - подписывайся на канал, там будет анонс о наборе.
😎 SQL Server Kit — практическая база инструментов и материалов!
Репозиторий представляет собой обширную коллекцию скриптов, статей, рекомендаций и утилит для администрирования и разработки на SQL. Внутри собраны решения для диагностики производительности, оптимизации запросов, мониторинга, резервного копирования, работы с индексами и анализа состояния базы данных. Также представлены ссылки на проверенные источники и лучшие практики работы с СУБД.
Оставляю ссылочку: GitHub 📱
Совет на всю жизнь — начните изучать вайбкодинг.
Нейронки уже собирают проекты от идеи до релиза, пишут код, находят и исправляют баги лучше команды айтишников. Это сэкономит вам десятки часов работы.
А чтобы научиться вайбкодить и не совершать ошибки - читайте канал AI-архитектор, где есть:
➖Инструкции по Antigravity и n8n.
➖Связки, которые приносят от 100 000 ₽ за проект
➖Реальный опыт программиста и вайбкодера, который настраивает автоматизацию ИИ для гос. компаний.
Подписывайтесь и смотрите в закрепе, как за 2 дня собрать ИИ-автоматизацию без единой строчки кода: /channel/+eBc7ivvcY34xZTli
Использование data-modifying CTE для цепочек операций в одном запросе!
Обычно изменения и чтение делают разными запросами, что создаёт лишние round-trip и риск рассинхронизации данных между операциями:
UPDATE orders
SET status = 'processing'
WHERE id = 123
RETURNING *;
RETURNING уже даёт доступ к изменённым строкам, но data-modifying CTE позволяет пойти дальше и использовать их в следующих шагах:WITH updated AS (...)
CTE фиксирует результат изменения и гарантирует, что последующие операции работают с тем же набором строк в рамках одного statement:INSERT INTO audit_log (order_id, new_status)
SELECT id, status
FROM updated;
SELECT.data-modifying CTE — это способ строить сложные, но безопасные цепочки операций внутри одного SQL-запроса.
📂 Напоминалка по реляционной модели и SQL!
Например, Primary Key гарантирует уникальность записей, Foreign Key — обеспечивает ссылочную целостность, а JOIN’ы позволяют собирать распределённые данные в единую выборку.
На картинке — основные концепции, которые используются при проектировании и работе с БД.
Сохрани, чтобы не потерять!
➡️ SQL Ready | #ресурс
🖥 Напоминалка по продвинутым функциям работы с датами!Эта шпаргалка собрала ключевые методы, которые позволяют не просто сравнивать даты, а точно извлекать нужные части, округлять по нужному уровню, форматировать для отчётов и рассчитывать интервалы в удобной форме.
➡️ SQL Ready | #шпора
😎 SQLServerCentral — крупнейшее сообщество и база знаний по Microsoft SQL Server!
Здесь публикуются ежедневные статьи, обучающие серии Stairway, подборки скриптов, обзоры книг, а также активные форумы и блоги для администраторов БД и разработчиков.
📌 Оставляю ссылочку: sqlservercentral
➡️ SQL Ready | #ресурс
🔥База платных курсов и книг по программированию на весь 2026 год!🔥
Канал новый, потому что слишком много людей ждут халявы.
Тут всё, что обычно стоит тысячи:
— Авторские курсы, которые не найти в открытом доступе
— Самые свежие книги для старта
— Всё, что нужно, чтобы за полгода вырасти от мидла до сеньора
➡️ Залетайте, пока есть доступ!
Почему после JOIN внезапно растут агрегаты!
Одна из самых неприятных ошибок в аналитическом SQL — когда запрос выглядит нормально, всё отрабатывает без ошибок, а цифры в итоге получаются больше, чем должны быть.
Классическая ситуация. Есть таблицы:
orders(id, customer_id, amount)
order_items(id, order_id, product_id, quantity)
SELECT SUM(o.amount) AS total_revenue
FROM orders o
JOIN order_items i
ON i.order_id = o.id;
order_items, то строка из orders после JOIN повторится 3 раза. И o.amount тоже попадёт в расчёт 3 раза. В итоге сумма завышается. Это как раз тот самый fan-out: одна строка размножается после JOIN.SELECT
COUNT(*) AS rows_after_join,
COUNT(DISTINCT o.id) AS unique_orders
FROM orders o
JOIN order_items i
ON i.order_id = o.id;
JOIN стало больше, чем уникальных заказов, значит у вас fan-out по уровню orders. Что делать правильно — зависит от задачи.JOIN вообще не нужен:SELECT SUM(amount)
FROM orders;
JOIN нужен только для фильтрации, например по конкретному товару, безопаснее использовать EXISTS:SELECT SUM(o.amount)
FROM orders o
WHERE EXISTS (
SELECT 1
FROM order_items i
WHERE i.order_id = o.id
AND i.product_id = 10
);
orders не ломается. Один заказ остаётся одной строкой.order_items:SELECT SUM(o.amount)
FROM orders o
JOIN (
SELECT DISTINCT order_id
FROM order_items
WHERE product_id = 10
) i ON i.order_id = o.id;
order_items:SELECT SUM(i.quantity)
FROM order_items i;
SELECT SUM(DISTINCT o.amount)
FROM orders o
JOIN order_items i
ON i.order_id = o.id;
DISTINCT сейчас всё починит, на деле — нет. Почему это плохая идея: если у двух разных заказов одинаковый amount, один из них просто схлопнется; запрос начинает давать вроде бы правдоподобный, но неверный результат.JOIN не один, а цепочка: orders — order_items — products — categoriesSELECT COUNT(*) FROM orders;
SELECT COUNT(*)
FROM orders
JOIN order_items ON order_items.order_id = orders.id;
SELECT COUNT(*)
FROM orders
JOIN order_items ON order_items.order_id = orders.id
JOIN products ON products.id = order_items.product_id;
JOIN начинаются лишние строки.JOIN, всегда держите в голове гранулярность данных. Что у вас является одной строкой: заказ, позиция заказа, клиент? Сначала определяете уровень данных — потом джойните и агрегируете.
Как вернуть строки в том же порядке, в котором пришли id?
Когда из приложения прилетает список id, обычный WHERE id = ANY(...) находит нужные строки, но порядок входного массива не сохраняет:
SELECT *
FROM users
WHERE id = ANY(ARRAY[42, 7, 99]);
WITH ORDINALITY добавляет каждой строке её позицию во входном наборе:unnest(ARRAY[42, 7, 99]) WITH ORDINALITY
ORDER BY x.ord
CASE, без ручной сортировки в коде и без лишнего постобработчика.WITH ORDINALITY — это простой способ сохранить порядок входных данных в SQL, он хорошо заходит в API и массовые выборки.