← Все курсы · Курс SQL · Все руководства

Когда индексировать таблицу: Практическое руководство для аналитиков

Прочитайте план, добавьте индекс, измерьте разницу · Часть Analyst Prep Kit

К , аналитик данных · Опубликовано

К концу этой страницы вы сможете определить, читает ли запрос всю вашу таблицу или сразу переходит к тому, что ему нужно, добавить индекс, который меняет то, что из этого происходит, узнать три распространенных способа, которыми запрос выбрасывает уже имеющийся индекс, и сказать, сколько стоят индексы, чтобы вы могли честно аргументировать свою позицию. Это около двадцати пяти минут, и каждый приведенный ниже тайминг измерялся на полумиллионе строк.

Вот что делать сегодня с запросом, который выполняется слишком медленно. Помещать EXPLAIN QUERY PLAN перед ним или EXPLAIN на Постгрес и большинстве других. Если в ответе есть слово сканировать в отношении большой таблицы движок считывает каждую строку, и это факт, который стоит учитывать, прежде чем что-либо менять.

Краткая версия: индекс — это отсортированная копия одного или нескольких столбцов, хранящихся рядом с таблицей, поэтому движок может перейти к значению, а не проходить мимо каждой строки в поисках его.

Идея состоит в том, чтобы идти против прыжков, поэтому она дает представление о картине.

Walk every row, or jump straight to it Two copies of the same table stand side by side, each drawn as a tall stack of twelve thin horizontal bars in no particular order, with one bar near the bottom of each stack picked out in a darker shade as the row being looked for. On the left, a single blue line enters at the top of the stack and zigzags down through every single bar in turn before reaching the highlighted one, touching all twelve. On the right, a narrow second strip stands beside the stack, its segments graded evenly from short to long to show that it is a sorted copy of one column. An amber line goes straight into one segment of that sorted strip, without passing any others, and then a short horizontal arrow crosses from that segment to the highlighted bar in the stack. Both routes end at the same row; one of them visits eleven rows it did not want and the other visits none.
Требуется та же таблица, та же строка. Слева механизм считывает каждую строку, пока не найдет совпадение. Справа он попадает прямо в отсортированную копию столбца, попадает на запись и следует за ней к строке.
Что вы узнаете
  1. Что такое индекс на самом деле
  2. Чтение плана: сканирование по поиску
  3. Измерение
  4. Сколько стоит индекс
  5. Три способа потерять имеющийся у вас индекс
  6. Составные индексы и самое левое правило
  7. Селективность: тот же индекс, с разницей в 130 раз.
  8. Что индексировать, а что нет
  9. Полное до и после
  10. Пограничные случаи, которые стоит знать
  11. Почему это работает
  12. Использование этого в вашем собственном проекте
  13. Все на одном экране
Каждое число на этой странице было измерено. Таблица заказов на 500 000 строк в SQLite, каждый запрос выполняется пять раз, сообщается о самом быстром выполнении, а планы запросов считываются из механизма, а не угадываются. SQLite используется, потому что в его планах указано SCAN или SEARCH словами; Постгрес называет одни и те же две вещи последовательным сканированием и сканированием индекса, и здесь все переносится.

1. Что такое индекс на самом деле

Перед пояснением: в вашей таблице 500 000 строк, и вы запрашиваете одного клиента. Скажите, сколько строк должен просмотреть движок.

Все, если только что-то не подскажет, где остановиться. Таблица представляет собой кучу строк, расположенных в произвольном порядке, поэтому единственный способ убедиться, что вы нашли все совпадения, — это проверить каждую строку. Это полное сканирование таблицы, и это не ошибка; это единственное правильное решение без дополнительной информации.

Индекс — это дополнительная информация. Это отдельная структура, содержащая значения одного или нескольких столбцов, хранящиеся в отсортированном порядке, с указателем от каждого значения обратно на соответствующую строку. Поскольку оно отсортировано, движок может найти значение так же, как вы находите слово в словаре: не читая каждую страницу, а многократно отбрасывая ту половину, которая не может его содержать.

