Развитие происходит по спирали: когда-то люди не умели правильно индексировать, потом (в основном) научились, потом пришли noSQL и все снова забыли знание древних. Что вы будете делать, когда последние из старых DBA отплывут в Валинор?

Снова и снова и сталкиваюсь с полным набором антипаттернов индексирования. Я их перечислю, но! Для каждого антипаттерна есть исключения, когда именно это и стоит делать. Поэтому кликбейтно сформулированное правило верно лишь в 95% случаях, но если вы хотите копнуть глубже, то прочитайте про исключения.

И в конце полезные скрипты для MSSQL, Postgres и MySQL - хотя статья фокусируется на опыте MS SQL

Все автоматически созданные индексы - глупость

Речь идет об индексах созданных по рекомендации MSSQL, с именами типа _dta_index_mytab_11_1172915250__K1_K26_8_11_28_46_90

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

Подобные рекомендации - подсказка, что неплохо бы разобраться, а что плохо с самим запросом. Как правило INCLUDE - это отчаянная попытка MS SQL уменьшить объем данных для Full Scan, 'вынеся' эти данные из основной таблицы в более маленький индекс с INCLUDE. Чем создавать такой индекс, посмотрите, а откуда вообще идут table scans.

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

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

Таблица с более чем 15 индексами - что-то здесь не так

Я видел таблицы с 40 и более индексами, созданными по принципу 'кашу маслом не испортишь'. Большинство их было создано автоматически.

Конечно, один индекс не сильно замедлит insert/update/delete, но десятки замедлят существенно, особенно на больших таблицах.

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

Индекс по колонке с низкой селективностью бесполезен

Часто видишь, как индексируют поля типа isCancelled bit, ObjectType smallint, sex int (всего 56, или сколько там сейчас значений), DepartmentId в фирме с одним Department итд. Здесь много 'но' и все они важны:

Исключение 1. Иногда колонка с низкой селективностью может быть 'прицеплена' к колонке с высокой. Путь у объекта есть id int, и side char(1) 'плечо' правое и левое, 'L','R'. если 'плечо' всегда и часто указывается в запросах:

... WHERE id=12345 and side='L'

то вполне поможет индекс по (id,side) - но не (side,id) если side иногда не указывается.

Исключение 2. Иногда статус объекта несбалансирован, пусть у нас есть поле IsProcessed. У большинства объектов IsProcessed=1, а нам надо часто искать объекты для процессинга по isProcessed=0. Тогда имеет смысл создать индекс:

create index ... on ... (IsProcessed) WHERE (IsProcessed=0)

У нас будет маленький быстрый индекс по необработанным документам. Искать по нему надо так: IsProcessed=0 (а не IsProcessed=@status, где @status=0)

Исключение 3. Селективность колонки мала, но селективность набора многих колонок высока. Если все эти колонки используются в WHERE, то такой индекс будет полезен.

А как же FOREIGN KEY? пусть у нас есть Departments, которые сделаны для общности, и на обозримое время их 1 штука? Ведь рекомендуется все FOREIGN KEYs 'обеспечивать' индексами?

Как и всякая рекомендация она имеет границы. Если у вас таблица с количеством записей под миллиард, то зачем бедной базе при каждом insert менять этот бессмысленный индекс об одном значении? Как часто он нужен? Как часто вы удаляете departments? Пусть там раз в год будет table scan.

Нарушает какие-то принципы дизайна? Да, как и любая денормализация для performance.

Индекс по более чем 3 колонкам - красный флаг

Это следствие из предыдущего пункта. Если вы посмотрите на этот индекс то обнаружите, что либо он создан автоматически, либо лишь некоторые колонки достаточно селективны. (см. Исключения 1 и 3 в предыдущем пункте).

Кстати, селективность колонок можно проверить в MSSQL, Postgres, MySQL вот так.

Если в колонке дата/время не на последнем месте, то проверьте

Речь идет об индексах типа (DT datetime + SomeUseful ...), то есть колонка datetime не одинока, но она не на последнем месте.

