Индексы и транзакции в базе данных
Как устроен индекс на B-дереве, когда он ускоряет и когда мешает, свойства ACID, уровни изоляции транзакций, аномалии чтения, блокировки и взаимоблокировки в СУБД.
Индексы и транзакции — две темы, без которых практическая часть курсовой по базам данных выглядит поверхностной. Первая отвечает за скорость, вторая за целостность данных.
Зачем нужен индекс
Без индекса СУБД читает всю таблицу целиком — полное сканирование со сложностью O(n). Индекс на B-дереве позволяет найти нужную строку за O(log n): в таблице на миллион строк это порядка двадцати сравнений вместо миллиона.
CREATE INDEX idx_employees_dept ON employees(dept_id);
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- Составной индекс: порядок столбцов имеет значение
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date);
-- Частичный индекс: только активные записи
CREATE INDEX idx_active_users ON users(last_login) WHERE is_active = true;
-- Посмотреть, используется ли индекс
EXPLAIN ANALYZE SELECT * FROM employees WHERE dept_id = 5;
| Тип индекса | Устройство | Для чего |
|---|---|---|
| B-дерево | Сбалансированное дерево | По умолчанию: =, <, >, BETWEEN, ORDER BY |
| Хеш | Хеш-таблица | Только строгое равенство, зато быстрее |
| GiST / GIN | Обобщённые деревья | Полнотекстовый поиск, массивы, JSON, геоданные |
| Битовая карта | Битовые маски | Столбцы с малым числом значений в аналитике |
| Кластерный | Физический порядок хранения | Один на таблицу, обычно первичный ключ |
Цена индекса
- Занимает место на диске — иногда сопоставимо с самой таблицей.
- Замедляет INSERT, UPDATE и DELETE: индекс нужно перестраивать при каждой записи.
- На маленькой таблице бесполезен: полное сканирование сотни строк быстрее обхода дерева.
- На столбце с малым числом различных значений (пол, статус) обычный индекс почти не помогает.
- Не используется, если столбец обёрнут функцией: WHERE UPPER(name) = 'ИВАНОВ' — нужен индекс по выражению.
Практическое правило: индексируйте внешние ключи, столбцы в WHERE и JOIN, поля сортировки. Не индексируйте всё подряд — на таблице с интенсивной записью лишние индексы обходятся дороже, чем экономят.
Транзакции и ACID
BEGIN;
UPDATE accounts SET balance = balance - 5000 WHERE id = 1;
UPDATE accounts SET balance = balance + 5000 WHERE id = 2;
COMMIT;
-- При ошибке между операциями:
ROLLBACK; -- деньги не исчезнут со счёта отправителя
| Свойство | Расшифровка | Что гарантирует |
|---|---|---|
| Atomicity | Атомарность | Либо все операции, либо ни одной |
| Consistency | Согласованность | БД переходит из одного корректного состояния в другое |
| Isolation | Изолированность | Параллельные транзакции не мешают друг другу |
| Durability | Долговечность | После COMMIT данные переживут отключение питания |
Аномалии параллельного доступа
| Аномалия | Что происходит |
|---|---|
| Грязное чтение | Прочитаны данные незавершённой транзакции, которая потом откатилась |
| Неповторяющееся чтение | Повторный SELECT той же строки вернул другое значение |
| Фантомное чтение | Повторный SELECT по условию вернул новые строки |
| Потерянное обновление | Две транзакции записали в одно поле, одна перезаписала другую |
Уровни изоляции
| Уровень | Грязное | Неповторяющееся | Фантомы |
|---|---|---|---|
| READ UNCOMMITTED | Возможно | Возможно | Возможны |
| READ COMMITTED | Нет | Возможно | Возможны |
| REPEATABLE READ | Нет | Нет | Возможны |
| SERIALIZABLE | Нет | Нет | Нет |
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- ... расчёты ...
SELECT balance FROM accounts WHERE id = 1; -- гарантированно то же значение
COMMIT;
Блокировки
- Разделяемая (S) — на чтение, несколько транзакций могут держать её одновременно.
- Исключительная (X) — на запись, несовместима ни с какой другой.
- Гранулярность: строка, страница, таблица. Чем мельче, тем выше параллелизм и больше накладных расходов на учёт.
- Оптимистичная блокировка через версионирование: проверяют номер версии при записи и повторяют операцию при конфликте.
-- Явная блокировка строки до конца транзакции
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- Оптимистичный вариант без блокировки
UPDATE accounts
SET balance = 900, version = version + 1
WHERE id = 1 AND version = 5;
-- Изменено 0 строк — значит кто-то опередил, повторяем операцию
Взаимоблокировки в СУБД
Две транзакции блокируют строки в разном порядке и ждут друг друга. Современные СУБД обнаруживают такой цикл автоматически и снимают одну транзакцию с ошибкой deadlock detected — приложение должно её повторить.
Профилактика та же, что в операционных системах: единый порядок обращения к строкам, короткие транзакции, отсутствие пользовательского ввода внутри транзакции.
Что показать в курсовой
- Замерить время запроса до и после создания индекса на таблице хотя бы в сто тысяч строк.
- Приложить вывод EXPLAIN ANALYZE с планом Seq Scan и Index Scan — наглядное доказательство эффекта.
- Продемонстрировать откат транзакции при нарушении ограничения целостности.
- Смоделировать аномалию на низком уровне изоляции и показать, как её устраняет повышение уровня.
Частые вопросы
Почему созданный индекс не используется?
Причин несколько: таблица слишком мала и планировщик выбрал сканирование, столбец обёрнут функцией, нарушено правило левого префикса составного индекса, либо не обновлена статистика — помогает ANALYZE.
Индекс на первичном ключе создавать вручную?
Нет, СУБД создаёт его автоматически при объявлении PRIMARY KEY или UNIQUE. Дублирующий индекс только займёт место.
Что произойдёт при сбое питания в середине транзакции?
При перезапуске СУБД по журналу упреждающей записи откатит незавершённые транзакции и накатит подтверждённые. Именно журнал обеспечивает атомарность и долговечность.