Сразу следуют два следствия, которые объясняют большую часть этой страницы. Индекс полезен только для охватываемых им столбцов, поэтому индекс для customer_id ничего не делает для запроса о region. И это помогает только в том случае, если запрос задает значение таким образом, чтобы отсортированный порядок мог ответить, поэтому существует пятый раздел.

2. Чтение плана: сканирование с помощью поиска

Вам никогда не придется угадывать, что из этого происходит. Любой движок подскажет вам это, а в SQLite это одно слово перед запросом.

EXPLAIN QUERY PLAN
SELECT COUNT(*), SUM(units*unit_price) FROM orders WHERE customer_id = 41234;

SCAN orders

SCAN означает каждую строку. После добавления индекса в этот столбец тот же запрос сообщает:

SEARCH orders USING INDEX idx_customer (customer_id=?)

SEARCH означает, что он прошел через индекс. Имя индекса находится прямо здесь, и таким образом вы подтверждаете, что тот, который вы добавили, используется, а не другой.

Третья фраза появляется постоянно и заслуживает внимания: COVERING INDEX. Это означает, что каждый столбец, необходимый запросу, находился внутри индекса, поэтому движку вообще не приходилось возвращаться к таблице. Это самый быстрый случай, и именно поэтому индекс, включающий столбец, который вы считаете, может превзойти индекс, включающий только столбец, по которому вы фильтруете.

В Постгрес те же три идеи читаются как Seq Scan, Index Scan и Index Only Scan. Разные слова, одно и то же различие и EXPLAIN ANALYZE там также указано время.

3. Измерение

Вот один и тот же запрос до и после одного индекса, на 500 000 строк, лучший из пяти прогонов.

SELECT COUNT(*), SUM(units*unit_price) FROM orders WHERE customer_id = 41234;

no index    SCAN orders                                        90.9 ms
index       SEARCH orders USING INDEX idx_customer (id=?)       0.2 ms

Результат идентичен: девять строк в сумме составляют 8415 в обоих случаях. Изменился только маршрут. В этом вся суть индекса и причина, по которой люди обращаются к нему в первую очередь.

Диапазон работает таким же образом. Фильтрация месяца из года по датам:

WHERE order_date >= '2026-03-01' AND order_date < '2026-04-01'

no index    SCAN orders                                       123.5 ms
index       SEARCH orders USING COVERING INDEX idx_date        3.9 ms

В любом случае 42 470 строк. Обратите внимание, что этот индекс вернулся в качестве покрывающего индекса: запросу требовалась только дата, а дата находится в индексе, поэтому таблица не была затронута.

Скажите вслух, почему анализ диапазона выполняется медленнее, чем поиск точки, даже с индексом. Он возвращает 42 470 совпадений, а не девять, и индекс быстро приводит вас к началу совпадений, но движку все равно приходится их проходить. Индекс устраняет затраты на поиск строк, а не затраты на чтение тех, которые вы запросили.

4. Сколько стоит индекс

Индексы не бесплатны, и знание цены — это то, что позволяет вам попросить их, не отмахиваясь.

Время строить. Создание индекса на 500 000 строк заняло 572 мс. Для ста миллионов строк это считанные минуты, а на некоторых движках таблица блокируется во время работы.

Диск. Размер файла базы данных составлял 25,1 МБ с первым индексом. Каждый индекс представляет собой вторую копию своих столбцов плюс указатели, поэтому таблица с шестью индексами может быть в несколько раз больше ее собственного размера.

Пишет. Это то, что люди недооценивают. Каждая вставка, обновление и удаление должны поддерживать каждый индекс таблицы. Измерено на той же машине со вставкой 200 000 строк:

no indexes        449 ms
three indexes    3124 ms      7.0x slower

Семь раз по трем индексам. Это сделка в одном числе: чтение становится быстрее, а запись медленнее, и что имеет значение, полностью зависит от того, для чего предназначена таблица. Таблица отчетов, загружаемая один раз за ночь, может содержать множество индексов. Таблица, выполняющая запись для каждого действия пользователя, не может.

Здесь также объясняется совет, который звучит странно, пока вы не увидите это: при массовой загрузке большой таблицы отбросьте индексы, загрузите их, а затем перестройте. Построить индекс один раз по готовым данным гораздо дешевле, чем поддерживать его для каждой строки.