Datetime часто ищутся не по равенству, а по диапазонам. В запросе:

... WHERE DT>='20211201' and DT<'20211205' and SomethingUseful=12345

данный индекс может быть использован лишь частично - только его первая часть, по DT. Проверка на SomethingUseful производится отдельно. В обратном порядке (SomethingUseful + DT) образуется непрерывный диапазон (12345,'20211201') ... (12345,'29211205') и индекс используется целиком

Исключение: у даты 'обрубается' время и она сравнивается на равенство.

Индексы не должны 'включаться' друг в друга

Если есть индекс по (A,B,C), то индекс по (A,B) излишен. Если указано WHERE A=... and B=... SQL может использовать начало индекса по (A,B,C)

Исключение: тем не менее, индекс по (A,B) короче и SQL будет его предпочитать, если условие по колонке C не указано. То же верно и для индексов с одинаковым началом и разными списками INCLUDE (здесь важно, от каких lookups 'спасает' INCLUDE)

Полезные скрипты

Для MS SQL предлагаю скрипт, который пройдет по всем индексам и представит отчет:

Показываются лишь колонки, которые участвуют хотя бы в одном индексе (вне INCLUDE), а число показывает порядковый номер в индексе. Чем старшее позиция, тем более блеклый синий цвет.

Вас наверное удивляет, что результат выглядит как HTML таблица? Потому что это часть большого проекта. Впрочем, для MSSQL вы можете использовать скрипт отдельно.

Скрипт для MSSQL, измените имя таблицы во второй строке
set nocount on 
declare @name varchar(128)='tablename'
select I.name as iname,IC.index_column_id, C.name as colname
  into #i
  from sys.index_columns IC, sys.indexes I, sys.columns C
  where IC.object_id=object_id(@name) and I.object_id=IC.object_id and I.index_id=IC.index_id and C.object_id=I.object_id and C.column_id=IC.column_id
declare @sql varchar(8000)='create table ##p (iname varchar(128) collate database_default', @c varchar(128), @blanks varchar(4000)= ''

