Почему NOT IN может вернуть пустой результат из-за NULL!
Одна из самых неприятных ловушек SQL — поведение
Таблицы:
Допустим, нужно получить пользователей, которых нет в
Пока в
Теперь запрос внезапно вернет:
Хотя пользователи без бана есть. Почему так происходит — SQL сравнивает условие примерно как:
Но:
не даёт TRUE или FALSE. Результат: UNKNOWN. А в
Это особенность трёхзначной логики SQL: TRUE, FALSE, UNKNOWN
Любое сравнение с NULL даёт UNKNOWN:
Поэтому
Безопасный вариант —
Почему
Ещё вариант — явно убрать NULL:
Но на практике:
Отдельный момент:
Быстрая проверка проблемы:
Если такие строки есть —
🔥
➡️ SQL Ready | #практика
Одна из самых неприятных ловушек 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
Хотя пользователи без бана есть. Почему так происходит — SQL сравнивает условие примерно как:
id <> 1
AND id <> 2
AND id <> NULL
Но:
id <> NULL
не даёт TRUE или FALSE. Результат: UNKNOWN. А в
WHERE проходят только TRUE. Из-за этого всё условие целиком перестаёт выполняться.Это особенность трёхзначной логики SQL: TRUE, FALSE, UNKNOWN
Любое сравнение с NULL даёт UNKNOWN:
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.Ещё вариант — явно убрать NULL:
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.Please open Telegram to view this post
VIEW IN TELEGRAM
👍21❤10🔥5🤝1
This media is not supported in your browser
VIEW IN TELEGRAM
Здесь собрано огромное количество материалов по SQL: запросы, JOIN’ы, подзапросы, оконные функции, индексы, оптимизация и работа с PostgreSQL. Репозиторий отлично подходит как для изучения базы, так и для углубления в более сложные темы. Особенно полезно то, что здесь много практических примеров и разборов реальных SQL-конструкций.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12❤7🤝5
Например, индексы ускоряют поиск данных, а репликация помогает распределять нагрузку и повышать отказоустойчивость.
На картинке — 9 основных подходов для улучшения производительности базы данных, которые полезно держать под рукой.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13👍7🤝6
PostgreSQL может избежать полной сортировки таблицы при ORDER BY ... LIMIT!
Большинство думает, что
Если нужен только top-10 результат, PostgreSQL не обязан сортировать миллионы строк полностью. Вместо этого он держит в памяти только N лучших строк во время
Это видно прямо в
Особенно интересно становится на огромных таблицах, где полный
Даже при маленьком
А если добавить подходящий индекс:
🔥 PostgreSQL вообще сможет обойтись без Sort и просто сделать Index Scan в нужном порядке.
➡️ SQL Ready | #совет
Большинство думает, что
ORDER BY всегда сортирует вообще всю таблицу. Но в PostgreSQL для LIMIT есть отдельная оптимизация — Top-N Heap Sort.SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 10;
Если нужен только top-10 результат, PostgreSQL не обязан сортировать миллионы строк полностью. Вместо этого он держит в памяти только N лучших строк во время
scan.Это видно прямо в
execution plan:Sort Method: top-N heapsort
Особенно интересно становится на огромных таблицах, где полный
sort мог бы уйти на диск:SET work_mem = '4MB';
EXPLAIN ANALYZE
SELECT *
FROM events
ORDER BY created_at DESC
LIMIT 50;
Даже при маленьком
work_mem PostgreSQL может избежать полной сортировки всех строк и работать заметно быстрее именно благодаря оптимизации Top-N.А если добавить подходящий индекс:
CREATE INDEX idx_events_created_at
ON events (created_at DESC);
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13👍9🤝6
Почему индекс может не использоваться, даже если он есть!
Очень частая ситуация: индекс создан, запрос написан вроде нормально, но SQL всё равно делает Seq Scan или Full Table Scan.
Имеем такую таблицу:
Индекс:
Кажется, что такой запрос точно должен использовать индекс:
И обычно действительно будет Index Scan.
Но достаточно небольшой детали — и индекс может перестать использоваться. Например:
Проблема в том, что индекс построен по колонке:
Для оптимизатора это уже не то же самое условие. В итоге серверу часто приходится: применять
Исправляется это
То же самое часто происходит с датами:
Из-за:
обычный индекс по
Правильнее писать диапазон:
Так условие остаётся sargable — то есть пригодным для эффективного использования индекса.
Ещё одна частая проблема — преобразования типов. Например, плохо:
Если
Правильнее:
Важно: простое условие вида:
в некоторых СУБД может нормально привести литерал к integer и всё равно использовать индекс. Проблема чаще начинается там, где преобразуется сама колонка или выражение становится сложнее.
Популярная ошибка с
Здесь обычный B-Tree индекс обычно бесполезен. Так как поиск начинается не с начала строки; сервер не может эффективно использовать упорядоченность индекса.
Если таблица маленькая:
🔥 Наличие индекса ещё не гарантирует его использование. Функции над колонками, преобразования типов, неправильные
➡️ SQL Ready | #практика
Очень частая ситуация: индекс создан, запрос написан вроде нормально, но SQL всё равно делает Seq Scan или Full Table Scan.
Имеем такую таблицу:
users(
id,
email,
created_at
)
Индекс:
CREATE INDEX idx_users_email
ON users(email);
Кажется, что такой запрос точно должен использовать индекс:
SELECT *
FROM users
WHERE email = 'test@example.com';
И обычно действительно будет Index Scan.
Но достаточно небольшой детали — и индекс может перестать использоваться. Например:
SELECT *
FROM users
WHERE LOWER(email) = 'test@example.com';
Проблема в том, что индекс построен по колонке:
email. А в условии используется выражение:LOWER(email)
Для оптимизатора это уже не то же самое условие. В итоге серверу часто приходится: применять
LOWER() к строкам; сравнивать результат; читать гораздо больше данных, чем ожидалось.Исправляется это
expression/functional index:CREATE INDEX idx_users_email_lower
ON users(LOWER(email));
То же самое часто происходит с датами:
SELECT *
FROM orders
WHERE DATE(created_at) = '2025-01-10';
Из-за:
DATE(created_at)
обычный индекс по
created_at может не помочь, потому что функция применяется к колонке.Правильнее писать диапазон:
SELECT *
FROM orders
WHERE created_at >= '2025-01-10'
AND created_at < '2025-01-11';
Так условие остаётся sargable — то есть пригодным для эффективного использования индекса.
Ещё одна частая проблема — преобразования типов. Например, плохо:
WHERE user_id::text = '100'
Если
user_id — integer, то здесь преобразование применяется к колонке. В такой ситуации обычный индекс по user_id может не использоваться.Правильнее:
WHERE user_id = 100
Важно: простое условие вида:
WHERE user_id = '100'
в некоторых СУБД может нормально привести литерал к integer и всё равно использовать индекс. Проблема чаще начинается там, где преобразуется сама колонка или выражение становится сложнее.
Популярная ошибка с
LIKE:WHERE email LIKE '%gmail.com'
Здесь обычный B-Tree индекс обычно бесполезен. Так как поиск начинается не с начала строки; сервер не может эффективно использовать упорядоченность индекса.
Если таблица маленькая:
100–1000 строк, то Full Scan может быть дешевле, чем прыжки по индексу.LIKE, OR-условия и устаревшая статистика могут сделать индекс бесполезным или менее выгодным для оптимизатора.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15👍7🤝5
This media is not supported in your browser
VIEW IN TELEGRAM
Этот репозиторий хорошо подойдёт тем, кто постоянно работает с базами данных и хочет быстро освежать в памяти нужные SQL-конструкции. Здесь всё подано компактно и по делу: запросы, JOIN’ы, агрегации, подзапросы и др. Особенно удобно использовать для подготовки к собеседованиям.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍5🤝4
PostgreSQL умеет обновлять только реально изменившиеся строки и это может сильно сократить WAL и нагрузку!
Многие приложения делают
Даже если
Проверить это можно через системную статистику:
Чтобы избежать “пустых”
На highload-системах это может заметно уменьшить
➡️ SQL Ready | #совет
Многие приложения делают
UPDATE даже тогда, когда данные вообще не изменились.UPDATE users
SET name = 'Alex'
WHERE id = 1;
Даже если
name уже равен 'Alex', PostgreSQL всё равно создаст новую версию строки (MVCC), запишет WAL, обновит индексы и увеличит нагрузку на autovacuum.Проверить это можно через системную статистику:
SELECT n_tup_upd
FROM pg_stat_user_tables
WHERE relname = 'users';
Чтобы избежать “пустых”
UPDATE, можно сравнивать старые и новые значения прямо в WHERE:UPDATE users
SET
name = $1,
email = $2
WHERE id = $3
AND (name, email) IS DISTINCT FROM ($1, $2);
IS DISTINCT FROM безопасно сравнивает даже NULL значения, в отличие от обычного !=:SELECT
NULL = NULL,
NULL IS DISTINCT FROM NULL;
На highload-системах это может заметно уменьшить
WAL, bloat, количество HOT/non-HOT update и нагрузку на autovacuum без изменения архитектуры.Please open Telegram to view this post
VIEW IN TELEGRAM
👍16❤6🔥4🤝1
В этой статье:
• Разбирается, почему массивы в PostgreSQL — это отдельная модель хранения со своими ограничениями и компромиссами;
• Показываются скрытые проблемы массивов: потеря ссылочной целостности, особенности GIN-индексов, TOAST, MVCC и дорогостоящие обновления;
• Объясняется, как правильно работать с массивами, когда использовать JSONB, intarray, pgvector и в каких случаях массивы действительно оправданы.🔊 Продолжайте читать на Habr!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍8🤝5❤1
В этой шпаргалке собраны ключевые команды для создания, изменения и удаления пользователей, назначения и отзыва прав, а также проверки текущих ролей и сессий. Они применяются при управлении безопасностью базы данных, настройке доступа и аналитической работе с ролями.Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14❤8🤝5
This media is not supported in your browser
VIEW IN TELEGRAM
На сайте собрана большая база материалов по PostgreSQL: установка, настройка, SQL-запросы, работа с таблицами, индексами, функциями, транзакциями и администрированием базы данных. Материал подаётся последовательно. Отличный ресурс как для новичков, так и для разработчиков, которым нужен удобный справочник с примерами и практическими объяснениями.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🔥8❤4👎1🤝1
NOT VALID constraints — как добавить CHECK и FOREIGN KEY на huge таблицу без долгой блокировки?
Обычно добавление
На production-таблицах в сотни строк это может превратиться в очень долгую блокировку DDL.
Но в PostgreSQL есть фича —
Без полного сканирования таблицы, сразу начинает проверять все новые записи. При этом старые строки пока не валидируются.
Позже ограничение можно провалидировать отдельно:
Самое интересное —
То же самое работает и для
🔥 Это одна из самых полезных возможностей PostgreSQL для безопасных миграций, постепенного внедрения ограничений и наведения порядка в старых базах без длительного простоя.
➡️ SQL Ready | #совет
Обычно добавление
CHECK или FOREIGN KEY на большую таблицу рискованная операция, потому что PostgreSQL начинает сразу сканировать все старые данные.ALTER TABLE orders
ADD CONSTRAINT orders_user_fk
FOREIGN KEY (user_id)
REFERENCES users(id);
На production-таблицах в сотни строк это может превратиться в очень долгую блокировку DDL.
Но в PostgreSQL есть фича —
NOT VALID:ALTER TABLE orders
ADD CONSTRAINT orders_price_check
CHECK (price > 0)
NOT VALID;
Без полного сканирования таблицы, сразу начинает проверять все новые записи. При этом старые строки пока не валидируются.
Позже ограничение можно провалидировать отдельно:
ALTER TABLE orders
VALIDATE CONSTRAINT orders_price_check;
Самое интересное —
VALIDATE CONSTRAINT не блокирует обычный concurrent DML как классический ALTER TABLE.То же самое работает и для
FOREIGN KEY:ALTER TABLE orders
ADD CONSTRAINT orders_user_fk
FOREIGN KEY (user_id)
REFERENCES users(id)
NOT VALID;
Please open Telegram to view this post
VIEW IN TELEGRAM
❤13👍7🔥7
This media is not supported in your browser
VIEW IN TELEGRAM
Здесь подробно разбираются запросы, работа с данными, фильтрация, JOIN’ы, агрегации и другие конструкции, которые постоянно используются в разработке. Материал подаётся последовательно и на примерах, поэтому намного проще понять логику запросов и научиться писать их самостоятельно.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥17👍7🤝5❤1
Антиджойн в SQL — как находить отсутствующие связи!
Одна из самых частых задач в аналитике — найти строки, для которых не существует связанных данных. Например, пользователей без заказов или товары без продаж.
Таблицы:
Многие пытаются писать через
Но здесь есть проблема: если подзапрос вернёт хотя бы один
Причина — логика
Решением может служить антиджойн через
SQL проверяет отсутствие связанной строки и сразу останавливается при первом совпадении.
Ту же задачу можно решить через
Этот паттерн и называется антиджойн — верни строки, для которых связи не существует.
Особенно полезно это в проверках целостности данных:
Так можно быстро найти битые записи с отсутствующими
🔥 Антиджойны пригодятся в аналитике, ETL, аудитах данных и поиске проблемных связей между таблицами.
➡️ SQL Ready | #практика
Одна из самых частых задач в аналитике — найти строки, для которых не существует связанных данных. Например, пользователей без заказов или товары без продаж.
Таблицы:
users(id, email)
orders(id, user_id)
Многие пытаются писать через
NOT IN:id="x8d2qa"
SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM orders
);
Но здесь есть проблема: если подзапрос вернёт хотя бы один
NULL, результат может стать пустым.Причина — логика
NULL в SQL ломает сравнение NOT IN.Решением может служить антиджойн через
NOT EXISTS:id="m4z7pk"
SELECT *
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);
SQL проверяет отсутствие связанной строки и сразу останавливается при первом совпадении.
Ту же задачу можно решить через
LEFT JOIN:id="f1q9vc"
SELECT u.*
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
WHERE o.id IS NULL;
LEFT JOIN оставляет все строки users, а WHERE o.id IS NULL отбирает только те, где совпадений не нашлось.Этот паттерн и называется антиджойн — верни строки, для которых связи не существует.
Особенно полезно это в проверках целостности данных:
id="k6n2yb"
SELECT *
FROM orders o
WHERE NOT EXISTS (
SELECT 1
FROM users u
WHERE u.id = o.user_id
);
Так можно быстро найти битые записи с отсутствующими
foreign key.Please open Telegram to view this post
VIEW IN TELEGRAM
❤18👍9🔥7
Например, шардинг по диапазону распределяет данные по определённым диапазонам значений, а шардинг по хэшу помогает равномерно распределять нагрузку между серверами.
На картинке — основные стратегии шардинга и маршрутизации запросов, которые используются в распределённых базах данных и высоконагруженных системах.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤13👍6🤝5
PostgreSQL умеет пропускать заблокированные строки без ожидания!
Большинство знают
Если несколько воркеров одновременно обрабатывают огромную таблицу задач:
Без
PostgreSQL позволяет просто пропускать уже занятые записи:
Теперь каждый воркер мгновенно получает только свободные строки без ожидания и конфликтов.
Это можно встроить прямо в
Получается параллельная обработка на уровне PostgreSQL без внешних систем очередей.
🔥 Аналогично строят высоконагруженные фоновые обработчики, обработку писем, биллинг и массовые пакетные операции.
➡️ SQL Ready | #совет
Большинство знают
SKIP LOCKED только для очередей через SELECT ... FOR UPDATE. Но мало кто использует его для параллельной пакетной обработки внутри PostgreSQL.Если несколько воркеров одновременно обрабатывают огромную таблицу задач:
SELECT id
FROM jobs
WHERE processed = false
FOR UPDATE;
Без
SKIP LOCKED процессы начинают ждать друг друга даже при наличии свободных строк.PostgreSQL позволяет просто пропускать уже занятые записи:
SELECT id
FROM jobs
WHERE processed = false
FOR UPDATE SKIP LOCKED;
Теперь каждый воркер мгновенно получает только свободные строки без ожидания и конфликтов.
Это можно встроить прямо в
UPDATE:WITH cte AS (
SELECT id
FROM jobs
WHERE processed = false
LIMIT 100
FOR UPDATE SKIP LOCKED
)
UPDATE jobs
SET processed = true
WHERE id IN (SELECT id FROM cte);
Получается параллельная обработка на уровне PostgreSQL без внешних систем очередей.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥16❤6👍5
This media is not supported in your browser
VIEW IN TELEGRAM
На сайте собрано множество обучающих материал по SQL: от базовых запросов SELECT и WHERE до JOIN, подзапросов, функций, сортировки и работы с таблицами. Всё объясняется простым языком с примерами запросов и постепенным усложнением тем, поэтому материал подойдёт как новичкам, так и тем, кто хочет систематизировать знания по бд.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍5🤝3