Индексы в MySQL

Thank you for reading this post, don't forget to subscribe!

Индек­сы в MySQL (Mysql indexes) — отлич­ный инстру­мент для опти­ми­за­ции SQL запро­сов. Что­бы понять, как они рабо­та­ют, посмот­рим на рабо­ту с дан­ны­ми без них.

1. Чтение данных с диска

На жест­ком дис­ке нет тако­го поня­тия, как файл. Есть поня­тие блок. Один файл обыч­но зани­ма­ет несколь­ко бло­ков. Каж­дый блок зна­ет, какой блок идет после него. Файл делит­ся на кус­ки и каж­дый кусок сохра­ня­ет­ся в пустой блок.

При чте­нии фай­ла, мы по оче­ре­ди про­хо­дим­ся по всем бло­кам и соби­ра­ем файл из кус­ков. Бло­ки одно­го фай­ла могут быть рас­ки­да­ны по дис­ку (фраг­мен­та­ция). Тогда чте­ние фай­ла замед­лит­ся, т.к. пона­до­бит­ся пры­гать раз­ным участ­кам диска.

Когда мы ищем что-то внут­ри фай­ла, нам пона­до­бит­ся прой­тись по всем бло­кам, в кото­рых он сохра­нен. Если файл очень боль­шой, то и коли­че­ство бло­ков будет зна­чи­тель­ным. Необ­хо­ди­мость пере­пры­ги­вать с бло­ка на блок, кото­рые могут нахо­дить­ся в раз­ных местах, силь­но замед­лит поиск данных.

2. Поиск данных в MySQL

Таб­ли­цы MySQL — это обыч­ные фай­лы. Выпол­ним запрос тако­го вида:

MySQL при этом откры­ва­ет файл, где хра­нят­ся дан­ные из таб­ли­цы users. А даль­ше — начи­на­ет пере­би­рать весь файл, что­бы най­ти нуж­ные записи.

Кро­ме это­го, MySQL будет срав­ни­вать дан­ные в каж­дой стро­ке таб­ли­цы со зна­че­ни­ем в запро­се. Допу­стим рабо­та ведет­ся с таб­ли­цей, в кото­рой есть 10 запи­сей. Тогда MySQL про­чи­та­ет все 10 запи­сей, срав­нит колон­ку age каж­дой из них со зна­че­ни­ем 29 и отбе­рет толь­ко под­хо­дя­щие данные:

Итак, есть две про­бле­мы при чте­нии данных:

  • Низ­кая ско­рость чте­ния фай­лов из-за рас­по­ло­же­ния бло­ков в раз­ных частях дис­ка (фраг­мен­та­ция).
  • Боль­шое коли­че­ство опе­ра­ций срав­не­ния для поис­ка нуж­ных данных.

3. Сортировка данных

Пред­ста­вим, что мы отсор­ти­ро­ва­ли наши 10 запи­сей по убы­ва­нию. Тогда исполь­зуя алго­ритм бинар­но­го поис­ка, мы мог­ли бы мак­си­мум за 4 опе­ра­ции отобрать нуж­ные нам значения:

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

Индекс — это и есть отсор­ти­ро­ван­ный набор зна­че­ний. В MySQL индек­сы все­гда стро­ят­ся для какой-то кон­крет­ной колон­ки. Напри­мер, мы мог­ли бы постро­ить индекс для колон­ки age из примера.

4. Выбор индексов в MySQL

В самом про­стом слу­чае, индекс необ­хо­ди­мо созда­вать для тех коло­нок, кото­рые при­сут­ству­ют в усло­вии WHERE.

Рас­смот­рим запрос из примера:

Нам необ­хо­ди­мо создать индекс на колон­ку age:

После этой опе­ра­ции MySQL нач­нет исполь­зо­вать индекс age для выпол­не­ния подоб­ных запро­сов. Индекс будет исполь­зо­вать­ся и для выбо­рок по диа­па­зо­нам зна­че­ний этой колонки:

Сортировка

Для запро­сов тако­го вида:

дей­ству­ет такое же пра­ви­ло — созда­ем индекс на колон­ку, по кото­рой про­ис­хо­дит сортировка:

Внутренности хранения индексов

Пред­ста­вим, что наша таб­ли­ца выгля­дит так:

После созда­ния индек­са на колон­ку age, MySQL сохра­нит все ее зна­че­ния в отсор­ти­ро­ван­ном виде:

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

Уникальные индексы

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

На колон­ку email необ­хо­ди­мо создать уни­каль­ный индекс:

Тогда при поис­ке дан­ных, MySQL оста­но­вит­ся после обна­ру­же­ния пер­во­го соот­вет­ствия. В слу­чае обыч­но­го индек­са будет обя­за­тель­но про­ве­де­на еще одна про­вер­ка (сле­ду­ю­ще­го зна­че­ния в индексе).

