← Все курсы · Курс SQL · Все руководства
Когда индексировать таблицу: Практическое руководство для аналитиков
К концу этой страницы вы сможете определить, читает ли запрос всю вашу таблицу или сразу переходит к тому, что ему нужно, добавить индекс, который меняет то, что из этого происходит, узнать три распространенных способа, которыми запрос выбрасывает уже имеющийся индекс, и сказать, сколько стоят индексы, чтобы вы могли честно аргументировать свою позицию. Это около двадцати пяти минут, и каждый приведенный ниже тайминг измерялся на полумиллионе строк.
Вот что делать сегодня с запросом, который выполняется слишком медленно. Помещать EXPLAIN QUERY PLAN перед ним или EXPLAIN на Постгрес и большинстве других. Если в ответе есть слово сканировать в отношении большой таблицы движок считывает каждую строку, и это факт, который стоит учитывать, прежде чем что-либо менять.
Краткая версия: индекс — это отсортированная копия одного или нескольких столбцов, хранящихся рядом с таблицей, поэтому движок может перейти к значению, а не проходить мимо каждой строки в поисках его.
Идея состоит в том, чтобы идти против прыжков, поэтому она дает представление о картине.
- Что такое индекс на самом деле
- Чтение плана: сканирование по поиску
- Измерение
- Сколько стоит индекс
- Три способа потерять имеющийся у вас индекс
- Составные индексы и самое левое правило
- Селективность: тот же индекс, с разницей в 130 раз.
- Что индексировать, а что нет
- Полное до и после
- Пограничные случаи, которые стоит знать
- Почему это работает
- Использование этого в вашем собственном проекте
- Все на одном экране
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).
Использование этого в вашем собственном проекте
Индексировать все хуже, чем ничего не индексировать. Вместо этого сделайте это по порядку.
- Найдите запрос, который действительно медленный, скорее по времени, чем по ощущениям. Обычно большая часть проблемы заключается в одном запросе.
- Прочитайте его план. Если нет сканирования большой таблицы, проблема не в индексации, и вы бы потратили день не на то.
- Убедитесь, что запрос не уничтожает уже имеющийся у него индекс.: функция в столбце, несоответствие типа, ведущий подстановочный знак.
- Сначала индексируйте ключи соединения, то столбцы в
WHEREна самых больших столах. - Измерьте до и после, лучший из нескольких прогонов, и запишите оба числа. Эта пара — единственный аргумент в пользу сохранения индекса.
- Если вы не можете создавать индексы, пришлите план и два тайминга тому, кто может. Конкретный запрос выполняется; медленный отчет – нет.
Если у вас под рукой есть бумага, один дополнительный эскиз стоит пяти минут. Напишите свой медленный запрос и обведите каждый столбец, который появляется в WHERE, в JOIN ... ONи в ORDER BY. Эти круги представляют собой весь список кандидатов на индекс в указанном порядке приоритета, и большинство людей считают, что их только два или три.
Все на одном экране
Это лист поиска. Закройте правую колонку, проработайте левую и произнесите каждый ответ вслух, прежде чем проверять его.
| вещь | Что он делает |
|---|---|
| Индекс | Отсортированная копия некоторых столбцов с указателями на строки. |
| Нет индекса | Полное сканирование таблицы. Правильно и пропорционально количеству строк. |
| Посмотрите, что происходит | 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 в больших таблицах. |
| Не индексировать | Маленькие таблицы, флаги с двумя значениями, столбцы с большим количеством текста. |
Еще одна мысль, и мне бы искренне хотелось услышать ответы других людей. Тот, который я помню, был ночным отчетом, который за год увеличился с четырех минут до пятидесяти, где исправлением был единственный индекс в столбце даты, и причина, по которой никто его не нашел, заключалась в том, что все предполагали, что запрос просто получил больше данных для обработки. Что оказалось однострочным исправлением чего-то медленного, которое вы унаследовали?
Ссылки
- Байер Р. и МакКрайт Э. (1972). Организация и ведение крупных упорядоченных индексов. Закон об информатике, 1(3), 173–189.
- Селинджер П.Г., Астрахан М.М., Чемберлин Д.Д., Лори Р.А. и Прайс Т.Г. (1979). Выбор пути доступа в системе управления реляционными базами данных. Материалы Международной конференции ACM SIGMOD 1979 г. по управлению данными., 23–34.
- Адесоп О.О., Тревизан Д.А. и Сундарараджан Н. (2017). Переосмысление использования тестов: метаанализ практического тестирования. Обзор образовательных исследований, 87(3), 659–701.
SQL для аналитиков составляет 458 страниц, индексы объяснены со стороны аналитика, так что вы можете запросить нужную и узнать, почему.
SQL для аналитиков, 19 $ →тот Курс SQL дает вам повторения: планы интерпретации, запросы, которые превосходят собственные индексы, и составные индексы, о которых нужно рассуждать, а также карточки с условиями, на которые может ответить отсортированный индекс. Теперь вы владеете методом; комплект - это практика. Ничего устанавливать, учетная запись не требуется.
Откройте набор SQL →