Создание и удаление индексов в production без простоя.
В production нельзя останавливать базу на часы. CONCURRENTLY позволяет создавать и удалять индексы без блокировки записи.
CREATE INDEX idx_users_email ON users(email);Что происходит:
Время создания индекса для 100 млн записей: 30 минут
В это время:
✅ SELECT — работают
❌ INSERT — ждут 30 минут
❌ UPDATE — ждут 30 минут
❌ DELETE — ждут 30 минут
Результат: простой приложения, таймауты, недовольные пользователи.
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);| Операция | CREATE INDEX | CREATE INDEX CONCURRENTLY |
|---|---|---|
| SELECT | ✅ Работает | ✅ Работает |
| INSERT | ❌ Блокируется | ✅ Работает |
| UPDATE | ❌ Блокируется | ✅ Работает |
| DELETE | ❌ Блокируется | ✅ Работает |
┌─────────────────────────────────────────────────┐
│ PASS 1: Сканирование таблицы │
│ - Строится основная структура индекса │
│ - Запись не блокируется │
│ - Новые данные отслеживаются │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ PASS 2: Обработка новых данных │
│ - Короткая блокировка ShareUpdateExclusiveLock │
│ - Применяются изменения с Pass 1 │
│ - Индекс становится видимым │
└─────────────────────────────────────────────────┘
CREATE INDEX:
CREATE INDEX CONCURRENTLY:
CREATE INDEX CONCURRENTLY index_name ON table_name (column);-- Таблица с 50 млн записей
CREATE TABLE orders (
id BIGSERIAL,
user_id BIGINT,
status VARCHAR(20),
created_at TIMESTAMP
);
-- Создание индекса без блокировок
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders(user_id);-- Уникальный индекс
CREATE UNIQUE INDEX CONCURRENTLY idx_users_email ON users(email);
-- Частичный индекс
CREATE INDEX CONCURRENTLY idx_pending_orders ON orders(user_id)
WHERE status = 'pending';
-- Составной индекс
CREATE INDEX CONCURRENTLY idx_orders_user_status ON orders(user_id, status);
-- Покрывающий индекс
CREATE INDEX CONCURRENTLY idx_orders_covering ON orders(user_id)
INCLUDE (status, created_at);DROP INDEX idx_users_email;Блокировка: ShareLock — блокирует запись.
DROP INDEX CONCURRENTLY idx_users_email;Блокировка: ShareUpdateExclusiveLock — не блокирует запись.
-- Неиспользуемый индекс
DROP INDEX CONCURRENTLY idx_unused_column;-- ❌ Ошибка
BEGIN;
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
COMMIT;
-- ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction blockПочему: CONCURRENTLY требует нескольких проходов и ожидания завершения транзакций.
-- Транзакция 1 (долгая)
BEGIN;
SELECT * FROM users WHERE id = 1;
-- ... держит транзакцию 10 минут ...
COMMIT;
-- Транзакция 2 (создание индекса)
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- Ждёт завершения Транзакции 1 перед Pass 2CREATE INDEX: 30 минут
CREATE INDEX CONCURRENTLY: 45 минут
Почему: два прохода + ожидание транзакций + накладные расходы.
-- PostgreSQL 12+
SELECT
pid,
query,
state,
now() - query_start as duration
FROM pg_stat_activity
WHERE query LIKE 'CREATE INDEX CONCURRENTLY%';SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;-- Проверка валидности индекса
SELECT
indexname,
pg_get_indexdef(indexrelid) as definition,
pg_relation_size(indexrelid) as size
FROM pg_indexes
WHERE tablename = 'users';
-- Индекс в процессе создания будет невалидным
SELECT
indexrelid::regclass as index_name,
indisvalid as is_valid,
indisready as is_ready
FROM pg_index
WHERE indexrelid = 'idx_users_email'::regclass;-- Отмена через Ctrl+C в psql или pg_cancel_backend()
SELECT pg_cancel_backend(pid)
FROM pg_stat_activity
WHERE query LIKE 'CREATE INDEX CONCURRENTLY%';Что происходит:
invalidDROP INDEX CONCURRENTLY idx_name;-- 1. Удалить невалидный индекс
DROP INDEX CONCURRENTLY IF EXISTS idx_users_email;
-- 2. Создать заново
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);-- ❌ Ошибка
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- ERROR: relation "idx_users_email" already exists
-- ✅ Решение
DROP INDEX CONCURRENTLY IF EXISTS idx_users_email;
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);-- ✅ Хорошо для production
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- ❌ Плохо для production (блокирует запись)
CREATE INDEX idx_users_email ON users(email);# Cron job на 3 AM
0 3 * * * psql -c "CREATE INDEX CONCURRENTLY ..."-- Отмена после 1 часа
SET statement_timeout = '1h';
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
RESET statement_timeout;-- Проверка длительных операций
SELECT
pid,
now() - query_start as duration,
query
FROM pg_stat_activity
WHERE state = 'active'
AND query LIKE '%INDEX%'
ORDER BY duration DESC;-- Дождитесь завершения старых транзакций
SELECT pg_current_xact_id();
-- Создавайте индекс после завершения
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);#!/bin/bash
# create_index_concurrently.sh
DB_NAME="mydb"
INDEX_NAME="idx_users_email"
TABLE_NAME="users"
COLUMN_NAME="email"
# Проверка существования индекса
if psql -d $DB_NAME -c "\di $INDEX_NAME" | grep -q $INDEX_NAME; then
echo "Index already exists"
exit 1
fi
# Создание индекса
echo "Creating index concurrently..."
psql -d $DB_NAME -c "CREATE INDEX CONCURRENTLY $INDEX_NAME ON $TABLE_NAME ($COLUMN_NAME);"
if [ $? -eq 0 ]; then
echo "Index created successfully"
else
echo "Index creation failed"
exit 1
fifrom alembic import op
def upgrade():
# Создание индекса CONCURRENTLY
op.create_index(
'idx_users_email',
'users',
['email'],
postgresql_concurrently=True
)
def downgrade():
# Удаление индекса CONCURRENTLY
op.drop_index(
'idx_users_email',
table_name='users',
postgresql_concurrently=True
)from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.AddIndex(
model_name='user',
index=models.Index(
fields=['email'],
name='idx_users_email',
),
),
]Django автоматически использует CONCURRENTLY для PostgreSQL.
-- 9 AM, база под нагрузкой
-- ❌ Не делайте так
CREATE INDEX idx_orders_created_at ON orders(created_at);
-- Блокировка на 30 минут, простой приложения
-- ✅ Делайте так
CREATE INDEX CONCURRENTLY idx_orders_created_at ON orders(created_at);
-- Работает 45 минут, но без простоя-- Найти неиспользуемые индексы
SELECT
indexrelname,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
-- Удалить без блокировок
DROP INDEX CONCURRENTLY idx_unused_1;
DROP INDEX CONCURRENTLY idx_unused_2;-- Обычное перестроение (блокирует)
REINDEX INDEX idx_users_email;
-- Перестроение без блокировок (PostgreSQL 12+)
REINDEX INDEX CONCURRENTLY idx_users_email;| Операция | CONCURRENTLY | Без CONCURRENTLY |
|---|---|---|
| CREATE INDEX | ✅ Не блокирует запись | ❌ Блокирует запись |
| DROP INDEX | ✅ Не блокирует запись | ❌ Блокирует запись |
| REINDEX INDEX | ✅ Не блокирует запись (12+) | ❌ Блокирует запись |
| CREATE INDEX UNIQUE | ✅ Не блокирует запись | ❌ Блокирует запись |
| ALTER INDEX | ⚠️ Зависит от операции | ⚠️ Зависит от операции |
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.