sqlpostgresqlindexesperformance

Sargable WHERE: Writing Index-Friendly Predicates

Why wrapping a column in a function kills the index and how to rewrite WHERE into ranges so PostgreSQL actually uses it.

12 min lasīšanaReferencesql · postgresql · indexes · performance · sargable · query-optimization
Šis raksts pašlaik ir krievu valodā — angļu tulkojums tiek gatavots.

Индекс в базе данных похож на оглавление в книге.

Если вы ищете главу по названию, вы открываете оглавление и сразу переходите на нужную страницу. Быстро и удобно.

Но если вы сформулировали поиск так, что оглавление уже нельзя использовать, придётся листать книгу целиком.

С SQL происходит то же самое.

Можно создать хороший индекс:

CREATE INDEX idx_orders_created_at
ON orders (created_at);

А потом написать условие так, что база всё равно не сможет нормально использовать этот индекс:

WHERE date(created_at) = '2026-03-15'

На первый взгляд условие понятное: найти заказы за 15 марта.

Но для индекса это неудобно. В индексе лежат значения created_at, а мы просим базу вычислить date(created_at) для каждой строки.

Такой предикат называют несаргабельным.

А хороший предикат, который позволяет базе использовать индекс для поиска, называют sargable.


Что значит sargable

Слово sargable происходит от английского выражения:

Search ARGument ABLE

Проще говоря, условие в WHERE называется sargable, если база может использовать индекс, чтобы быстро найти нужные строки.

Например, есть индекс:

CREATE INDEX idx_users_email
ON users (email);

Хорошее условие:

WHERE email = 'ann@example.com'

База может открыть индекс по email и быстро найти нужную строку.

Плохое условие:

WHERE lower(email) = 'ann@example.com'

Здесь мы уже ищем не само значение email, а результат функции lower(email).

Обычный индекс по email хранит исходные email, а не заранее посчитанные значения lower(email).

Поэтому базе сложнее использовать этот индекс напрямую.

Главная идея такая:

Чем проще условие по индексируемой колонке,
тем больше шансов, что индекс будет использован эффективно.

Базовая таблица для примеров

Будем использовать таблицу заказов:

CREATE TABLE orders (
    id         bigint PRIMARY KEY,
    user_id    bigint        NOT NULL,
    amount     numeric(12,2) NOT NULL,
    status     text          NOT NULL,
    created_at timestamptz   NOT NULL DEFAULT now()
);

И таблицу пользователей:

CREATE TABLE users (
    id         bigint PRIMARY KEY,
    email      text NOT NULL,
    name       text NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now()
);

Создадим несколько индексов:

CREATE INDEX idx_orders_created_at
ON orders (created_at);

CREATE INDEX idx_orders_amount
ON orders (amount);

CREATE INDEX idx_users_email
ON users (email);

CREATE INDEX idx_users_name
ON users (name);

Теперь разберём, какие условия помогают этим индексам, а какие мешают.


Простое сравнение — хороший вариант

Самый понятный sargable-предикат — обычное сравнение колонки с константой.

Например:

SELECT id, amount, status
FROM orders
WHERE created_at >= '2026-01-01';

Здесь колонка created_at стоит «голой», без функции, без вычислений и без преобразований.

Индекс:

CREATE INDEX idx_orders_created_at
ON orders (created_at);

может помочь базе быстро найти строки начиная с нужной даты.

То же самое с числом:

SELECT id, amount
FROM orders
WHERE amount > 1000;

Индекс по amount может использоваться для поиска диапазона.

Хорошие условия обычно выглядят так:

WHERE column = value
WHERE column > value
WHERE column >= value
WHERE column < value
WHERE column BETWEEN value1 AND value2

Главное: колонка остаётся сама собой.


Что чаще всего ломает использование индекса

Индекс обычно строится по значениям колонки.

Например:

CREATE INDEX idx_orders_created_at
ON orders (created_at);

В этом индексе лежат значения created_at.

Но если написать:

WHERE date(created_at) = '2026-03-15'

мы уже ищем не created_at, а результат функции date(created_at).

Базе нужно взять каждую строку, применить к ней функцию date(), получить дату без времени и сравнить с '2026-03-15'.

Индекс по исходному created_at в такой ситуации может не помочь так, как вы ожидаете.

Типичные примеры, которые мешают индексу:

WHERE lower(email) = 'ann@example.com'
WHERE date(created_at) = '2026-03-15'
WHERE created_at::date = '2026-03-15'
WHERE EXTRACT(YEAR FROM created_at) = 2026
WHERE amount + 10 > 1000
WHERE salary * 12 > 100000
WHERE coalesce(status, '') = 'paid'

Общий признак:

Над колонкой что-то сделали,
и только потом сравнили результат.

Правило: не трогайте колонку, переносите вычисления вправо

Хорошее практическое правило:

В условии WHERE индексируемая колонка должна оставаться «голой».

Плохо:

WHERE salary * 12 > 100000

Лучше:

WHERE salary > 100000 / 12

В первом варианте база должна вычислить salary * 12 для строк.

Во втором варианте вычисление находится на стороне константы, а колонка salary остаётся обычной колонкой.

Плохо:

WHERE amount + 10 > 1000

Лучше:

WHERE amount > 990

Плохо:

WHERE price * 1.2 <= 600

Лучше:

WHERE price <= 600 / 1.2

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


Самая частая ошибка с датами

Одна из самых популярных ошибок — искать день через функцию:

SELECT id, amount
FROM orders
WHERE date(created_at) = '2026-03-15';

Логически запрос понятный: найти заказы за 15 марта.

Но технически мы применили функцию date() к колонке created_at.

Если created_at хранит дату и время, например:

2026-03-15 10:30:00
2026-03-15 18:45:00
2026-03-16 01:20:00

то date(created_at) отрежет время и оставит только дату.

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

Поэтому лучше переписать запрос на диапазон.


Правильный поиск одного дня через диапазон

Вместо:

WHERE date(created_at) = '2026-03-15'

пишите:

WHERE created_at >= '2026-03-15'
  AND created_at <  '2026-03-16'

Полный запрос:

SELECT id, amount
FROM orders
WHERE created_at >= '2026-03-15'
  AND created_at <  '2026-03-16';

Это называется полуоткрытый интервал.

Левая граница включается:

created_at >= '2026-03-15'

Правая граница не включается:

created_at < '2026-03-16'

Так мы берём весь день 15 марта:

2026-03-15 00:00:00
...
2026-03-15 23:59:59.999999

но не захватываем 16 марта.

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


Почему не стоит писать BETWEEN для дат с временем

Иногда пишут так:

WHERE created_at BETWEEN '2026-03-15' AND '2026-03-15 23:59:59'

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

Почему?

Потому что точность времени может быть выше секунд.

Например, значение:

2026-03-15 23:59:59.500

больше, чем:

2026-03-15 23:59:59

и может не попасть в выборку.

Поэтому для дат и времени лучше использовать полуоткрытый интервал:

WHERE created_at >= '2026-03-15'
  AND created_at <  '2026-03-16'

Так вы не зависите от точности хранения времени.


Поиск года без EXTRACT

Ещё один частый пример:

SELECT id, amount
FROM orders
WHERE EXTRACT(YEAR FROM created_at) = 2026;

Логика понятна: хотим заказы за 2026 год.

Но снова проблема: функция над колонкой.

Лучше написать диапазон:

SELECT id, amount
FROM orders
WHERE created_at >= '2026-01-01'
  AND created_at <  '2027-01-01';

Это и понятнее для индекса, и обычно быстрее.

То же самое для месяца:

Плохо:

WHERE date_trunc('month', created_at) = '2026-03-01'

Лучше:

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

Для недели, квартала и любого периода идея та же: не применяйте функцию к колонке, а задавайте границы диапазона.


Преобразование типа тоже может мешать

Не только функции, но и приведение типа может испортить использование индекса.

Например:

WHERE created_at::date = '2026-03-15'

Это почти то же самое, что:

WHERE date(created_at) = '2026-03-15'

Мы преобразовали колонку created_at к дате без времени.

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

Лучше снова использовать диапазон:

WHERE created_at >= '2026-03-15'
  AND created_at <  '2026-03-16'

Хороший принцип:

Приводите к нужному типу константу,
а не колонку.

То есть лучше так:

WHERE created_at >= '2026-03-15'::timestamptz

чем так:

WHERE created_at::date = '2026-03-15'

Регистронезависимый поиск email

Теперь разберём пример с email.

Есть индекс:

CREATE INDEX idx_users_email
ON users (email);

И есть запрос:

SELECT id, email
FROM users
WHERE lower(email) = 'ann@example.com';

Так часто пишут, чтобы найти email без учёта регистра.

Но обычный индекс по email не хранит lower(email). Он хранит исходное значение.

Например, в таблице может лежать:

Ann@Example.COM

А в запросе мы сравниваем:

ann@example.com

Если каждый раз применять lower(email), индекс по обычному email становится менее полезным.

Есть несколько нормальных решений.


Решение 1: хранить email уже нормализованным

Самый простой и часто лучший вариант — нормализовать email перед записью.

Например, всегда хранить email в нижнем регистре и без пробелов по краям:

ann@example.com

Тогда запрос остаётся простым:

SELECT id, email
FROM users
WHERE email = 'ann@example.com';

И обычный индекс по email работает хорошо:

CREATE INDEX idx_users_email
ON users (email);

Чтобы не забывать нормализацию, её можно делать:

  • в приложении;
  • через BEFORE INSERT OR UPDATE триггер;
  • через отдельную нормализованную колонку;
  • через generated column.

Например, можно хранить отдельное поле:

email_norm text NOT NULL

и индексировать его:

CREATE INDEX idx_users_email_norm
ON users (email_norm);

Тогда поиск будет таким:

SELECT id, email
FROM users
WHERE email_norm = 'ann@example.com';

Решение 2: использовать citext

В PostgreSQL есть расширение citext, которое даёт регистронезависимый текстовый тип.

Подключается так:

CREATE EXTENSION IF NOT EXISTS citext;

Можно сделать колонку:

email citext NOT NULL

Тогда сравнение:

WHERE email = 'ann@example.com'

будет регистронезависимым.

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

Но citext — это уже осознанное решение на уровне схемы. Его нужно выбирать заранее и понимать последствия для сравнения строк.


Решение 3: индекс по выражению

Иногда функция в условии действительно нужна.

Например, вы не можете изменить хранение email прямо сейчас, но хотите ускорить запрос:

WHERE lower(email) = 'ann@example.com'

Тогда можно создать индекс не по колонке, а по выражению:

CREATE INDEX idx_users_lower_email
ON users (lower(email));

Теперь PostgreSQL хранит в индексе результат выражения lower(email).

И запрос:

SELECT id, email
FROM users
WHERE lower(email) = 'ann@example.com';

сможет использовать этот индекс.

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

Но важно: выражение в запросе должно совпадать с выражением в индексе.

Если индекс создан по:

lower(email)

а в запросе написано что-то другое, например:

lower(trim(email))

это уже другое выражение. Такой индекс может не подойти.


Не делайте функциональные индексы там, где достаточно диапазона

Функциональные индексы полезны, но не стоит использовать их без необходимости.

Например, можно создать индекс:

CREATE INDEX idx_orders_created_date
ON orders ((created_at::date));

и потом писать:

WHERE created_at::date = '2026-03-15'

Это может работать.

Но часто лучше и проще писать диапазон:

WHERE created_at >= '2026-03-15'
  AND created_at <  '2026-03-16'

Почему?

Потому что обычный индекс по created_at более универсален.

Он подходит для:

WHERE created_at >= '2026-03-15'
WHERE created_at < '2026-04-01'
ORDER BY created_at DESC
WHERE created_at >= '2026-03-01'
  AND created_at <  '2026-04-01'

А индекс по created_at::date полезен только для конкретного выражения.

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


LIKE: когда индекс помогает

Теперь поговорим про текстовый поиск.

Есть индекс:

CREATE INDEX idx_users_name
ON users (name);

Запрос:

SELECT id, name
FROM users
WHERE name LIKE 'Ann%';

ищет имена, которые начинаются с Ann.

Например:

Ann
Anna
Annabelle

Такой поиск называется префиксным.

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

Условно база может найти место, где начинаются значения на Ann, и читать дальше, пока префикс совпадает.


LIKE с ведущим процентом

А вот такой запрос намного хуже для обычного B-tree индекса:

SELECT id, name
FROM users
WHERE name LIKE '%ann%';

Он ищет подстроку ann где угодно внутри имени.

Например:

Joanna
Brianna
Ann

Проблема в том, что строка может начинаться с чего угодно.

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

Поэтому обычный B-tree индекс не может просто прыгнуть к нужному месту.

Такие условия часто приводят к полному сканированию или требуют других типов индексов.

Практическое правило:

Условие Обычный B-tree индекс
name LIKE 'Ann%' Может помочь
name LIKE '%ann' Обычно не помогает
name LIKE '%ann%' Обычно не помогает

PostgreSQL и text_pattern_ops

В PostgreSQL есть важная деталь.

Для префиксного LIKE обычный B-tree индекс хорошо работает не во всех настройках локали.

Иногда для надёжного ускорения LIKE 'Ann%' создают специальный индекс с operator class text_pattern_ops:

CREATE INDEX idx_users_name_pattern
ON users (name text_pattern_ops);

Тогда запрос:

SELECT id, name
FROM users
WHERE name LIKE 'Ann%';

может использовать этот индекс для поиска по префиксу.

Для новичка главное запомнить:

LIKE 'abc%' — хороший кандидат для B-tree.
LIKE '%abc%' — нет, нужен другой подход.

Поиск подстроки: pg_trgm

Если вам нужен поиск по подстроке:

WHERE name LIKE '%ann%'

обычный B-tree индекс не подходит.

В PostgreSQL для таких задач часто используют расширение pg_trgm.

Подключаем расширение:

CREATE EXTENSION IF NOT EXISTS pg_trgm;

Создаём GIN-индекс:

CREATE INDEX idx_users_name_trgm
ON users USING gin (name gin_trgm_ops);

Теперь запросы вроде:

SELECT id, name
FROM users
WHERE name LIKE '%ann%';

или:

SELECT id, name
FROM users
WHERE name ILIKE '%ann%';

могут выполняться намного быстрее.

Это уже не sargable в смысле обычного B-tree поиска, но это правильный индекс под такой тип задачи.


OR тоже может усложнить использование индекса

Иногда индексу мешают не функции, а сложная логика условия.

Например:

SELECT *
FROM orders
WHERE user_id = 42
   OR status = 'paid';

Здесь две разные колонки и условие OR.

PostgreSQL иногда может использовать несколько индексов и объединить результат. Но не всегда это будет эффективно.

Если запрос важный и работает плохо, иногда его можно переписать через UNION.

Например:

SELECT *
FROM orders
WHERE user_id = 42

UNION

SELECT *
FROM orders
WHERE status = 'paid';

Это не универсальный совет, но полезный приём: иногда два простых sargable-запроса работают лучше, чем один сложный OR.

Проверять нужно через EXPLAIN.


COALESCE в WHERE

Ещё один популярный пример:

WHERE coalesce(status, '') = 'paid'

Так часто пишут, чтобы обработать NULL.

Но здесь снова функция над колонкой.

Если бизнес-логика позволяет, лучше сделать колонку status обязательной:

status text NOT NULL

и писать просто:

WHERE status = 'paid'

Если NULL всё-таки возможен, можно переписать условие явно:

WHERE status = 'paid'

В большинстве случаев строки с NULL всё равно не должны попасть в результат, потому что NULL = 'paid' не является истинным.

Если же вам нужна особая логика для NULL, лучше написать её явно:

WHERE status = 'paid'
   OR status IS NULL

Но помните: OR тоже может усложнить план. Иногда для таких случаев нужен частичный индекс, отдельный индекс или изменение модели данных.


Неявные преобразования типов

Иногда индекс не используется хорошо из-за несоответствия типов.

Например, колонка user_id имеет тип bigint, а в запросе вы сравниваете её со строкой:

WHERE user_id = '42'

PostgreSQL часто сам приведёт константу к нужному типу, и всё будет нормально.

Но в более сложных случаях, особенно в разных СУБД, неявные преобразования могут привести к тому, что база начнёт преобразовывать колонку, а не константу.

Плохая идея:

WHERE user_id::text = '42'

Здесь мы явно превратили колонку в текст.

Лучше:

WHERE user_id = 42

Принцип тот же:

Не приводите колонку к другому типу в WHERE,
если хотите использовать обычный индекс по этой колонке.

IN обычно нормальный

Условие IN часто остаётся sargable.

Например:

SELECT *
FROM orders
WHERE status IN ('paid', 'shipped', 'cancelled');

Индекс по status может использоваться.

То же самое:

SELECT *
FROM orders
WHERE user_id IN (10, 20, 30);

Для индекса это похоже на несколько точечных поисков.

Но если список IN огромный, всё зависит от размера таблицы, статистики и плана. Как всегда, проверяем через EXPLAIN.


NOT и неравенства

Условия вида:

WHERE status <> 'paid'

или:

WHERE NOT status = 'paid'

часто менее полезны для индекса, чем точное равенство.

Почему?

Потому что такое условие может возвращать большую часть таблицы.

Если 90% заказов имеют статус не paid, то индекс не особо помогает: базе всё равно нужно прочитать очень много строк.

Это не столько проблема sargability, сколько проблема селективности.

Индекс особенно полезен, когда условие отбирает небольшую долю строк.

Например:

WHERE status = 'failed'

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

А:

WHERE status <> 'paid'

может быть плохим, если таких строк большинство.


Sargable не значит «индекс точно будет использован»

Важный момент: sargable-предикат не гарантирует, что PostgreSQL обязательно выберет индекс.

Например:

WHERE status = 'paid'

Это нормальное условие.

Но если почти все строки имеют status = 'paid', индекс может быть бесполезен. Базе проще прочитать всю таблицу, чем прыгать по индексу и всё равно доставать почти каждую строку.

Поэтому sargable означает:

У базы есть возможность эффективно использовать индекс.

Но окончательное решение принимает оптимизатор.

Он смотрит на:

  • размер таблицы;
  • статистику;
  • селективность условия;
  • стоимость чтения индекса и таблицы;
  • доступные индексы;
  • актуальность статистики.

Поэтому после переписывания запроса нужно проверять план.


Как проверять через EXPLAIN

Допустим, у нас есть индекс:

CREATE INDEX idx_orders_created_at
ON orders (created_at);

Плохой вариант:

EXPLAIN
SELECT id, amount
FROM orders
WHERE date(created_at) = '2026-03-15';

Хороший вариант:

EXPLAIN
SELECT id, amount
FROM orders
WHERE created_at >= '2026-03-15'
  AND created_at <  '2026-03-16';

В хорошем плане вы чаще ожидаете увидеть что-то вроде:

Index Scan
Bitmap Index Scan
Index Only Scan

А если видите:

Seq Scan
Filter: ...

значит, база читает таблицу последовательно и фильтрует строки после чтения.

Для реальной диагностики лучше использовать:

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, amount
FROM orders
WHERE created_at >= '2026-03-15'
  AND created_at <  '2026-03-16';

Так вы увидите не только план, но и фактическое выполнение.


Индекс по выражению: когда без функции никак

Иногда функция в WHERE действительно нужна и переписать запрос красиво нельзя.

Например:

WHERE lower(email) = 'ann@example.com'

Тогда можно создать индекс по выражению:

CREATE INDEX idx_users_lower_email
ON users (lower(email));

После этого запрос:

SELECT id, email
FROM users
WHERE lower(email) = 'ann@example.com';

может стать быстрым.

Ещё пример: если вы часто ищете по дате без времени и сознательно хотите именно такой доступ:

CREATE INDEX idx_orders_created_day
ON orders ((created_at::date));

Тогда запрос:

SELECT id, amount
FROM orders
WHERE created_at::date = '2026-03-15';

может использовать этот индекс.

Но для дат чаще всё равно лучше диапазон по исходному created_at, потому что он универсальнее.


Generated column как альтернатива выражению

Иногда вместо функционального индекса удобно сделать отдельную вычисляемую колонку.

Например, нормализованный email:

ALTER TABLE users
ADD COLUMN email_norm text
GENERATED ALWAYS AS (lower(trim(email))) STORED;

И индекс по ней:

CREATE INDEX idx_users_email_norm
ON users (email_norm);

Теперь запрос простой:

SELECT id, email
FROM users
WHERE email_norm = 'ann@example.com';

Плюс такого подхода — в схеме явно видно, что у пользователя есть нормализованный email.

Минус — дополнительная колонка и дополнительный индекс.

Как обычно, выбор зависит от задачи.


MySQL: принцип тот же

В MySQL идея такая же.

Плохо:

WHERE DATE(created_at) = '2026-03-15'

Лучше:

WHERE created_at >= '2026-03-15'
  AND created_at <  '2026-03-16'

Плохо:

WHERE LOWER(email) = 'ann@example.com'

Лучше хранить email в нормализованном виде, использовать подходящую collation или создать функциональный индекс, если версия MySQL это поддерживает.

В MySQL 8.0.13+ есть функциональные индексы, например:

CREATE INDEX idx_users_lower_email
ON users ((lower(email)));

В более старых версиях часто использовали generated column:

email_norm varchar(255)
    GENERATED ALWAYS AS (lower(email)) STORED

и обычный индекс по ней.


ClickHouse: другой индекс, похожий принцип

В ClickHouse нет обычного B-tree индекса как в PostgreSQL.

Там важны:

  • PARTITION BY;
  • ORDER BY;
  • разреженный первичный индекс;
  • отсечение гранул данных.

Но общий принцип похож:

Если таблица отсортирована по created_at,
фильтр по голому created_at помогает читать меньше данных.

Хорошо:

WHERE created_at >= '2026-03-15'
  AND created_at <  '2026-03-16'

Хуже:

WHERE toDate(created_at) = '2026-03-15'

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


Частые переписывания

Дата через функцию

Плохо:

WHERE date(created_at) = '2026-03-15'

Лучше:

WHERE created_at >= '2026-03-15'
  AND created_at <  '2026-03-16'

Год через EXTRACT

Плохо:

WHERE EXTRACT(YEAR FROM created_at) = 2026

Лучше:

WHERE created_at >= '2026-01-01'
  AND created_at <  '2027-01-01'

Месяц через date_trunc

Плохо:

WHERE date_trunc('month', created_at) = '2026-03-01'

Лучше:

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

Арифметика над колонкой

Плохо:

WHERE salary * 12 > 100000

Лучше:

WHERE salary > 100000 / 12

Приведение колонки к типу

Плохо:

WHERE user_id::text = '42'

Лучше:

WHERE user_id = 42

lower над email

Плохо для обычного индекса по email:

WHERE lower(email) = 'ann@example.com'

Лучше, если email хранится нормализованным:

WHERE email = 'ann@example.com'

Или создать индекс по выражению:

CREATE INDEX idx_users_lower_email
ON users (lower(email));

Короткая шпаргалка

Плохо Лучше
date(created_at) = '2026-03-15' created_at >= '2026-03-15' AND created_at < '2026-03-16'
created_at::date = '2026-03-15' Диапазон по created_at
EXTRACT(YEAR FROM created_at) = 2026 created_at >= '2026-01-01' AND created_at < '2027-01-01'
date_trunc('month', created_at) = ... Диапазон от начала месяца до начала следующего
lower(email) = ... Нормализованная колонка, citext или индекс по lower(email)
salary * 12 > 100000 salary > 100000 / 12
user_id::text = '42' user_id = 42
name LIKE '%ann%' pg_trgm / полнотекстовый поиск / другой индекс
name LIKE 'Ann%' Может использовать B-tree, иногда нужен text_pattern_ops

Главное, что нужно запомнить

Sargable-условие — это такое условие, которое помогает базе использовать индекс для поиска, а не заставляет её вычислять выражение для каждой строки.

Главное правило:

Оставляйте индексируемую колонку «голой».

Плохо:

WHERE function(column) = value

Лучше:

WHERE column = value

или:

WHERE column >= start_value
  AND column <  end_value

Для дат почти всегда думайте диапазонами:

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

Для текста помните:

LIKE 'abc%' — хороший кандидат для обычного индекса.
LIKE '%abc%' — нужен другой подход.

Если функция действительно нужна, создавайте индекс по выражению:

CREATE INDEX idx_users_lower_email
ON users (lower(email));

Но не забывайте цену индексов: место на диске, более медленные вставки и обновления, обслуживание.

И самое важное — всегда проверяйте план:

EXPLAIN (ANALYZE, BUFFERS)
SELECT ...

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

Praktizējies ar reāliem uzdevumiem

Risini uzdevumus SQL trenažierī ar tūlītēju novērtēšanu un padomiem.

Atvērt trenažieri