-- create table
DECLARE servers CURSOR FOR select name from sys.columns where object_id=object_id(@name) and name in (select colname from #i) order by column_id
OPEN servers;  
FETCH NEXT FROM servers into @c  
WHILE @@FETCH_STATUS = 0  
BEGIN  
  set @sql=@sql+' , ['+@c+'] varchar(2)'
  set @blanks=@blanks+','''''
  FETCH NEXT FROM servers into @c
END  
CLOSE servers;  
DEALLOCATE servers;
set @sql=@sql+')'
exec(@sql)

--select * from #i
set @sql='insert into ##p select distinct iname'+@blanks+' from #i '
exec(@sql)

-- create table
DECLARE servers CURSOR FOR select name from sys.columns where object_id=object_id(@name) and name in (select colname from #i) order by column_id
OPEN servers;  
FETCH NEXT FROM servers into @c  
WHILE @@FETCH_STATUS = 0  
BEGIN  
  set @sql='update ##p set ['+@c+']=convert(varchar,index_column_id) from #i where #i.colname='''+@c+''' and #i.iname=##p.iname'
  print @sql
  exec(@sql)
  FETCH NEXT FROM servers into @c
END  
CLOSE servers;  
DEALLOCATE servers;

select * from ##p
--select iname,count(*) from #i group by iname order by 2
drop table #i
drop table ##p

Для Postgre и MySQL есть версии в самих модулях, скачайте их по ссылкам и ищите файл с именем *indexing. , равно как и для MSSQL

Комментарии (14)


  1. horevivan
    31.01.2022 14:35
    +1

    В последнее время, всё чаще, использую для индексирования хранимые вью (MS SQL). Из плюсов наглядность, агрегации и группировки. Буду признателен если, кто поделиться минусами данного подхода.


    1. Tzimie Автор
      31.01.2022 14:44
      +1

      1. Многочисленные ограничения на indexed view

      2. Невозможность alter table, которые там используются, из-за schemabinding. Конечно, view можно пересоздать, но если записей очень много, это долго


      1. horevivan
        31.01.2022 15:12
        +1

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

        Вот сейчас вспомнил неприятный момент с хранимыми вью - нужно обязательно приписывать with(noexpand) при выборке из них. Вью может не подхватиться, хотя указана явно, и раскрыться в запрос.


        1. Tzimie Автор
          31.01.2022 15:48
          +2

          А это ограничение standard edition


      1. za2li
        01.02.2022 23:41

        ещё индексы на вьюхах в онлайне не построить.


  1. Kilor
    31.01.2022 14:51
    +2

    Аналогичное писал на примере PostgreSQL с пояснительными картинками: https://habr.com/ru/company/tensor/blog/488104/

    Еще добавлю пример исключения для низкоселективных полей, когда они используются для сортировки - например, по иерархии "папки в начале": idx(parent::int, isbranch::bool) -> ... WHERE parent = $1 ORDER BY isbranch DESC LIMIT 20;


  1. Sdolgov
    31.01.2022 15:03
    +3

    Создается ощущение что индексы все бывают только B-tree. В оракле, например, для низкоселективных полей есть bitmap. Наверняка в этих системах тоже есть аналоги какие-то. И рассуждать о применимости индекса только из селективности как-то однобоко.


    1. Tellamonid
      31.01.2022 15:27
      +1

      Вот эта часть из статьи как раз подходит к битмап-индексам: "Селективность колонки мала, но селективность набора многих колонок высока. Если все эти колонки используются в WHERE, то такой индекс будет полезен."

      Сила битмап-индексов в том, что они хорошо сочетаются друг с другом (а маски нулей и единиц, соответсвенно, накладываются). А недостаток – слишком много строк блокируется при модификации данных, поэтому такие индексы, как правило, не используют в OLTP-системах, а только в хранилищах.


  1. Korobei
    31.01.2022 17:13

    Было бы ещё хорошо добавить правила по выбору когда индекс должен быть clustered или нет, а то в большинстве случаев тупо по unique id делается, хотя видно что есть гораздо более предпочтительный кандидат, особенно для дочерних таблиц.


  1. Starche
    31.01.2022 22:57

    Если в колонке дата/время не на последнем месте, то проверьте

    Добавлю от себя исключение из недавнего на работе: если индекс не для сравнения, а для сортировки, то он вполне может начинаться с даты. `ORDER BY date, id` может быть использован для досортировки данных с одинаковой датой (это полезно, например, для пагинации, чтобы одни и те же строки не попали на разные страницы)


  1. rrrad
    01.02.2022 03:23

    Есть три замечания по поводу PostgreSQL: 1) колонку с низкой селективностью, по которой используется предикат в запросе, очень часто надо добавлять не в индекс, а в предикат частичного индекса (при этом уменьшается размер индекса); 2) если неселективная колонка не меняет значения и значения не слишком перемешаны в хипе, может помочь brin-индекс (особенно начиная с 14й версии, в которой появились multirange), в худшем случае, он не сильно много места и ресурсов на поддержку отъест, по сравнению с другими типами (но лучше проверять, на сколько он хорош в конкретном случае); 3) Postgres, никто ведь не пишет "MS SQ".


  1. eugeneyp
    01.02.2022 14:00

    У MS SQL Server есть отвратительная особенность: при отключенном версионировании( по разным причинам) сервер делает Lock на чтение данных! В этих случаях используется обходной путь чтобы избежать блокировок строятся индексы с кучей колонок в include секции. Выглядит уродливо, но альтернативы в виде read uncommited еще хуже.


    1. za2li
      01.02.2022 23:26

      Напишите пожалуйста подробнее. Что значит отключение версионирования? И какой лок накладывается? Впервые слышу про эту проблему...


      1. Tzimie Автор
        01.02.2022 23:27
        +1

        На самом деле там идёт firehorse, как это вроде называют майкрософтовцы, а не Лок.