5. Составные индексы

MySQL может исполь­зо­вать толь­ко один индекс для запро­са (кро­ме слу­ча­ев, когда MySQL спо­со­бен объ­еди­нить резуль­та­ты выбо­рок по несколь­ким индек­сам). Поэто­му, для запро­сов, в кото­рых исполь­зу­ет­ся несколь­ко коло­нок, необ­хо­ди­мо исполь­зо­вать состав­ные индек­сы.

Рас­смот­рим такой запрос:

Нам сле­ду­ет создать состав­ной индекс на обе колонки:

Устройство составного индекса

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

зна­че­ния состав­но­го индек­са будут такими:

Это озна­ча­ет, что оче­ред­ность коло­нок в индек­се будет играть боль­шую роль. Обыч­но колон­ки, кото­рые исполь­зу­ют­ся в усло­ви­ях WHERE, сле­ду­ет ста­вить в нача­ло индек­са. Колон­ки из ORDER BY — в конец.

Поиск по диапазону

Пред­ста­вим, что наш запрос будет исполь­зо­вать не срав­не­ние, а поиск по диапазону:

Тогда MySQL не смо­жет исполь­зо­вать пол­ный индекс, т.к. зна­че­ния gender будут отли­чать­ся для раз­ных зна­че­ний колон­ки age. В этом слу­чае база дан­ных попы­та­ет­ся исполь­зо­вать часть индек­са (толь­ко age), что­бы выпол­нить этот запрос:

Сна­ча­ла будут отфиль­тро­ва­ны все дан­ные, кото­рые под­хо­дят под усло­вие age <= 29. Затем, поиск по зна­че­нию "male" будет про­из­ве­ден без исполь­зо­ва­ния индекса.

Сортировка

Состав­ные индек­сы так­же мож­но исполь­зо­вать, если выпол­ня­ет­ся сортировка:

В этом слу­чае нам нуж­но будет создать индекс в дру­гом поряд­ке, т.к. сор­ти­ров­ка (ORDER) про­ис­хо­дит после филь­тра­ции (WHERE):

Такой поря­док коло­нок в индек­се поз­во­лит выпол­нить филь­тра­цию по пер­вой части индек­са, а затем отсор­ти­ро­вать резуль­тат по второй.

Коло­нок в индек­се может быть боль­ше, если требуется:

В этом слу­чае сле­ду­ет создать такой индекс:

6. Использование EXPLAIN для анализа индексов

Инструк­ция EXPLAIN пока­жет дан­ные об исполь­зо­ва­нии индек­сов для кон­крет­но­го запро­са. Например:

Колон­ка key пока­зы­ва­ет исполь­зу­е­мый индекс. Колон­ка possible_keys пока­зы­ва­ет все индек­сы, кото­рые могут быть исполь­зо­ва­ны для это­го запро­са. Колон­ка rows пока­зы­ва­ет чис­ло запи­сей, кото­рые при­шлось про­чи­тать базе дан­ных для выпол­не­ния это­го запро­са (в таб­ли­це все­го 336 записей).

Как видим, в при­ме­ре не исполь­зу­ет­ся ни один индекс. После созда­ния индекса:

Про­чи­та­на все­го одна запись, т.к. был исполь­зо­ван индекс.

Проверка длинны составных индексов

Explain так­же помо­жет опре­де­лить пра­виль­ность исполь­зо­ва­ния состав­но­го индек­са. Про­ве­рим запрос из при­ме­ра (с индек­сом на колон­ки age и gender):

Зна­че­ние key_len пока­зы­ва­ет исполь­зу­е­мую дли­ну индек­са. В нашем слу­чае 24 бай­та — длин­на все­го индек­са (5 байт age + 19 байт gender).

Если мы выпол­ним изме­ним точ­ное срав­не­ние на поиск по диа­па­зо­ну, уви­дим что MySQL исполь­зу­ет толь­ко часть индекса:

Это сиг­нал о том, что создан­ный индекс не под­хо­дит для это­го запро­са. Если же мы созда­дим пра­виль­ный индекс:

В этом слу­чае MySQL исполь­зу­ет весь индекс gender_age, т.к. поря­док коло­нок в нем поз­во­ля­ет сде­лать эту выборку.

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

Вер­нем­ся к запросу:

Для тако­го запро­са необ­хо­ди­мо создать состав­ной индекс. Но как пра­виль­но выбрать после­до­ва­тель­ность коло­нок в индек­се? Вари­ан­та два:

  • age, gender
  • gender, age

Подой­дут оба. Но рабо­тать они будут с раз­ной эффективностью.

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

