EvolCode
треки/Базы данных·03 / 07
12 мин чтения

Индексы — почему один запрос быстрый, а другой кладёт сервер

Самая частая жалоба «сайт стал медленный» — это БД без правильных индексов. На разработке у тебя 50 строк, всё летает. На проде через год — 500 тысяч записей, и тот же запрос превратился в 5-секундный freeze. Хорошая новость: индекс добавляется одной строкой. Плохая — нужно понимать какой именно. Этот урок учит думать про индексы как взрослые.

Что такое индекс — на пальцах

Представь книгу на 1000 страниц без оглавления. Чтобы найти главу про драконов, читаешь подряд. Это full table scan — БД проверяет каждую строку. С оглавлением (индексом) сразу прыгаешь на нужную страницу. Внутри индекс — это отдельная структура (B-tree), отсортированная по выбранному полю, с указателями на строки в основной таблице. Когда ищешь WHERE email = "X" — БД идёт в индекс, бинарным поиском за log(n) находит строку, прыгает в таблицу. Из 10М строк — 23 сравнения вместо 10М.

Когда индекс точно нужен

Три железных случая. Первое: поле в WHERE которое часто фильтруется — userId, email, status, createdAt при сортировке. Второе: foreign key — Prisma не создаёт индекс на FK автоматически (в отличие от MySQL), а они нужны почти всегда для JOIN. Третье: уникальность — @unique автоматически создаёт индекс. Подсказка: если ты пишешь .findMany({where: {X}}) — поле X должно быть индексировано. Если .findUnique — Prisma требует @unique или @id.

Composite index — индекс на несколько полей

@@index([userId, createdAt]) — индекс на ДВЕ колонки в этом порядке. Используется когда ты часто фильтруешь по userId И сортируешь по createdAt. БД сначала находит userId в индексе, потом строки уже отсортированы по createdAt — JOIN или ORDER BY делается без отдельной сортировки. ВАЖНО: порядок полей в composite index имеет значение — индекс [a, b] помогает запросам WHERE a = ? и WHERE a = ? AND b = ?, но НЕ помогает WHERE b = ? (без условия на a).

EXPLAIN ANALYZE — как проверить что индекс работает

EXPLAIN ANALYZE SELECT — Postgres покажет план выполнения. Ищешь два слова: "Seq Scan" — БД пробежала всю таблицу (плохо для больших таблиц). "Index Scan" — использовала индекс (хорошо). Если ожидаешь индекс, а видишь Seq Scan — индекс есть но не используется. Причины: маленькая таблица (БД решила что full scan быстрее), функция в WHERE (WHERE LOWER(email) = ... не использует индекс на email), implicit type cast. На малых таблицах не парься. На таблицах от 10к строк — обязательно проверяй.

Цена индекса — почему не сделать индексы на всё

Индекс — это копия данных в отсортированной структуре. Каждый INSERT/UPDATE/DELETE кроме записи в таблицу обновляет ВСЕ индексы. На таблице с 10 индексами вставка в 10 раз медленнее. Плюс индексы занимают место — обычно 10-30% от размера таблицы каждый. Поэтому правило: индексируй только то, что часто запрашиваешь. Не «на всякий случай». Не индексируй поля с низкой селективностью (status: 3 значения — индекс почти бесполезен).

примерprisma
model Post {
  id        String   @id @default(cuid())
  userId    String
  title     String
  slug      String   @unique           // автоматический индекс
  status    String                      // НЕ индексируем — мало значений
  createdAt DateTime @default(now())
  user      User     @relation(fields: [userId], references: [id])

  // FK на userId — нужен индекс для быстрого JOIN
  @@index([userId])

  // Часто: «посты юзера за период» — composite индекс
  @@index([userId, createdAt])
}

// Применение в коде:
// ✅ Использует @@index([userId, createdAt])
const posts = await prisma.post.findMany({
  where: { userId: 'abc' },
  orderBy: { createdAt: 'desc' },
});

// ✅ Использует @unique slug
const post = await prisma.post.findUnique({ where: { slug } });

// ⚠️ НЕ использует индекс на slug:
const posts2 = await prisma.post.findMany({
  where: { slug: { contains: 'abc' } },  // LIKE — full scan
});

// EXPLAIN ANALYZE через сырой SQL — проверка что индекс работает
const plan = await prisma.$queryRaw`
  EXPLAIN ANALYZE
  SELECT * FROM "Post"
  WHERE "userId" = 'abc'
  ORDER BY "createdAt" DESC
  LIMIT 20
`;
// В выводе ищи "Index Scan using Post_userId_createdAt_idx"
// Если "Seq Scan" — индекс не используется

Когда какой индекс нужен — на примерах

главное
  • 01Индекс = оглавление книги. Без него БД читает все строки подряд (full table scan)
  • 02Индексируй: поля в WHERE, FK для JOIN, поля для ORDER BY на больших таблицах
  • 03@unique автоматически создаёт индекс. @id тоже. FK — нужно вручную через @@index
  • 04Composite index ([a, b]) работает для WHERE a = ? и WHERE a = ? AND b = ?, но НЕ для WHERE b = ?
  • 05Не индексируй поля с низкой селективностью (status из 3 значений) — польза мизерная, цена реальная
  • 06EXPLAIN ANALYZE покажет используется ли индекс. Ищи "Index Scan" vs "Seq Scan"
  • 07Каждый индекс замедляет INSERT/UPDATE/DELETE и занимает место — добавляй только нужные
проверь себя
01У тебя есть запрос `findMany({ where: { userId }, orderBy: { createdAt: "desc" }})` на таблице 1М строк — он медленный. Какой индекс добавить?
02Запрос `findMany({ where: { email: { contains: "@gmail" }}})`. Используется ли индекс на поле email?
03Зачем вообще ограничивать число индексов — почему не повесить @@index на все поля?
следующий урок: Где держать базу данных — Postgres на Neon