БД без боли: моя шпаргалка для собесов в Java. Часть 4
Всем привет!
Я Senior Java Developer в банке, и за эти годы мне довелось пройти немало собеседований. Где-то было спокойно, где-то — как в допросной, с каверзными вопросами, странными задачами и вечным «а почему так, а не иначе?». В процессе я собрал целую коллекцию тем, которые всплывают снова и снова, особенно когда дело касается баз данных.
Сегодня хочу поделиться этим опытом и разобрать вопросы, которые чаще всего задают именно по SQL-базам.
В профиле уже есть другие части для подготовки:
❗❗Дисклеймер❗❗
Эта статья не является учебником по технологиям. Здесь я не буду углубляться в то, как всё работает под капотом или почему это устроено именно так. Это сжатая методичка по вопросам на собеседованиях — только факты, без лишней воды!
Транзакции
Транзакция — это набор операций с базой, который выполняется полностью или совсем не выполняется.
ACID
Атомарсность - никакая транзакция не будет зафиксирована в системе частично
Согласованность - успешная транзакция всегда приводит данные в корректное, допустимое состояние
Изолированность - параллельные транзакции не мешают друг другу и работают так, будто выполняются по очереди
Прочность - если транзакция закоммитилась, её данные гарантированно сохранены и не исчезнут
Уровни изоляции
Read uncommitted
Видит зафиксированные и не зафиксированные данные транзакций.
PostgreSQL фактически не поддерживает “грязное чтение” (dirty read).
Любые SELECT видят только зафиксированные данные, даже если указан Read Uncommitted. Это официально: в PostgreSQL Read Uncommitted ведёт себя как Read Committed.
Read committed
каждая операция внутри транзакции видит только те данные, которые были зафиксированы к моменту выполнения конкретного запроса, а не к началу транзакции
Repeatable read
В начале транзакции PostgreSQL делает снимок состояния БД (snapshot).
Все последующие запросы этой транзакции читают одну и ту же консистентную картину, независимо от того, что успели закоммитить другие транзакции.
Поэтому non-repeatable reads отсутствуют — одна и та же выборка всегда возвращает одинаковые данные в рамках транзакции.
реализация Repeatable Read в PostgreSQL не допускает фантомных чтений
Serializable
Она гарантирует, что даже при конкурентном выполнении транзакций результат останется таким же, как и в случае их последовательного (по одной за раз) выполнения, без всякой конкурентности.
Читающие транзакции не мешают друг другу — ты можешь спокойно читать данные, пока кто-то другой их меняет.
Параллельные транзакции записи тоже не блокируют друг друга, если они работают с разными объектами.
Блокировка возникает только тогда, когда две транзакции пытаются изменить один и тот же объект одновременно.
Вот очень полезная табличка из официальной доки Postgres
Индексы
Индекс - это вспомогательная структура, которая хранится вместе с таблицей и помогает быстро находить нужные записи, ускоряя выборку данных.
Важно отметить, что индексы могут также замедлять добавление, удаление и изменение данных, так как при этом операции также требуется обновление соответствующих индексов
Хеш-индексы
Хранилища типа ключ — значение работают как словари в языках программирования. Обычно реализуются через хеш-таблицу, где каждому ключу соответствует адрес значения в файле данных.
При добавлении или обновлении пары «ключ — значение» хеш-таблица обновляется, чтобы хранить правильный адрес. А чтобы прочитать значение, система просто смотрит адрес в хеш-таблице и обращается к файлу — быстро и эффективно.
SS-таблицы и LSM-деревья
SS-таблица (SSTable) — это файл, где ключи отсортированы и каждый встречается только один раз. Такая структура облегчает слияние сегментов даже больших файлов, не загружая всю таблицу в память, и работает по принципу сортировки слиянием.
Чтобы найти нужный ключ, не обязательно держать все индексы в памяти: достаточно знать соседние ключи и просматривать файл между ними до нужного значения.
B-деревья
B-дерево — самая распространённая индексная структура. Как и SSTable, оно хранит пары «ключ — значение» в отсортированном виде, что позволяет быстро искать ключи и выполнять диапазонные запросы.
Но в отличие от SSTable, B-дерево делит данные на страницы фиксированного размера (обычно 4 КБ), которые читаются и записываются по одной. Каждая страница хранит ключи и ссылки на дочерние страницы, формируя дерево. Поиск всегда начинается с корневой страницы и проходит через страницы, отвечающие за диапазоны ключей, пока не найдётся нужное значение.
Разница между индексами (user_id, status) и (user_id) INCLUDE (status)
Составной индекс (user_id, status)
1. Структура: Оба поля являются частью ключа индекса
2. Сортировка: Данные сортируются сначала по user_id, затем по status
3. Использование: Эффективен для -
o Запросов с условиями на user_id
o Запросов с условиями на user_id И status
o Сортировок по этим полям
Индекс с INCLUDE (user_id) INCLUDE (status)
1. Структура: Только user_idв ключе индекса, status хранится в листьях индекса
2. Сортировка: Данные сортируются только по user_id
3. Использование: Эффективен для:
o Запросов с условиями только на user_id
o Запросов, где нужны оба поля (покрывающий индекс)
o Не поддерживает поиск/сортировку по status отдельно
Ключевые различия
1. Размер: INCLUDE-индекс обычно компактнее
2. Гибкость: Составной индекс поддерживает больше типов запросов
3. Производительность: Для запросов с фильтрацией по обоим полям составной индекс работает лучше
4. Поддержка: INCLUDE синтаксис доступен не во всех СУБД (появился в PostgreSQL 11, SQL Server и др.)
Алгоритм вешанья индексов
Это чистой мой алгоритм, я не говорю, что он САМЫЙ ВЕРНЫЙ и нет других)
Алгоритм вешанья индексов
1) Определить цель индексирования: нужно понять, для каких операций или запросов индекс будет использоваться. Например, это может быть поиск по определенному полю, сортировка или слияние данных.
2) Оценить затраты и выгоды: провести анализ текущей структуры базы данных и определить, какие данные и операции будут выигрывать от индексирования. Оценить затраты на создание и поддержку индекса.
3) Анализ запросов: изучить типичные запросы, которые будут выполняться в базе данных, и выяснить, какие поля или комбинации полей чаще всего используются в условиях WHERE или ORDER BY. Это поможет определить, какие поля следует индексировать.
4) Избегать избыточности: создание индексов для каждого поля не всегда является эффективным решением. Определить комбинации полей, которые часто используются в запросах, и создать составные индексы для них.
5) Определить порядок сортировки: для поля, по которому будет выполняться сортировка или группировка, определить порядок и создать индекс соответствующего типа.
6) Определить уникальность: если поле должно содержать только уникальные значения, создать уникальный индекс для этого поля.
7) Обновление и поддержка индекса: учесть, что каждая операция добавления, обновления или удаления данных влияет на индексирование. Обновление индексов может занимать время и ресурсы, поэтому важно оптимизировать этот процесс.
8) Мониторинг и оптимизация: регулярно производить мониторинг производительности базы данных и запросов. Если производительность снижается, можно рассмотреть возможность создания или изменения индексов для улучшения выполнения запросов.
Репликация
Репликация — это копирование данных с одной базы на другие.
Зачем: для отказоустойчивости и высокой доступности.
Виды: синхронная (данные пишутся одновременно на все реплики) и асинхронная (реплики обновляются с задержкой).
Реплика может использоваться для чтения, но запись идёт в мастер-базу.
Шардирование
Шардирование — это горизонтальное деление данных между разными серверами.
Каждая шард содержит часть данных (например, по диапазону ключей или хешу).
Позволяет масштабировать систему: нагрузка на запись и чтение распределяется между несколькими инстансами.
Секционирование
Секционирование — это деление таблицы внутри одной базы на логические части (секции).
Например, по дате: январь, февраль, март.
Упрощает поиск и может ускорять некоторые запросы, но все данные остаются в одной БД, в отличие от шардирования.
SQL vs NoSQL
SQL (реляционные БД)
Структура: таблицы с фиксированными колонками и строками.
Язык: SQL, строгие схемы (schema).
Гарантии: ACID, транзакции.
Когда применять: сложные связи между данными, строгие требования к консистентности, аналитика, банки, ERP.
NoSQL (нереляционные БД)
Структура: документы, ключ‑значение, графы или колоночные хранилища.
Гибкие схемы, горизонтальное масштабирование проще.
Когда применять: большие объёмы данных, высокая нагрузка на запись, быстрый поиск по ключу, распределённые системы, кэш, логи, IoT.
Когда можно использовать денормализацию бд
Денормализация — это сознательное объединение данных, которые обычно хранятся в разных таблицах, чтобы уменьшить количество соединений (JOIN) и ускорить запросы.
Используют, когда:
Часто выполняются сложные запросы с большим количеством JOIN, и это становится узким местом по производительности.
Нужна оптимизация чтения в системах с высокой нагрузкой на SELECT.
Можно пожертвовать скоростью записи ради ускорения чтения (так как обновление денормализованных данных сложнее).
VIEW
Это виртуальная таблица, которая хранит только запрос, а не данные.
Данные берутся из базовых таблиц каждый раз при обращении.
Плюс: удобство, безопасность, можно скрыть сложные JOIN’ы.
Минус: запросы могут быть медленнее, если базовые таблицы большие.
Materialized VIEW
Это настоящая таблица, которая хранит результат запроса.
Нельзя вставлять, обновлять или удалять строки напрямую — изменения происходят только через
REFRESH MATERIALIZED VIEW
.Плюс: быстрый доступ к данным, не нужно пересчитывать результат при каждом запросе.
Минус: нужно периодически обновлять, чтобы данные оставались актуальными
Итог
Сегодня мы рассмотрели ключевые аспекты работы с базами данных: структуру таблиц, индексы, транзакции, запросы. Все это часто встречается на собеседованиях по SQL и работе с СУБД. Список тем составлен на основе моего опыта и опыта коллег, проходивших собеседования на позиции от Junior до Senior.
Всем спасибо за внимание, удачных собесов и хорошего дня!)