Эта инфор­ма­ция гово­рит нам вот о чем:

  1. Любое зна­че­ние колон­ки age обыч­но содер­жит око­ло 200 записей.
  2. Любое зна­че­ние колон­ки gender — око­ло 6000 записей.

Если колон­ка age будет идти пер­вой в индек­се, тогда MySQL после пер­вой части индек­са сокра­тит коли­че­ство запи­сей до 200. Оста­нет­ся сде­лать выбор­ку по ним. Если же колон­ка gender будет идти пер­вой, то коли­че­ство запи­сей будет сокра­ще­но до 6000 после пер­вой части индек­са. Т.е. на поря­док боль­ше, чем в слу­чае age.

Это зна­чит, что индекс age_gender будет рабо­тать луч­ше, чем gender_age.

Селек­тив­ность колон­ки опре­де­ля­ет­ся коли­че­ством запи­сей в таб­ли­це с оди­на­ко­вы­ми зна­че­ни­я­ми. Когда запи­сей с оди­на­ко­вым зна­че­ни­ем мало — селек­тив­ность высо­кая. Такие колон­ки необ­хо­ди­мо исполь­зо­вать пер­вы­ми в состав­ных индексах.

8. Первичные ключи

Пер­вич­ный ключ (Primary Key) — это осо­бый тип индек­са, кото­рый явля­ет­ся иден­ти­фи­ка­то­ром запи­сей в таб­ли­це. Он обя­за­тель­но уни­каль­ный и ука­зы­ва­ет­ся при созда­нии таблиц:

При исполь­зо­ва­нии таб­лиц InnoDB все­гда опре­де­ляй­те пер­вич­ные клю­чи. Если пер­вич­но­го клю­ча нет, MySQL все рав­но создаст вир­ту­аль­ный скры­тый ключ.

Кластерные индексы

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

Кла­стер­ные индек­сы сохра­ня­ют дан­ные запи­сей цели­ком, а не ссыл­ки на них. При рабо­те с таким индек­сом не тре­бу­ет­ся допол­ни­тель­ной опе­ра­ции чте­ния данных.

Пер­вич­ные клю­чи таб­лиц InnoDB явля­ют­ся кла­стер­ны­ми. Поэто­му выбор­ки по ним про­ис­хо­дят очень эффективно.

Overhead

Важ­но пом­нить, что индек­сы пред­по­ла­га­ют допол­ни­тель­ные опе­ра­ции запи­си на диск. При каж­дом обнов­ле­нии или добав­ле­нии дан­ных в таб­ли­цу, про­ис­хо­дит так­же запись и обнов­ле­ние дан­ных в индексе.

Созда­вай­те толь­ко необ­хо­ди­мые индек­сы, что­бы не рас­хо­до­вать зря ресур­сы сер­ве­ра. Кон­тро­ли­руй­те раз­ме­ры индек­сов для Ваших таблиц:

Когда создавать индексы?

  • Индек­сы сле­ду­ет созда­вать по мере обна­ру­же­ния мед­лен­ных запро­сов. В этом помо­жет slow log в MySQL. Запро­сы, кото­рые выпол­ня­ют­ся более 1 секун­ды явля­ют­ся пер­вы­ми кан­ди­да­та­ми на оптимизацию.
  • Начи­най­те созда­ние индек­сов с самых частых запро­сов. Запрос, выпол­ня­ю­щий­ся секун­ду, но 1000 раз в день нано­сит боль­ше ущер­ба, чем 10-секунд­ный запрос, кото­рый выпол­ня­ет­ся несколь­ко раз в день.
  • Не созда­вай­те индек­сы на таб­ли­цах, чис­ло запи­сей в кото­рых мень­ше несколь­ких тысяч. Для таких раз­ме­ров выиг­рыш от исполь­зо­ва­ния индек­са будет почти незаметен.
  • Не созда­вай­те индек­сы зара­нее, напри­мер, в сре­де раз­ра­бот­ки. Индек­сы долж­ны уста­нав­ли­вать­ся исклю­чи­тель­но под фор­му и тип нагруз­ки рабо­та­ю­щей системы.
  • Уда­ляй­те неис­поль­зу­е­мые индексы.

Самое важное

Выде­ляй­те доста­точ­но вре­ме­ни на ана­лиз и орга­ни­за­цию индек­сов в MySQL (и дру­гих базах дан­ных). На это может уйти намно­го боль­ше вре­ме­ни, чем на про­ек­ти­ро­ва­ние струк­ту­ры базы дан­ных. Удоб­но будет орга­ни­зо­вать тесто­вую сре­ду с копи­ей реаль­ных дан­ных и про­ве­рять там раз­ные струк­ту­ры индексов.

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

Не ссы­те.