5. Три способа потерять имеющийся у вас индекс

Перед пояснением: указатель на customer_id существует, и фильтры запросов на customer_id. Скажите, гарантирует ли это, что он будет использоваться.

Это не так, и все три показателя были измерены на основе таблицы, в которой существовал индекс.

Функция или арифметика в столбце.

WHERE customer_id = 41234        SEARCH ... (customer_id=?)     0.2 ms
WHERE customer_id + 0 = 41234    SCAN                          57.9 ms

Индекс сортируется по customer_id, а не по customer_id + 0. С точки зрения движка это другое выражение, и у него нет его отсортированной копии, поэтому он вычисляет его для каждой строки. Версия с датой хуже:

WHERE order_date >= '2026-03-01' AND order_date < '2026-04-01'    3.9 ms
WHERE strftime('%Y-%m', order_date) = '2026-03'                 488.8 ms

Те же 42 470 строк, в 125 раз медленнее и медленнее, чем было при отсутствии индекса вообще, потому что теперь он выполняет полный проход. и вызов функции полмиллиона раз. Это конкретная причина рекомендаций в руководство по датам для сравнения необработанного столбца с диапазоном, а не для его переноса.

Ведущий подстановочный знак. LIKE 'Nor%' в принципе можно использовать отсортированный индекс, потому что все, что начинается с этих трех букв, находится вместе. LIKE '%orth' не может ни в одном движке, потому что отсортированный список не дает отправной точки для неизвестного префикса. На используемом здесь движке ни одна форма не использовала индекс, и обе сканировались примерно за 80–96 мс, в то время как простое равенство в том же столбце выполнялось как ПОИСК за 16,0 мс. Правило, которое следует принять: равенство или диапазон префиксов удобны для индексирования, а сопоставление текста в середине значения — это другая проблема, требующая другого типа индекса.

Несоответствие типов. Сравнение индексированного числа со строкой в ​​кавычках или индексированной строки с числом заставляет движок конвертировать одну сторону в строке, что является замаскированной функцией для столбца. Если запрос неожиданно сканируется, прежде чем проверять что-либо еще, убедитесь, что обе стороны сравнения имеют один и тот же тип.

6. Составные индексы и самое левое правило

Индекс может охватывать несколько столбцов, и порядок их перечисления определяет, для чего его можно использовать. Один индекс на (region, product), три запроса, все измеренные:

WHERE region = 'North'                         SEARCH (region=?)              16.1 ms
WHERE region = 'North' AND product = 'Desk'    SEARCH (region=? AND product=?)  4.7 ms
WHERE product = 'Desk'                         SCAN                            77.1 ms

Первые два используют его, а третий нет. Причина в том, что означает сортировка: индекс сортируется сначала по регионам, а по продуктам только внутри каждого региона. Спросите регион, и вы знаете, с чего начать. Запросите товар без региона, и соответствующие записи разбросаны по всему индексу, поэтому переходить не к чему.

Это правило крайнего левого префикса, и это самая полезная вещь, которую нужно знать о составных индексах. Индекс на (a, b, c) обслуживает запросы по a, на a and bи дальше a, b and c. Он ничего не делает для запроса b один.

Это дает практическое правило упорядочивания: первым ставьте столбец, по которому вы всегда фильтруете, а вторым — тот, который вы иногда добавляете. Если вам действительно нужны оба направления, то это два индекса, и теперь вы вернулись в четвертый раздел, решая, стоит ли затрат на запись того.

7. Селективность: тот же индекс, с разницей в 130 раз.

До пояснения: один индекс, один столбец, два запроса, отличающиеся только количеством в них. Предскажите, займут ли они одинаковое время.

WHERE customer_id > 100        499,157 rows match      26.1 ms
WHERE customer_id > 59990           74 rows match       0.2 ms

Оба использовали индекс. Один из них в 130 раз медленнее другого, и ничего в запросе или индексе не изменилось, кроме того, какая часть таблицы совпала.

У этого свойства есть имя, избирательность: какую часть таблицы сохраняет условие. Индекс имеет смысл иметь, когда условие является выборочным, и мало что дает, если это не так, поскольку возврат большей части таблицы означает чтение большей части таблицы, как бы вы туда ни попали. Тот же эффект проявляется в столбце только с двумя значениями:

WHERE is_returned = 0     490,000 rows      45.2 ms
WHERE is_returned = 1      10,000 rows       0.9 ms

Идентичный индекс в одном и том же столбце отлично подходит для редких значений и почти бессмысленен для обычных. Вот почему «индексировать каждый столбец» — плохой совет: индекс для флага, который составляет 98% от одного значения, оплачивает полную стоимость записи и почти ничего не приносит от запроса, который люди фактически выполняют.

Именно поэтому движки ведут статистику распределения значений и почему запрос, который был быстрым в прошлом месяце, сегодня может быть медленным, и его никто не меняет: данные изменили форму, и оптимизатор теперь делает другой выбор.

8. Что индексировать, а что нет

Краткий список в том порядке, в котором я бы его проработал.

Проиндексируйте этоПочему
Первичные ключиУже сделано за вас в большинстве двигателей.
Внешние ключи, столбцы, к которым вы присоединяетесьЧасто это самый крупный выигрыш, поскольку соединение без него представляет собой сканирование за поиск.
Столбцы в WHERE на больших столахОсобенно даты, когда вы фильтруете период из истории.
Столбцы, по которым вы сортируете, для получения важных результатовСортированный индекс может предоставлять порядок без этапа сортировки.
Не беспокоитьПочему
Маленькие столикиЧтение нескольких тысяч строк — это уже быстро. В любом случае движок может игнорировать индекс.
Столбцы с низкой кардинальностиРаздел седьмой. Флаг с двумя значениями мало зарабатывает и стоит столько же.
Столбцы, которые пишутся постоянноКаждое обновление также поддерживает индекс.
Каждая колонна, обороняясьВ семь раз медленнее пишет и файл гораздо большего размера, для индексов ничего не использует.

Еще один совет, который важнее любого из вышеперечисленных: как аналитик вы часто не можете создать индекс, потому что у вас есть доступ на чтение к чужой базе данных. Это не тупик. Вывод плана по-прежнему доступен вам, и «этот запрос сканирует сто миллионов строк, потому что в нем нет индекса». order_date« — это конкретный, практически осуществимый запрос, который владелец базы данных может выполнить за минуту. Расплывчатые запросы на ускорение ничего не дают; результат плана и измеренное время получают индекс.

И когда ответ на самом деле отрицательный, оставшиеся ходы — те, которые нужно сделать. Обработка больших наборов данных: фильтровать ранее, агрегировать в базе данных, выполнять выборку во время разработки и материализовать результат один раз, вместо повторного запроса.

Полное до и после

Тот же вопрос: что из полумиллиона заказов купил один покупатель?

До

SELECT COUNT(*), SUM(units*unit_price)
FROM orders WHERE customer_id = 41234;

plan : SCAN orders
time : 90.9 ms

Правильно, и он прочитал все 500 000 строк и вернул девять из них. На информационной панели, которая запускает это один раз для каждого клиента при загрузке страницы, это вся проблема с производительностью, и ничто в запросе не выглядит неправильным.

после

CREATE INDEX idx_customer ON orders(customer_id);     -- 572 ms, once

SELECT COUNT(*), SUM(units*unit_price)
FROM orders WHERE customer_id = 41234;

plan : SEARCH orders USING INDEX idx_customer (customer_id=?)
time : 0.2 ms

Те же девять рядов, те же 8415. Один оператор, выполняется один раз, и его стоимость составляет 25 МБ файла, полсекунды времени сборки и более медленная запись в таблицу, которая в любом случае загружается пакетно.

Претензия, и это повод прочитать план, прежде чем что-либо оптимизировать: запрос не изменился, данные не изменились, и тот же результат изменился с 90,9 мс до 0,2 мс, потому что движок перестал читать строки, которые он никогда не собирался возвращать.

Пограничные случаи, которые стоит знать

Шесть, которые появятся.

Оптимизатор намеренно игнорирует ваш индекс. Если он считает, что большая часть таблицы совпадает, сканирование действительно обходится дешевле, чем прыгать по нему, и оно будет сканировать. Обычно это правильно, а если и неверно, то это потому, что статистика устарела.

