🔑 UUIDv4 тихо убивает твою базу
UUID удобен: генерируешь на бэке, ключи уникальны, и никто не подсмотрит число заказов, поменяв цифру в URL. Кажется, идеальный Primary Key.
Но обычный UUIDv4 — полностью случайный. А базе это очень не нравится.
⚙️ Почему так
Индексы живут в B-Tree, а дереву нужен порядок.
🔹 BigInt с автоинкрементом → новая запись дописывается в конец. Мгновенно.
🔹UUIDv4 → случайный ID лезет в середину дерева. База рвёт заполненную страницу пополам и перетасовывает данные. Это и есть Page Split.
📉 Что происходит в продакшене
Пока строк пара сотен тысяч — тишина.
На миллионах начинается боль:
🔹 тормозят INSERT — CPU занят не записью, а перебалансировкой
🔹 фрагментация — страницы полупустые, индекс пухнет в разы
🔹 вымывание кэша — раздутый индекс не лезет в RAM, и база уходит на диск
🚀 Решение — UUIDv7
Отказываться от UUID не нужно. Просто бери седьмую версию.
В начале строки — timestamp (время до миллисекунды), и только потом случайные биты.
🔹 Ключи всегда растут
🔹 Для базы это почти автоинкремент
🔹Записи ложатся в конец, фрагментация исчезает
Скорость вставок остаётся ровной даже на таблицах в десятки гигабайт.
💡 Стартуешь проект? Закладывай UUIDv7 сразу.
UUID удобен: генерируешь на бэке, ключи уникальны, и никто не подсмотрит число заказов, поменяв цифру в URL. Кажется, идеальный Primary Key.
Но обычный UUIDv4 — полностью случайный. А базе это очень не нравится.
⚙️ Почему так
Индексы живут в B-Tree, а дереву нужен порядок.
🔹 BigInt с автоинкрементом → новая запись дописывается в конец. Мгновенно.
🔹UUIDv4 → случайный ID лезет в середину дерева. База рвёт заполненную страницу пополам и перетасовывает данные. Это и есть Page Split.
📉 Что происходит в продакшене
Пока строк пара сотен тысяч — тишина.
На миллионах начинается боль:
🔹 тормозят INSERT — CPU занят не записью, а перебалансировкой
🔹 фрагментация — страницы полупустые, индекс пухнет в разы
🔹 вымывание кэша — раздутый индекс не лезет в RAM, и база уходит на диск
🚀 Решение — UUIDv7
Отказываться от UUID не нужно. Просто бери седьмую версию.
В начале строки — timestamp (время до миллисекунды), и только потом случайные биты.
🔹 Ключи всегда растут
🔹 Для базы это почти автоинкремент
🔹Записи ложатся в конец, фрагментация исчезает
Скорость вставок остаётся ровной даже на таблицах в десятки гигабайт.
💡 Стартуешь проект? Закладывай UUIDv7 сразу.
👍27🔥10👌3❤2
🚀 Поздний JOIN: как ускорить OFFSET на больших данных в 10 раз
Классическая пагинация через
📉 В чём проблема
🔹 Представь поиск 100-й страницы в толстой энциклопедии. Вместо оглавления ты читаешь весь текст с первой страницы, пока не дойдёшь до сотой.
🔹 Так же делает база при
🚀 Решение — Поздний JOIN (Deferred Join)
Сначала мы быстро находим нужные идентификаторы, а уже к ним присоединяем полные данные:
⚙️ Как это работает
🔸 Внутренний запрос работает как оглавление. Он бежит только по легкому индексу (цене и
🔸 Внешний
💡 Этот контринтуитивный трюк спасет твою пагинацию, когда таблица огромная, а отказаться от навигации по номерам страниц нельзя.
Классическая пагинация через
OFFSET сильно замедляет базу. Она читает тысячи тяжелых строк целиком только для того, чтобы их отбросить.📉 В чём проблема
🔹 Представь поиск 100-й страницы в толстой энциклопедии. Вместо оглавления ты читаешь весь текст с первой страницы, пока не дойдёшь до сотой.
🔹 Так же делает база при
OFFSET 100000: она собирает с диска все данные (длинные тексты, даты), отсчитывает ненужные сто тысяч строк и просто выбрасывает их. Это огромная трата ресурсов.🚀 Решение — Поздний JOIN (Deferred Join)
Сначала мы быстро находим нужные идентификаторы, а уже к ним присоединяем полные данные:
SELECT p.*
FROM (
SELECT id FROM products
ORDER BY price
LIMIT 50 OFFSET 100000
) AS sub
JOIN products p ON p.id = sub.id;
⚙️ Как это работает
🔸 Внутренний запрос работает как оглавление. Он бежит только по легкому индексу (цене и
id), мгновенно пропуская 100 тысяч записей, и выдает 50 нужных id.🔸 Внешний
JOIN обращается к самой таблице и загружает тяжелые данные только для этих 50 финальных строк.💡 Этот контринтуитивный трюк спасет твою пагинацию, когда таблица огромная, а отказаться от навигации по номерам страниц нельзя.
🔥33👍9❤8
🥷 Подстава от LIKE: почему 'user_1' находит чужие данные
Все знают, что знак процента (
⚙️ В чём ловушка
В SQL символ
Если ты попытаешься найти конкретного пользователя:
База радостно вернёт не только user_1, но и user-1, userA1 или user91. Подчёркивание сработает как джокер в колоде карт.
🚀 Как починить
Чтобы база искала именно сам символ подчёркивания, его нужно экранировать с помощью оператора
🔹 Мы сами выбираем символ для экранирования (здесь это
🔹 Теперь комбинация
💡 Всегда экранируй спецсимволы в LIKE, чтобы точный поиск не превращался в непредсказуемую лотерею.
Все знают, что знак процента (
%) в поиске заменяет любой кусок текста. Но многие забывают про скрытую угрозу — нижнее подчёркивание.⚙️ В чём ловушка
В SQL символ
_ (underscore) — это тоже спецсимвол. Он означает «ровно один любой знак». Если ты попытаешься найти конкретного пользователя:
SELECT * FROM users WHERE login LIKE 'user_1';
База радостно вернёт не только user_1, но и user-1, userA1 или user91. Подчёркивание сработает как джокер в колоде карт.
🚀 Как починить
Чтобы база искала именно сам символ подчёркивания, его нужно экранировать с помощью оператора
ESCAPE.SELECT * FROM users
WHERE login LIKE 'user!_1' ESCAPE '!';
🔹 Мы сами выбираем символ для экранирования (здесь это
!).🔹 Теперь комбинация
!_ воспринимается базой как обычный текст, а не спецсимвол поиска.💡 Всегда экранируй спецсимволы в LIKE, чтобы точный поиск не превращался в непредсказуемую лотерею.
👍47❤10🔥10🤯3
🧟♂️ Подстава Soft Delete: почему удалённый юзер ломает регистрацию
Ты внедрил мягкое удаление (
⚙️ В чём проблема
🔹 Обычно на колонке
🔹 Но для базы «удалённый» юзер всё ещё существует. Строка физически никуда не делась, поэтому уникальный индекс блокирует новую регистрацию с этим же адресом.
🚀 Идеальное решение
Можно усложнять код бэкенда, но лучше поручить эту задачу самой базе с помощью частичного индекса (Partial Index). Он будет следить за уникальностью только среди активных пользователей.
📈 Почему это круто
🔸 Вся логика защиты от дублей остаётся на уровне БД — никаких костылей в коде.
🔸 Индекс работает быстрее и занимает меньше места, так как просто игнорирует удалённые записи.
💡 Частичный индекс — самое изящное решение для Soft Delete, которое бережёт нервы и дисковое пространство.
Ты внедрил мягкое удаление (
is_deleted = true). Пользователь удаляет аккаунт, а через месяц решает вернуться с тем же email. И тут регистрация падает с ошибкой!⚙️ В чём проблема
🔹 Обычно на колонке
email висит уникальный индекс, чтобы в базе не было клонов.🔹 Но для базы «удалённый» юзер всё ещё существует. Строка физически никуда не делась, поэтому уникальный индекс блокирует новую регистрацию с этим же адресом.
🚀 Идеальное решение
Можно усложнять код бэкенда, но лучше поручить эту задачу самой базе с помощью частичного индекса (Partial Index). Он будет следить за уникальностью только среди активных пользователей.
CREATE UNIQUE INDEX active_users_email_idx
ON users (email)
WHERE is_deleted = false;
📈 Почему это круто
🔸 Вся логика защиты от дублей остаётся на уровне БД — никаких костылей в коде.
🔸 Индекс работает быстрее и занимает меньше места, так как просто игнорирует удалённые записи.
💡 Частичный индекс — самое изящное решение для Soft Delete, которое бережёт нервы и дисковое пространство.
🔥27❤8👍8
🕳️ Один NULL — и запрос пустой. Скрытая ловушка NOT IN
Это классические грабли и частая задача на собеседованиях. Ты пишешь обычный фильтр, ожидаешь сотню строк, а база возвращает абсолютный ноль.
⚙️ Почему так происходит
🔹 Оператор
🔹 В SQL любое сравнение с
🔹 Логика базы строгая:
📉 Как это выглядит в коде
Если в таблице
🚀 Надёжное решение
Используй
💡 Всегда выбирай
Это классические грабли и частая задача на собеседованиях. Ты пишешь обычный фильтр, ожидаешь сотню строк, а база возвращает абсолютный ноль.
⚙️ Почему так происходит
🔹 Оператор
NOT IN разворачивается базой в цепочку проверок: id != 1 AND id != 2 AND id != NULL.🔹 В SQL любое сравнение с
NULL даёт не False, а статус Unknown (неизвестно).🔹 Логика базы строгая:
True AND Unknown превращается в Unknown. Строка отбрасывается, так как условие не выполнилось на 100%.📉 Как это выглядит в коде
SELECT * FROM users
WHERE id NOT IN (
SELECT banned_id FROM ban_list
);
Если в таблице
ban_list затесался хотя бы один NULL, запрос ничего не вернёт. База не может гарантировать, что пользователь чист, если один из банов «неизвестен».🚀 Надёжное решение
Используй
NOT EXISTS. Этот оператор проверяет сам факт наличия связи, а не сравнивает значения напрямую. Пустые значения его не сломают:SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM ban_list b
WHERE b.banned_id = u.id
);
💡 Всегда выбирай
NOT EXISTS для фильтрации по подзапросам, чтобы не наступить на мину неявных пустых значений.👍22❤15🔥9🙏2🗿1
👻 Забытые кавычки: почему одно число ломает индекс
Ты ищешь пользователя по номеру телефона, пишешь простой запрос, а база внезапно задумывается на долгие секунды. Причина — всего лишь пропущенные кавычки.
⚙️ Как появляется ошибка
Обычно номера телефонов хранят в строковых колонках (
📉 Что происходит под капотом
🔹 База видит несовпадение типов: колонка — строка, а условие поиска — число.
🔹 Чтобы их сравнить, база решает неявно привести всю колонку к числу. По сути, выполняет скрытый
🔹 Любое преобразование данных в колонке моментально отключает её индекс.
🔹 Это как переписать всю телефонную книгу в другой формат ради поиска одного абонента. Запрос уходит в долгое и тяжелое сканирование всей таблицы от начала до конца (Full Table Scan).
🚀 Как правильно
🔸 Всегда передавай строковые значения в кавычках, даже если они состоят только из цифр:
💡 Неявное приведение типов — тихий убийца производительности, поэтому всегда следи за типами данных в условиях поиска.
Ты ищешь пользователя по номеру телефона, пишешь простой запрос, а база внезапно задумывается на долгие секунды. Причина — всего лишь пропущенные кавычки.
⚙️ Как появляется ошибка
Обычно номера телефонов хранят в строковых колонках (
VARCHAR). Но при поиске разработчик может передать число без кавычек:SELECT * FROM users WHERE phone = 79991234567;
📉 Что происходит под капотом
🔹 База видит несовпадение типов: колонка — строка, а условие поиска — число.
🔹 Чтобы их сравнить, база решает неявно привести всю колонку к числу. По сути, выполняет скрытый
CAST(phone AS BIGINT) для каждой строки.🔹 Любое преобразование данных в колонке моментально отключает её индекс.
🔹 Это как переписать всю телефонную книгу в другой формат ради поиска одного абонента. Запрос уходит в долгое и тяжелое сканирование всей таблицы от начала до конца (Full Table Scan).
🚀 Как правильно
🔸 Всегда передавай строковые значения в кавычках, даже если они состоят только из цифр:
SELECT * FROM users WHERE phone = '79991234567';
💡 Неявное приведение типов — тихий убийца производительности, поэтому всегда следи за типами данных в условиях поиска.
❤18👍10🔥4
🗑 Удалили миллион строк, а место на диске не вернулось
Ты радостно делаешь
⚙️ Почему база ничего не удаляет
🔹 В PostgreSQL и MySQL работает механизм MVCC (многоверсионность).
🔹 При операции
🔹 Это как вычеркнуть запись ручкой в блокноте: прочитать её уже нельзя, но физическое место на странице она всё ещё занимает.
📉 Зачем нужны «мёртвые зоны»
Ради безопасности и параллельной работы. Пока ты удаляешь записи, другой процесс может строить по ним отчёт. База заботливо хранит старую версию данных, пока все текущие запросы не завершатся.
🚀 Как реально освободить диск
Встроенные фоновые процессы очищают старые метки и отдают место внутри таблицы для новых строк, но не возвращают его операционной системе. Чтобы ужать файл и вернуть гигабайты диску, нужны команды:
🔹 В PostgreSQL:
🔹 В MySQL:
⚠️ Осторожно: эти команды полностью блокируют таблицу на всё время своей работы.
💡 Операция DELETE только прячет данные, а реальное место возвращают лишь тяжёлые команды полного сжатия.
Ты радостно делаешь
DELETE FROM logs, ждёшь освобождения диска, но ничего не происходит. Современные базы данных нас обманывают.⚙️ Почему база ничего не удаляет
🔹 В PostgreSQL и MySQL работает механизм MVCC (многоверсионность).
🔹 При операции
DELETE физического стирания нет. База просто ставит на строку невидимую метку «удалено».🔹 Это как вычеркнуть запись ручкой в блокноте: прочитать её уже нельзя, но физическое место на странице она всё ещё занимает.
📉 Зачем нужны «мёртвые зоны»
Ради безопасности и параллельной работы. Пока ты удаляешь записи, другой процесс может строить по ним отчёт. База заботливо хранит старую версию данных, пока все текущие запросы не завершатся.
🚀 Как реально освободить диск
Встроенные фоновые процессы очищают старые метки и отдают место внутри таблицы для новых строк, но не возвращают его операционной системе. Чтобы ужать файл и вернуть гигабайты диску, нужны команды:
🔹 В PostgreSQL:
VACUUM FULL logs;
🔹 В MySQL:
OPTIMIZE TABLE logs;
⚠️ Осторожно: эти команды полностью блокируют таблицу на всё время своей работы.
💡 Операция DELETE только прячет данные, а реальное место возвращают лишь тяжёлые команды полного сжатия.
👍13🤯7❤2🔥2
🤯 Парадокс 3VL: почему WHERE блокирует NULL, а CHECK — радостно пропускает?
Ты добавил условие
⚙️ Трехзначная логика (3VL)
В SQL кроме
🛡️ Двойные стандарты SQL
🔹 Фильтр WHERE — строгий охранник. Он возвращает строку, только если условие дало
🔸 Валидация CHECK — ленивый вахтер. Она отклоняет запись, только если условие дало
🚀 Решение проблемы
Одного
💡 Ограничения базы строго судят неверные данные, но пасуют перед неизвестностью.
Ты добавил условие
CHECK (salary > 0), чтобы в базу не попадали кривые зарплаты. Но вдруг обнаруживаешь там пустые значения. Как так вышло?⚙️ Трехзначная логика (3VL)
В SQL кроме
TRUE и FALSE есть состояние UNKNOWN (неизвестно). Если сравнить пустоту с нулем (NULL > 0), результат будет не ложь, а именно неизвестность. И механизмы базы реагируют на это по-разному.🛡️ Двойные стандарты SQL
🔹 Фильтр WHERE — строгий охранник. Он возвращает строку, только если условие дало
TRUE. Состояние UNKNOWN он воспринимает как FALSE и скрывает запись.🔸 Валидация CHECK — ленивый вахтер. Она отклоняет запись, только если условие дало
FALSE. Если результат UNKNOWN, ограничение пожимает плечами и пропускает значение в таблицу.CREATE TABLE emp (
salary INT CHECK (salary > 0)
);
-- Запишется без ошибок!
INSERT INTO emp (salary) VALUES (NULL);
🚀 Решение проблемы
Одного
CHECK бывает мало. Если поле не должно содержать пустоту, всегда страхуй его правилом NOT NULL.💡 Ограничения базы строго судят неверные данные, но пасуют перед неизвестностью.
👍22🔥4🤯3❤2
💣 Цепная реакция: почему ON DELETE CASCADE запрещают на проде
В туториалах эту фичу хвалят: удалил пользователя, а база сама зачистила его заказы. На практике это мина, которая может незаметно стереть половину данных.
⚠️ В чем главная опасность
🔹 Невидимая угроза. Одна неточность в запросе:
И каскад автоматически стирает связанные платежи, историю и профили.
🔹 Тишина в логах. Приложение даже не узнает, что база удалила еще сотни строк в других таблицах. Восстановить хронологию ошибки будет очень сложно.
🔹 Блокировки. Удаление одной строки тянет за собой скрытые проверки в десятках связанных таблиц, замедляя остальные операции.
🚀 Как делают правильно
🔸 Явное удаление. Сначала код приложения отдельными запросами удаляет зависимости, пишет подробные логи, и только потом удаляет самого пользователя.
🔸 Мягкое удаление (Soft Delete). Вместо
🔸 Защита от ошибки. Внешние ключи настраивают с
💡 Удобство автоматической зачистки не окупает риск потерять данные без следа — управляй удалением явно.
В туториалах эту фичу хвалят: удалил пользователя, а база сама зачистила его заказы. На практике это мина, которая может незаметно стереть половину данных.
⚠️ В чем главная опасность
🔹 Невидимая угроза. Одна неточность в запросе:
DELETE FROM users WHERE status = 'banned';
И каскад автоматически стирает связанные платежи, историю и профили.
🔹 Тишина в логах. Приложение даже не узнает, что база удалила еще сотни строк в других таблицах. Восстановить хронологию ошибки будет очень сложно.
🔹 Блокировки. Удаление одной строки тянет за собой скрытые проверки в десятках связанных таблиц, замедляя остальные операции.
🚀 Как делают правильно
🔸 Явное удаление. Сначала код приложения отдельными запросами удаляет зависимости, пишет подробные логи, и только потом удаляет самого пользователя.
🔸 Мягкое удаление (Soft Delete). Вместо
DELETE строке просто меняют статус на удаленную: is_deleted = true.🔸 Защита от ошибки. Внешние ключи настраивают с
ON DELETE RESTRICT. База физически не даст удалить родительскую строку, пока существуют дочерние.💡 Удобство автоматической зачистки не окупает риск потерять данные без следа — управляй удалением явно.
👍8❤5👌2
🧮 Математика с подвохом: почему сложение обнуляет зарплату, а SUM() — нет
Считаешь итоговую выплату сотруднику как
⚠️ Ловушка прямого сложения
🔹
🔹 Любая математическая операция с неизвестностью заражает итог. Если оклад 1000, а бонус
🛡 Почему SUM() ведёт себя иначе
🔸 Агрегатные функции (
🔸 Если сделать
🚀 Как спасти строчные вычисления
Используй
💡 При прямом сложении колонок всегда страхуй необязательные поля нулём, чтобы пустота не съела твои данные.
Считаешь итоговую выплату сотруднику как
salary + bonus? Если премии нет (в базе лежит NULL), математика сыграет злую шутку, и человек останется вообще без денег в итоговом отчёте. ⚠️ Ловушка прямого сложения
🔹
NULL — это не ноль, а полная «неизвестность». 🔹 Любая математическая операция с неизвестностью заражает итог. Если оклад 1000, а бонус
NULL, выражение 1000 + NULL вернёт пустоту (NULL). 🛡 Почему SUM() ведёт себя иначе
🔸 Агрегатные функции (
SUM(), AVG()) созданы для работы с группами строк и спроектированы так, чтобы игнорировать пустые ячейки.🔸 Если сделать
SUM(bonus) для значений 100, 200 и NULL, функция просто перешагнёт через пустоту и спокойно выдаст 300.🚀 Как спасти строчные вычисления
Используй
COALESCE — функцию, которая перехватит NULL и заменит его на запасной вариант.SELECT
salary + COALESCE(bonus, 0) AS total_pay
FROM employees;
💡 При прямом сложении колонок всегда страхуй необязательные поля нулём, чтобы пустота не съела твои данные.
👍29❤4🤯4🔥1