LOWER LIKE vs iLIKE

Как сравнивается производительность следующих двух компонентов запроса?

НИЖНЯЯ ЛИКВИДАЦИЯ

... LOWER(description) LIKE '%abcde%' ...

ILIKE

... description iLIKE '%abcde%' ...

Ответ 1

Ответ зависит от многих факторов, таких как версия Postgres, кодировка и локаль - в частности, LC_COLLATE.

Голое выражение lower(description) LIKE '%abc%' обычно немного быстрее, чем description ILIKE '%abc%', и любое из них немного быстрее, чем эквивалентное регулярное выражение: description ~* 'abc'. Это важно для последовательных сканирований, где выражение должно оцениваться для каждой тестируемой строки.

Но для больших таблиц, которые вы демонстрируете в своем ответе, можно использовать индекс. Для произвольных паттернов (не только с левой привязкой) я предлагаю индекс триграммы с использованием дополнительного модуля pg_trgm. Затем мы говорим о миллисекундах вместо секунд, и разница между приведенными выше выражениями обнуляется.

Индексы GIN и GiST (с использованием классов операторов gin_trgm_ops или gist_trgm_ops) поддерживают LIKE (~~), ILIKE (~~*), ~, ~* (и некоторые другие варианты), С индексом GIN триграммы в description (обычно больше, чем GiST, но быстрее для чтения), ваш запрос будет использовать description ILIKE 'case_insensitive_pattern'.

Связанный:

Основы сопоставления с образцом в Postgres:

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

description ILIKE '%abc%'

Или с оператором регулярного выражения -insensitive (без подстановочных знаков %):

description ~* 'abc'

Индекс в (description) не поддерживает запросы в lower(description), например:

lower(description) LIKE '%abc%'

И наоборот.

С предикатами только для lower(description) индекс выражения является немного лучшим вариантом.

Во всех других случаях предпочтительнее индекс (description), поскольку он поддерживает как предикаты с учетом регистра, так и предикаты -insensitive.

Ответ 2

Согласно моим тестам (по десять каждого запроса), LOWER LIKE примерно 17% быстрее, чем iLIKE.

Объяснение

Я создал миллион строк, содержащих некоторые случайные смешанные текстовые данные:

require 'securerandom'
inserts = []
1000000.times do |i|
        inserts << "(1, 'fake', '#{SecureRandom.urlsafe_base64(64)}')"
end
sql = "insert into books (user_id, title, description) values #{inserts.join(', ')}"
ActiveRecord::Base.connection.execute(sql)

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

my_test_db=# select count(id) from books ;
  count  
---------
 1000009

(Да, у меня есть девять дополнительных строк из других тестов - не проблема.)

Пример запроса и результатов:

my_test_db=# SELECT "books".* FROM "books" WHERE "books"."published" = 'f'
my_test_db=# and (LOWER(description) LIKE '%abcde%') ;
   id    | user_id | title |                                      description                                       | published 
---------+---------+-------+----------------------------------------------------------------------------------------+------
 1232322 |       1 | fake  | 5WRGr7oCKABcdehqPKsUqV8ji61rsNGS1TX6pW5LJKrspOI_ttLNbaSyRz1BwTGQxp3OaxW7Xl6fzVpCu9y3fA | f
 1487103 |       1 | fake  | J6q0VkZ8-UlxIMZ_MFU_wsz_8MP3ZBQvkUo8-2INiDIp7yCZYoXqRyp1Lg7JyOwfsIVdpPIKNt1uLeaBCdelPQ | f
 1817819 |       1 | fake  | YubxlSkJOvmQo1hkk5pA1q2mMK6T7cOdcU3ADUKZO8s3otEAbCdEcmm72IOxiBdaXSrw20Nq2Lb383lq230wYg | f

Результаты для LOWER LIKE

my_test_db=# EXPLAIN ANALYZE SELECT "books".* FROM "books" WHERE "books"."published" = 'f' and (LOWER(description) LIKE '%abcde%') ;
                                                   QUERY PLAN                                                   
----------------------------------------------------------------------------------------------------------------
 Seq Scan on books  (cost=0.00..32420.14 rows=1600 width=117) (actual time=938.627..4114.038 rows=3 loops=1)
   Filter: ((NOT published) AND (lower(description) ~~ '%abcde%'::text))
   Rows Removed by Filter: 1000006
 Total runtime: 4114.098 ms

Результаты для iLIKE

my_test_db=# EXPLAIN ANALYZE SELECT "books".* FROM "books" WHERE "books"."published" = 'f' and (description iLIKE '%abcde%') ;
                                                   QUERY PLAN                                                   
----------------------------------------------------------------------------------------------------------------
 Seq Scan on books  (cost=0.00..29920.11 rows=100 width=117) (actual time=1147.612..4986.771 rows=3 loops=1)
   Filter: ((NOT published) AND (description ~~* '%abcde%'::text))
   Rows Removed by Filter: 1000006
 Total runtime: 4986.831 ms

Раскрытие информации о базе данных

Версия для Postgres:

my_test_db=# select version();
                                                                                 version
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 9.2.4 on x86_64-apple-darwin12.4.0, compiled by i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00), 64-bit

Настройка сортировки:

my_test_db=# select datcollate from pg_database where datname = 'my_test_db';
 datcollate  
-------------
 en_CA.UTF-8

Определение таблицы:

my_test_db=# \d books 
                                      Table "public.books"
   Column    |            Type             |                       Modifiers
-------------+-----------------------------+-------------------------------------------------------
 id          | integer                     | not null default nextval('books_id_seq'::regclass)
 user_id     | integer                     | not null
 title       | character varying(255)      | not null
 description | text                        | not null default ''::text
 published   | boolean                     | not null default false
Indexes:
    "books_pkey" PRIMARY KEY, btree (id)

Ответ 3

В моем рельсовом проекте. ILIKE почти в 10 раз быстрее, чем LOWER LIKE, я добавляю индекс GIN в столбец entities.name

> Entity.where("LOWER(name) LIKE ?", name.strip.downcase).limit(1).first
Entity Load (2443.9ms)  SELECT  "entities".* FROM "entities" WHERE (lower(name) like 'baidu') ORDER BY "entities"."id" ASC LIMIT $1  [["LIMIT", 1]]
> Entity.where("name ILIKE ?", name.strip).limit(1).first
Entity Load (285.0ms)  SELECT  "entities".* FROM "entities" WHERE (name ilike 'Baidu') ORDER BY "entities"."id" ASC LIMIT $1  [["LIMIT", 1]]
# explain analyze SELECT  "entities".* FROM "entities" WHERE (name ilike 'Baidu') ORDER BY "entities"."id" ASC LIMIT 1;
                                                                   QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=3186.03..3186.04 rows=1 width=1588) (actual time=7.812..7.812 rows=1 loops=1)
   ->  Sort  (cost=3186.03..3187.07 rows=414 width=1588) (actual time=7.811..7.811 rows=1 loops=1)
         Sort Key: id
         Sort Method: quicksort  Memory: 26kB
         ->  Bitmap Heap Scan on entities  (cost=1543.21..3183.96 rows=414 width=1588) (actual time=7.797..7.805 rows=1 loops=1)
               Recheck Cond: ((name)::text ~~* 'Baidu'::text)
               Rows Removed by Index Recheck: 6
               Heap Blocks: exact=7
               ->  Bitmap Index Scan on index_entities_on_name  (cost=0.00..1543.11 rows=414 width=0) (actual time=7.787..7.787 rows=7 loops=1)
                     Index Cond: ((name)::text ~~* 'Baidu'::text)
 Planning Time: 6.375 ms
 Execution Time: 7.874 ms
(12 rows)

Индекс GIN действительно помогает улучшить производительность ILIKE