Устаревшая статистика. Механизмы выбирают планы из сводок данных, которые периодически обновляются. После большой нагрузки запустите команду обновления движка, ANALYZE как на SQLite, так и на Постгрес, или план может быть основан на таблице, которая больше не существует в этой форме.

Индексы в представлении. Простое представление не содержит данных и не может быть проиндексировано. Вы индексируете базовые таблицы или вам нужно материализованное представление, которое представляет собой реальный сохраненный результат.

НУЛИ. Сохраняются ли нули в индексе и есть ли IS NULL возможность его использования зависит от двигателя. Если вы часто фильтруете пропущенные значения, проверьте это, а не предполагайте.

Индекс, который дублирует другой. Индекс на (a) является избыточным, когда индекс на (a, b) уже существует по крайнему левому правилу. Это чистая стоимость записи. Найдите их, прежде чем добавлять больше.

Присоединяется. Неиндексированный ключ соединения — самая дорогая вещь в большинстве медленных запросов, поскольку поиск происходит один раз для каждой строки другой таблицы. Если соединение происходит медленно, сначала индексируйте ключ на большей стороне; механика того, что делает соединение, находится в руководство по объединениям.

Почему это работает

Причина, по которой поиск может сократиться с девяноста миллисекунд до долей одной, заключается в том, что эти два маршрута имеют разные кривые роста, а не разные константы. Сканирование пропорционально количеству строк: удвойте таблицу, и вы удвоите время. Поиск в отсортированной структуре увеличивается с логарифмом размера, поскольку на каждом шаге отбрасывается большая часть того, что осталось. Структурой, которая сделала это практичным для данных на диске, является B-дерево, предназначенное для поддержания сбалансированного и мелкого большого упорядоченного индекса, так что любое значение может быть достигнуто за несколько чтений страниц, даже по мере роста файла (Байер & МакКрайт, 1972 г., Закон об информатике, 1(3), 173–189). Почти каждый индекс, который вы встретите, является его разновидностью, и именно из-за его формы выигрыш становится больше по мере того, как таблица становится больше, а не меньше.

Вторая половина страницы посвящена тому, что наличие индекса — это не то же самое, что его использование. База данных решает для каждого запроса, какой маршрут дешевле, оценивая, сколько строк затронет каждый вариант, и оценивая их стоимость; именно эта оценка объясняет, почему один и тот же индекс выбирается для одного значения и игнорируется для другого. Схема принятия такого решения, включая идею оценки избирательности условия и выбора пути доступа на его основе, была заложена рано и до сих пор является формой того, что делают оптимизаторы (Селинджер, Астрахань, Чемберлен, Лори & Цена, 1979, Материалы Международной конференции ACM SIGMOD 1979 г. по управлению данными., 23–34). 130-кратное отличие седьмого раздела заключается в том, что механизмы работают правильно, а сбои в пятом разделе заключаются в том, что они работают правильно с выражением, которое вы не хотели писать.

Одно замечание в шпаргалке ниже. Он создан для того, чтобы его просматривали и вспоминали, а не перечитывали, поскольку практическое тестирование опережает повторное изучение очень большого количества сравнений, в том числе проводимых на реальных курсах, а не в лабораториях (Адесоп, Тревизан & Сундарараджан, 2017, Обзор образовательных исследований, 87(3), 659–701).

Использование этого в вашем собственном проекте

Индексировать все хуже, чем ничего не индексировать. Вместо этого сделайте это по порядку.

  1. Найдите запрос, который действительно медленный, скорее по времени, чем по ощущениям. Обычно большая часть проблемы заключается в одном запросе.
  2. Прочитайте его план. Если нет сканирования большой таблицы, проблема не в индексации, и вы бы потратили день не на то.
  3. Убедитесь, что запрос не уничтожает уже имеющийся у него индекс.: функция в столбце, несоответствие типа, ведущий подстановочный знак.
  4. Сначала индексируйте ключи соединения, то столбцы в WHERE на самых больших столах.
  5. Измерьте до и после, лучший из нескольких прогонов, и запишите оба числа. Эта пара — единственный аргумент в пользу сохранения индекса.
  6. Если вы не можете создавать индексы, пришлите план и два тайминга тому, кто может. Конкретный запрос выполняется; медленный отчет – нет.

