Совет дня №24
Когда мы попадаем в индекс, желательно понимать, что при этом происходит. В Postgres btree-индекс – это дерево распределительных узлов, и на последнем уровне хранятся пары: значение -> номер страницы (блока). Поэтому даже если вы попали в индекс, предстоит некоторая работа: проверить, есть ли такой блок в буферном кэше, если нет – подгрузить его с диска. Затем пройтись по всем строкам блока (линейным перебором) и найти ту, что подходит условию.
В редких случаях можно срезать угол: взять данные только из индекса, не обращаясь к диску.
Если выполнить такой запрос:
select id from users where id = 123456
, то значение id будет взято из индекса – нет смысла обращаться в
таблицу. Конечно, это наивный пример: мы и так знаем айдишку. Но он полезен для
диапазонов: скажем, выбрать айдишки между 100 и 200 или <= 10000. В этом случае
они будут взяты из индекса.
Более реалистичный пример: предположим, есть таблица пользователей. Из прошлого совета про ключи мы помним, что у людей должен быть суррогатный ключ, а почта используется для уникальности. Поэтому таблица объявлена так:
create table users (
id integer primary key,
email text not null
);
Уникальность гарантирует специальный индекс:
create unique index idx_users_email
on users (email);
На практике индекс строят по выражению trim(lower(email)), но сейчас это не
важно.
Вставим миллион пользователей:
insert into users
select x, format('email_%s@test.com', x)
from generate_series(1, 1000000) as seq(x);
Задача в следующем: нужно часто получать айди пользователя по его почте (обратный резолв). Нам передают почту, по ней мы находим айди, и все запросы пляшут от этого айдишника. Напишем следующий запрос:
select id from users
where email = 'email_500000@test.com';
-- 500000
Он довольно шустрый:
┌──────────────────────────────────────────
│ Index Scan using idx_users_email on users
│ Index Cond: (email = 'email_500000@test
│ Planning Time: 0.155 ms
│ Execution Time: 0.896 ms
└──────────────────────────────────────────
Теперь удалим этот индекс и построим немного другой:
drop index idx_users_email;
create unique index idx_users_email
on users (email) include (id);
Такой индекс называется покрывающим из-за выражения include (id). Смысл в том,
что значение id хранится в дереве btree как метаданные. Это увеличивает
размер индекса, зато позволяет взять указанное поле без обращения к диску. Вот
что показывает новый план:
┌───────────────────────────────────────────────
│ Index Only Scan using idx_users_email on users
│ Index Cond: (email = 'email_500000@test.com'
│ Heap Fetches: 0
│ Planning Time: 0.830 ms
│ Execution Time: 0.141 ms
└───────────────────────────────────────────────
Обратите внимание на “Index Only Scan” – только индекс без обращения к диску. Время выполнения ниже, стоимость тоже.
Если прогреть индекс idx_users_email (см. прошлый совет), он будет находиться
в памяти. В этом случае резолв айдишки по почте не отличается от Redis: все
происходит в памяти без обращения к диску.
Прием тем эффективней, чем больше столбцов и записей в таблице. Не обязательно пихать его тут и там, но в нужный момент он очень полезен.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter