MySql vs Postgres. Оператор LIKE
обновлено:
Вы, наверное, помните эту известную сцену из фильма “Девушка с татуировкой дракона”? Главная героиня пытается получить полицейский отчет интересующих ее пользователей. Если поставить на паузу этот фрагмент, то можно заметить, что запрос содержит использование функции SUBSTR, где имя и фамилия начинаются на ‘R’ и ‘L’ соответственно. Но так ли это оптимально с точки зрения плана запроса? Как изменится план запроса, если мы попробуем добавить не сложные индексы? Будет ли оптимальнее план, если использовать привычный и понятный оператор LIKE?

В этой статье мы попробуем сравнить планы запросов популярных баз данных: MySQL и PostgreSQL. Важно, что статья не претендует на научную публикацию, это лишь попытка смоделировать подобную ситуацию и вынести на размышление.
Чем обоснован выбор СУБД
В сцене фильма героиня подключается к MySQL, а вот PostgreSQL был выбран исключительно из частого использования на проектах.
Данные
Для тестирования будем использовать уже готовый набор имен dataset
Среда
- Процессор: Apple M4 Pro
- Общее количество ядер: 14 (10 performance and 4 efficiency)
- Память: 48 GB (LPDDR5)
- Диск: SSD 500,28 GB (AP0512Z)
Подготовка
Сначала установим СУБД. Использовать будем виртуальную среду. Для этого можно использовать docker и docker-compose как привычный инструмент разработчика.
docker-compose.yml
services:
postgres:
image: postgres:latest
restart: on-failure
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./us_people_names.txt:/opt/test_data/us_people_names.txt
environment:
TZ: UTC+4
POSTGRES_USER: root
POSTGRES_PASSWORD: root
POSTGRES_DB: operator-like
healthcheck:
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
interval: 10s
timeout: 5s
start_period: 10s
retries: 5
mysql:
image: mysql:latest
restart: on-failure
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./us_people_names.txt:/var/lib/mysql-files/us_people_names.txt
environment:
TZ: UTC+4
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: operator-like
healthcheck:
test: mysqladmin ping -u root -p $$MYSQL_ROOT_PASSWORD
interval: 10s
timeout: 5s
start_period: 10s
retries: 5
volumes:
postgres_data:
mysql_data:- docker-compose pull – загрузить все необходимые образа.
- docker-compose up -d – позволяет собрать и запустить сервисы в background режиме.
В данном compose можно увидеть, что будет создана база данных с именем operator-like.
Также можно заметить bind mount для тестовых данных, чтобы можно было выполнить в дальнейшем импорт данных в таблицу.
Маппинг портов не обязателен, вы можете подключаться к серверу СУБД внутри контейнера. Для меня привычнее использовать datagrip в качестве клиента для популярных СУБД.
На момент написания статьи использовались следующие версии СУБД в образе:
- MySQL=9.2.0-1.el9
- PostgreSQL=17.4-1.pgdg120+2
После запуска сервисов, можно посмотреть их статус, выполнив команду docker-compose ps
***@*** ~/I/r/o/operator-like> docker-compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
operator-like-mysql-1 mysql:latest "docker-entrypoint.s…" mysql 7 seconds ago Up 6 seconds (health: starting) 0.0.0.0:3306->3306/tcp, 33060/tcp
operator-like-postgres-1 postgres:latest "docker-entrypoint.s…" postgres 7 seconds ago Up 6 seconds (healthy) 0.0.0.0:5432->5432/tcpПосле подключения можно наблюдать примерно такую структуру:

Реализация теста
Для тестирования будем использовать две базы данных: MySQL и PostgreSQL. Мы создадим в каждой из них по несколько таблиц, с разными типами индексов и без них. Затем выполним запрос на выборку всех записей из этих таблиц и замерим время выполнения и размеры занимаемых данных.
Тестирование без использования индексов
Создадим таблицу PEOPLE_WITHOUT_INDEX.
MySQL
CREATE TABLE PEOPLE_WITHOUT_INDEX
(
STATE VARCHAR(2),
GENDER VARCHAR(1),
YEAR INT,
NAME VARCHAR(255),
NUMBER INT
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;PostgreSQL
CREATE TABLE PEOPLE_WITHOUT_INDEX
(
STATE VARCHAR,
GENDER VARCHAR,
YEAR VARCHAR,
NAME VARCHAR(255),
NUMBER VARCHAR
);Сделаем импорт данных из CSV (принимаем во внимание header).
MySQL
LOAD DATA INFILE '/var/lib/mysql-files/us_people_names.txt'
INTO TABLE PEOPLE_WITHOUT_INDEX
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;PostgreSQL
COPY PEOPLE_WITHOUT_INDEX FROM '/opt/test_data/us_people_names.txt'
DELIMITER ',' CSV HEADER;После импорта получим 5552452 строк в таблице.
SELECT COUNT(*) FROM PEOPLE_WITHOUT_INDEX;Посмотрим размер таблиц и индексов.
MySQL
SELECT TABLE_NAME,
ROUND(DATA_LENGTH / 1024 / 1024) AS 'Table size (MB)',
ROUND(INDEX_LENGTH / 1024 / 1024) AS 'Indexes size (MB)',
ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024) AS 'Total size (MB)'
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'operator-like';PostgreSQL
select TABLE_NAME,
ROUND(PG_TABLE_SIZE(QUOTE_IDENT(TABLE_NAME)) / 1024 / 1024) AS "Table size (MB)",
ROUND(PG_INDEXES_SIZE(QUOTE_IDENT(TABLE_NAME)) / 1024 / 1024) AS "Indexes size (MB)",
ROUND(PG_TOTAL_RELATION_SIZE(QUOTE_IDENT(TABLE_NAME)) / 1024 / 1024) AS "Total size (MB)"
from INFORMATION_SCHEMA.TABLES
where TABLE_SCHEMA = 'public';| RDBMS | TABLE_NAME | Table size (MB) | Indexes size (MB) | Total size (MB) |
|---|---|---|---|---|
| MySQL | PEOPLE_WITHOUT_INDEX | 260 | 0 | 260 |
| PostgreSQL | PEOPLE_WITHOUT_INDEX | 276 | 0 | 276 |
Теперь попробуем построить план запроса с простым условием (т.е. не содержит подзапросов и объединений) WHERE, как это делал хакер, и сразу выполним его.
Запрос содержит SUBSTR
EXPLAIN ANALYZE SELECT *
FROM PEOPLE_WITHOUT_INDEX
WHERE SUBSTR(NAME, 1, 1) = 'R';MySQL
-> Filter: (substr(PEOPLE_WITHOUT_INDEX.`NAME`,1,1) = 'R') (cost=569651 rows=5.53e+6) (actual time=0.0886..1277 rows=319986 loops=1)
-> Table scan on PEOPLE_WITHOUT_INDEX (cost=569651 rows=5.53e+6) (actual time=0.0676..1072 rows=5.55e+6 loops=1)
Execution Time: 1326 msPostgreSQL
Gather (cost=1000.00..73846.91 rows=27762 width=18) (actual time=0.273..129.770 rows=319986 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on people_without_index (cost=0.00..70070.71 rows=11568 width=18) (actual time=0.045..123.214 rows=106662 loops=3)
" Filter: (substr((name)::text, 1, 1) = 'R'::text)"
Rows Removed by Filter: 1744155
Planning Time: 0.150 ms
Execution Time: 135.613 msЗаменим функцию SUBSTR на LIKE с префиксом ‘R%’
EXPLAIN ANALYZE SELECT *
FROM PEOPLE_WITHOUT_INDEX
WHERE NAME LIKE 'R%';MySQL
-> Filter: (PEOPLE_WITHOUT_INDEX.`NAME` like 'R%') (cost=569651 rows=614409) (actual time=0.0295..1225 rows=319986 loops=1)
-> Table scan on PEOPLE_WITHOUT_INDEX (cost=569651 rows=5.53e+6) (actual time=0.0233..1067 rows=5.55e+6 loops=1)
Execution Time: 1261 msPostgreSQL
Gather (cost=1000.00..97870.52 rows=325836 width=18) (actual time=0.200..94.721 rows=319986 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on people_without_index (cost=0.00..64286.92 rows=135765 width=18) (actual time=0.034..88.139 rows=106662 loops=3)
Filter: ((name)::text ~~ 'R%'::text)
Rows Removed by Filter: 1744155
Planning Time: 0.063 ms
Execution Time: 101.151 msВывод: На первый взгляд, кажется, что на таком объеме данных без индексов сильной разницы нет между SUBSTR и LIKE. Но можно заметить, что PostgreSQL лучше справился с этой задачей по фактическому времени выполнению. Так как выделил несколько “воркеров” и использовал Parallel Seq Scan. Причем, дополнительно разработчику ничего не пришлось настраивать.
Тестирование с использованием B-tree индекса
Попробуем добавить B-tree индекс для колонки NAME и сравним планы запросов. Создадим таблицу PEOPLE_WITH_BTREE_INDEX, сделаем импорт данных из файла (как мы уже делали ранее) и добавим индекс.
MySQL (индекс при описании таблицы)
CREATE TABLE PEOPLE_WITH_BTREE_INDEX
(
STATE VARCHAR(2),
GENDER VARCHAR(1),
YEAR INT,
NAME VARCHAR(255),
NUMBER INT,
KEY NAME_BTREE_IDX (NAME) USING BTREE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;Обновим информацию о таблице
ANALYZE TABLE PEOPLE_WITH_BTREE_INDEX;PostgreSQL (создание индекса)
CREATE INDEX NAME_BTREE_IDX ON PEOPLE_WITH_BTREE_INDEX USING BTREE (NAME);Для большей эффективности пересчитаем reltuples и relpages
VACUUM ANALYZE;Запрос содержит SUBSTR
EXPLAIN ANALYZE
SELECT *
FROM PEOPLE_WITH_BTREE_INDEX
WHERE SUBSTR(NAME, 1, 1) = 'R';MySQL
-> Filter: (substr(PEOPLE_WITH_BTREE_INDEX.`NAME`,1,1) = 'R') (cost=569816 rows=5.53e+6) (actual time=0.309..1264 rows=319986 loops=1)
-> Table scan on PEOPLE_WITH_BTREE_INDEX (cost=569816 rows=5.53e+6) (actual time=0.298..1060 rows=5.55e+6 loops=1)
Execution Time: 1295 msPostgreSQL
Gather (cost=1000.00..73847.12 rows=27762 width=18) (actual time=0.338..132.379 rows=319986 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on people_with_btree_index (cost=0.00..70070.93 rows=11568 width=18) (actual time=0.046..125.406 rows=106662 loops=3)
" Filter: (substr((name)::text, 1, 1) = 'R'::text)"
Rows Removed by Filter: 1744155
Planning Time: 0.194 ms
Execution Time: 138.543 msЗаменим функцию SUBSTR на LIKE с префиксом ‘R%’
EXPLAIN ANALYZE
SELECT *
FROM PEOPLE_WITH_BTREE_INDEX
WHERE NAME LIKE 'R%';MySQL
-> Filter: (PEOPLE_WITH_BTREE_INDEX.`NAME` like 'R%') (cost=569816 rows=855298) (actual time=0.215..1365 rows=319986 loops=1)
-> Table scan on PEOPLE_WITH_BTREE_INDEX (cost=569816 rows=5.53e+6) (actual time=0.207..1208 rows=5.55e+6 loops=1)
Execution Time: 1401 msPostgreSQL
Seq Scan on people_with_btree_index (cost=10000000000.00..10000104773.85 rows=323341 width=18) (actual time=17.707..202.756 rows=319986 loops=1)
Filter: ((name)::text ~~ 'R%'::text)
Rows Removed by Filter: 5232466
Planning Time: 0.091 ms
JIT:
Functions: 2
" Options: Inlining true, Optimization true, Expressions true, Deforming true"
" Timing: Generation 0.628 ms (Deform 0.219 ms), Inlining 4.446 ms, Optimization 7.710 ms, Emission 5.484 ms, Total 18.269 ms"
Execution Time: 209.402 msПопробуем зафорсить план, где будет принудительно использован индекс NAME_BTREE_IDX
MySQL
EXPLAIN ANALYZE
SELECT *
FROM PEOPLE_WITH_BTREE_INDEX FORCE INDEX (NAME_BTREE_IDX)
WHERE NAME LIKE 'R%';
---
-> Index range scan on PEOPLE_WITH_BTREE_INDEX using NAME_BTREE_IDX over ('R' <= NAME <= 'R??'), with index condition: (PEOPLE_WITH_BTREE_INDEX.`NAME` like 'R%') (cost=2.21 rows=1) (actual time=0.0753..8306 rows=319986 loops=1)
Execution Time: 8373 msPostgreSQL
SET ENABLE_SEQSCAN = false;
EXPLAIN ANALYZE
SELECT *
FROM PEOPLE_WITH_BTREE_INDEX
WHERE NAME LIKE 'R%';
---
Seq Scan on people_with_btree_index (cost=10000000000.00..10000104773.85 rows=323341 width=18) (actual time=18.625..200.180 rows=319986 loops=1)
Filter: ((name)::text ~~ 'R%'::text)
Rows Removed by Filter: 5232466
Planning Time: 0.072 ms
JIT:
Functions: 2
" Options: Inlining true, Optimization true, Expressions true, Deforming true"
" Timing: Generation 0.211 ms (Deform 0.093 ms), Inlining 4.118 ms, Optimization 9.777 ms, Emission 4.708 ms, Total 18.814 ms"
Execution Time: 206.165 msНебольшое наблюдение, если нужно только посчитать кол-во таких строк, то производительность в обоих случаях кратно возрастает (наблюдаем полное покрытие индексом)
EXPLAIN ANALYZE
SELECT COUNT(*)
FROM PEOPLE_WITH_BTREE_INDEX
WHERE NAME LIKE 'R%';MySQL
-> Aggregate: count(0) (cost=464374 rows=1) (actual time=89.1..89.1 rows=1 loops=1)
-> Filter: (PEOPLE_WITH_BTREE_INDEX.`NAME` like 'R%') (cost=267301 rows=855298) (actual time=0.0189..80.5 rows=319986 loops=1)
-> Covering index range scan on PEOPLE_WITH_BTREE_INDEX using NAME_BTREE_IDX over ('R' <= NAME <= 'R??') (cost=267301 rows=855298) (actual time=0.0168..62.3 rows=319986 loops=1)
Execution Time: 103 msPostgreSQL
Finalize Aggregate (cost=77542.90..77542.91 rows=1 width=8) (actual time=94.663..96.282 rows=1 loops=1)
-> Gather (cost=77542.69..77542.90 rows=2 width=8) (actual time=94.605..96.279 rows=3 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=76542.69..76542.70 rows=1 width=8) (actual time=84.726..84.727 rows=1 loops=3)
-> Parallel Index Only Scan using name_btree_idx on people_with_btree_index (cost=0.43..76205.88 rows=134725 width=0) (actual time=66.855..82.297 rows=106662 loops=3)
Filter: ((name)::text ~~ 'R%'::text)
Rows Removed by Filter: 1744155
Heap Fetches: 0
Planning Time: 0.091 ms
Execution Time: 96.308 msВ реальной жизни данные возможно бы запрашивались постранично, попробуем добавить пагинацию и не забываем конечно же, что данные нужно отсортировать.
Запрос получить 100 записей со смещением в 10 000.
EXPLAIN ANALYZE
SELECT *
FROM PEOPLE_WITH_BTREE_INDEX
WHERE NAME LIKE 'R%'
ORDER BY NAME
LIMIT 100 OFFSET 10000;MySQL
-> Limit/Offset: 100/10000 row(s) (cost=569816 rows=100) (actual time=212..214 rows=100 loops=1)
-> Index range scan on PEOPLE_WITH_BTREE_INDEX using NAME_BTREE_IDX over ('R' <= NAME <= 'R??'), with index condition: (PEOPLE_WITH_BTREE_INDEX.`NAME` like 'R%') (cost=569816 rows=855298) (actual time=0.0246..213 rows=10100 loops=1)
Execution Time: 236 msPostgreSQL
Limit (cost=7984.25..8064.09 rows=100 width=18) (actual time=3512.102..3512.184 rows=100 loops=1)
-> Index Scan using name_btree_idx on people_with_btree_index (cost=0.43..258150.05 rows=323341 width=18) (actual time=3503.577..3512.018 rows=10100 loops=1)
Filter: ((name)::text ~~ 'R%'::text)
Rows Removed by Filter: 4371454
Planning Time: 0.160 ms
Execution Time: 3512.221 msВывод: добавление B-tree индекса не только не улучшает ситуацию, а ухудшает ее. Планировщик не всегда отдает предпочтение Index scan, видимо по каким-то причинам предпочтение отдается Seq Scan ( статистика, ssd диск и др.).
Но если необходимо, только посчитать кол-во таких строк, то индекс кратно улучшает ситуацию. В плане можно наблюдать полное покрытие индексом.
Также если попробовать приблизить ситуацию к реальной жизни, где мы запрашиваем данные постранично, то MySQL сильно выигрывает в производительности за счет использования B-tree индекса. Дополнительно проводились тесты без индекса - результаты были значительно хуже.
Тестирование с использованием специализированных индексов
Попробуем исправить ситуацию и добавить полнотекстовый индекс для колонки NAME и также сравним планы запросов.
Для MySQL будем использовать FULLTEXT индекс.
Для PostgreSQL будем использовать GIN индекс. GIN индекс требует чтобы тип данных был векторным, для этого добавим дополнительную колонку NAME_VECTOR. Данные получим путем преобразования NAME в вектор, функция TO_TSVECTOR.
Создадим таблицы PEOPLE_WITH_TEXT_INDEX соответственно. Повторим импорт данных из файла, и добавим индексы.
Важно: в этом случае импорт данных может занять значительное время!
MySQL (индекс при описании таблицы)
CREATE TABLE PEOPLE_WITH_TEXT_INDEX
(
STATE VARCHAR(2),
GENDER VARCHAR(1),
YEAR INT,
NAME VARCHAR(255),
NUMBER INT,
FULLTEXT KEY NAME_FULLTEXT_IDX (NAME)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;PostgreSQL (добавление колонки)
ALTER TABLE PEOPLE_WITH_TEXT_INDEX
ADD COLUMN NAME_VECTOR TSVECTOR;PostgreSQL (заполняем данными)
UPDATE PEOPLE_WITH_TEXT_INDEX
SET NAME_VECTOR = TO_TSVECTOR(NAME)
WHERE NAME_VECTOR IS NULL;PostgreSQL (создаем индекс)
CREATE INDEX NAME_GIN_IDX ON PEOPLE_WITH_TEXT_INDEX USING GIN (NAME_VECTOR);Сравним планы запросов
Запрос содержит SUBSTR
EXPLAIN ANALYZE
SELECT *
FROM PEOPLE_WITH_TEXT_INDEX
WHERE SUBSTR(NAME, 1, 1) = 'R';MySQL
-> Filter: (substr(PEOPLE_WITH_TEXT_INDEX.`NAME`,1,1) = 'R') (cost=559153 rows=5.4e+6) (actual time=0.035..1458 rows=319986 loops=1)
-> Table scan on PEOPLE_WITH_TEXT_INDEX (cost=559153 rows=5.4e+6) (actual time=0.027..1254 rows=5.55e+6 loops=1)
Execution Time: 1493 msPostgreSQL
Gather (cost=1000.00..86324.73 rows=27762 width=37) (actual time=0.265..136.680 rows=319986 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on people_with_text_index (cost=0.00..82548.53 rows=11568 width=37) (actual time=0.034..123.265 rows=106662 loops=3)
" Filter: (substr((name)::text, 1, 1) = 'R'::text)"
Rows Removed by Filter: 1744155
Planning Time: 0.112 ms
Execution Time: 142.652 msЗаменим функцию SUBSTR на LIKE с префиксом ‘R%’
EXPLAIN ANALYZE
SELECT *
FROM PEOPLE_WITH_TEXT_INDEX
WHERE NAME LIKE 'R%';MySQL
-> Filter: (PEOPLE_WITH_TEXT_INDEX.`NAME` like 'R%') (cost=559153 rows=599473) (actual time=0.0275..1394 rows=319986 loops=1)
-> Table scan on PEOPLE_WITH_TEXT_INDEX (cost=559153 rows=5.4e+6) (actual time=0.0215..1239 rows=5.55e+6 loops=1)
Execution Time: 1433 msPostgreSQL
Seq Scan on people_with_text_index (cost=10000000000.00..10000117251.06 rows=349252 width=37) (actual time=17.419..215.448 rows=319986 loops=1)
Filter: ((name)::text ~~ 'R%'::text)
Rows Removed by Filter: 5232466
Planning Time: 0.092 ms
JIT:
Functions: 2
" Options: Inlining true, Optimization true, Expressions true, Deforming true"
" Timing: Generation 0.375 ms (Deform 0.215 ms), Inlining 4.066 ms, Optimization 8.088 ms, Emission 5.183 ms, Total 17.712 ms"
Execution Time: 221.606 msИспользуем возможности полнотекстового поиска, преобразуем запрос LIKE 'R%'
MySQL (используем выражения MATCH и AGAINST) https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html
EXPLAIN ANALYZE
SELECT *
FROM PEOPLE_WITH_TEXT_INDEX
WHERE MATCH(NAME) AGAINST ('R*' IN BOOLEAN MODE);
---
-> Filter: (match PEOPLE_WITH_TEXT_INDEX.`NAME` against ('R*' in boolean mode)) (cost=1.1 rows=1) (actual time=38.3..511 rows=319984 loops=1)
-> Full-text index search on PEOPLE_WITH_TEXT_INDEX using NAME_FULLTEXT_IDX (NAME = 'R*') (cost=1.1 rows=1) (actual time=38.3..500 rows=319984 loops=1)
Execution Time: 708 msPostgreSQL (используем to_tsquery) https://www.postgresql.org/docs/current/textsearch-controls.html
EXPLAIN ANALYZE
SELECT *
FROM PEOPLE_WITH_TEXT_INDEX
WHERE NAME_VECTOR @@ to_tsquery('R:*');
---
Gather (cost=3562.52..128540.80 rows=368393 width=37) (actual time=48.037..105.636 rows=319986 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Bitmap Heap Scan on people_with_text_index (cost=2562.52..90701.50 rows=153497 width=37) (actual time=36.083..87.022 rows=106662 loops=3)
Recheck Cond: (name_vector @@ to_tsquery('R:*'::text))
Heap Blocks: exact=14652
-> Bitmap Index Scan on name_gin_idx (cost=0.00..2470.42 rows=368393 width=0) (actual time=42.129..42.130 rows=319986 loops=1)
Index Cond: (name_vector @@ to_tsquery('R:*'::text))
Planning Time: 0.415 ms
JIT:
Functions: 9
" Options: Inlining false, Optimization false, Expressions true, Deforming true"
" Timing: Generation 0.935 ms (Deform 0.408 ms), Inlining 0.000 ms, Optimization 0.272 ms, Emission 3.863 ms, Total 5.070 ms"
Execution Time: 112.505 msВывод: только лишь добавление полнотекстовых индексов никак не улучшит ситуацию, даже если пытаться зафорсить планировщик (как правило предпочтение будет отдано seq scan). Такие индексы требует знаний определенного синтаксиса, а также понимание как они устроены в разных СУБД. Это позволит писать оптимальные запросы, которые дают значительный прирост производительности. В примерах выше показано как можно получить одинаковые результаты также, как если бы мы использовали оператор LIKE.
Дополнительные проверки
Важно понимать, что поиск по одному символу является едва ли селективным. На этот счет есть отдельные дискуссии: https://stackoverflow.com/a/13452528.
И были проведены дополнительные эксперименты, которые показали, что индекс никак не улучшает ситуацию:
EXPLAIN ANALYZE
SELECT *
FROM PEOPLE_WITH_TEXT_INDEX
WHERE NAME LIKE 'Loi%';Для PostgreSQL еще проводили тесты с использованием оператора ILIKE (регистронезависимый like), вкратце, результаты
значительно хуже по сравнению с LIKE
Понятно, что можно выжать максимум производительности зная как “готовить” определенную СУБД. Мы пытались понять, что можно получить имея какое-то общее представление.
Сводная таблица
Среднее время (Execution Time)
| Table | Operation | MySQL, ms | PostgreSQL, ms |
|---|---|---|---|
| PEOPLE_WITHOUT_INDEX | SUBSTR | 1326 | 135.6 |
| PEOPLE_WITHOUT_INDEX | LIKE | 1261 | 101 |
| PEOPLE_WITH_BTREE_INDEX | SUBSTR | 1295 | 138.5 |
| PEOPLE_WITH_BTREE_INDEX | LIKE | 1401 | 209.4 |
| PEOPLE_WITH_BTREE_INDEX | LIKE (force index) | 8373 | 206.1 |
| PEOPLE_WITH_BTREE_INDEX | LIKE (only count) | 103 | 96.3 |
| PEOPLE_WITH_BTREE_INDEX | LIKE (pagination) | 236 | 3512.2 |
| PEOPLE_WITH_TEXT_INDEX | SUBSTR | 1493 | 142.6 |
| PEOPLE_WITH_TEXT_INDEX | LIKE | 1433 | 221.6 |
| PEOPLE_WITH_TEXT_INDEX | LIKE (force index) | - | - |
| PEOPLE_WITH_TEXT_INDEX | LIKE (only count) | - | - |
| PEOPLE_WITH_TEXT_INDEX | LIKE (pagination) | - | - |
| PEOPLE_WITH_TEXT_INDEX | MATCH | 708 | - |
| PEOPLE_WITH_TEXT_INDEX | tsquery (vector) | - | 112.5 |
Место на диске (под таблицу и индекс)
| Table | MySQL, table Mb | PostgreSQL, table Mb | MySQL, index Mb | PostgreSQL, index Mb | MySQL, total Mb | PostgreSQL, total Mb |
|---|---|---|---|---|---|---|
| PEOPLE_WITHOUT_INDEX | 260 | 276 | 0 | 0 | 260 | 276 |
| PEOPLE_WITH_BTREE_INDEX | 260 | 276 | 186 | 38 | 446 | 314 |
| PEOPLE_WITH_TEXT_INDEX | 306 | 373 | 105 | 27 | 411 | 401 |
Дополнительно к статье
Для планировщика PostgreSQL можно использовать сервис https://explain.tensor.ru/ наглядно показывает, где имеются проблемы.

Еще есть альтернативный сервис https://explain.depesz.com/

В этом примере можно увидеть:
- # - порядковый номер
- exclusive - время на операцию
- inclusive - время на выполнение всех связанных операций (на примере видно, что для узла Gather нужно выполнить Seq Scan)
- rows x - промах планировщика (кол-во строк)