Если у вас под рукой есть бумага, один дополнительный эскиз стоит пяти минут. Напишите свой медленный запрос и обведите каждый столбец, который появляется в WHERE, в JOIN ... ONи в ORDER BY. Эти круги представляют собой весь список кандидатов на индекс в указанном порядке приоритета, и большинство людей считают, что их только два или три.

Подробнее об этом и еще что-нибудь подобное. Все инструкции находятся в одном месте на индекс направляющих. Если вы предпочитаете вводить запросы, а не читать о них, SQL-практикум дает вам по одному выполняемому запросу к реальной базе данных.

Все на одном экране

Это лист поиска. Закройте правую колонку, проработайте левую и произнесите каждый ответ вслух, прежде чем проверять его.

вещьЧто он делает
ИндексОтсортированная копия некоторых столбцов с указателями на строки.
Нет индексаПолное сканирование таблицы. Правильно и пропорционально количеству строк.
Посмотрите, что происходитEXPLAIN QUERY PLAN, или EXPLAIN на большинстве двигателей.
SCANКаждый ряд. В Постгрес это называется Последовательное сканирование.
SEARCHЗашёл через индекс. Постгрес называет это сканированием индекса.
COVERING INDEXВсе необходимое было в индексе. Таблица ни разу не читалась.
Размеренная победаОт 90,9 до 0,2 мс для 500 000 строк, результат тот же.
Стоимость сборкиЗдесь 572 мс. Минуты на очень большом столе.
Стоимость записиТри индекса сделали 200 000 вставок в 7 раз медленнее.
Массовая загрузкаОтбросьте индексы, загрузите, перестройте.
Функция на столбцеТеряет индекс. strftime(date) был в 125 раз медленнее диапазона.
Ведущий подстановочный знакНевозможно использовать отсортированный индекс ни в одном движке.
Несоответствие типаПреобразование на строку. Скрытая функция.
Составной индексСортируется по первому столбцу, затем внутри него по второму.
правило крайнего левого префикса(a,b) обслуживает а, а а с б. Не б один.
избирательностьКакую часть таблицы сохраняет условие. Редкие ценности побеждают.
Тот же индекс, два значения490 000 совпадений 45,2 мс, 10 000 совпадений 0,9 мс.
Индексировать сначалаОбъедините ключи, а затем столбцы WHERE в больших таблицах.
Не индексироватьМаленькие таблицы, флаги с двумя значениями, столбцы с большим количеством текста.
Единственная привычка, которую следует сохранить. Прежде чем что-либо менять, прочтите план и измерьте до и после. Индекс, добавленный интуитивно, представляет собой постоянную стоимость записи, уплачиваемую за неизмеримую выгоду, а чтение плана занимает десять секунд. Если запрос медленный, что не описано на этой странице, существует общая шлейф диагностики на предмет застревания.

Еще одна мысль, и мне бы искренне хотелось услышать ответы других людей. Тот, который я помню, был ночным отчетом, который за год увеличился с четырех минут до пятидесяти, где исправлением был единственный индекс в столбце даты, и причина, по которой никто его не нашел, заключалась в том, что все предполагали, что запрос просто получил больше данных для обработки. Что оказалось однострочным исправлением чего-то медленного, которое вы унаследовали?

Ссылки

Запрос занял четыре минуты, и никто не может вам сказать, на что они были потрачены.

SQL для аналитиков составляет 458 страниц, индексы объяснены со стороны аналитика, так что вы можете запросить нужную и узнать, почему.

SQL для аналитиков, 19 $ →
Практикуйтесь в чтении планов по вопросам, на которые вы можете рассчитывать время.

тот Курс SQL дает вам повторения: планы интерпретации, запросы, которые превосходят собственные индексы, и составные индексы, о которых нужно рассуждать, а также карточки с условиями, на которые может ответить отсортированный индекс. Теперь вы владеете методом; комплект - это практика. Ничего устанавливать, учетная запись не требуется.

Откройте набор SQL →