Як пришвидшити ПОРЯДОК сортування при використанні GIN-індексу в PostgreSQL?


13

У мене така таблиця:

CREATE TABLE products (
  id serial PRIMARY KEY, 
  category_ids integer[],
  published boolean NOT NULL,
  score integer NOT NULL,
  title varchar NOT NULL);

Продукт може належати до кількох категорій. category_idsстовпець містить список ідентифікаторів усіх категорій товару.

Типовий запит виглядає приблизно так (завжди в пошуках однієї категорії):

SELECT * FROM products WHERE published
  AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title
LIMIT 20 OFFSET 8000;

Для його прискорення я використовую такий індекс:

CREATE INDEX idx_test1 ON products
  USING GIN (category_ids gin__int_ops) WHERE published;

Цей дуже допомагає, якщо в одній категорії не надто багато продуктів. Він швидко фільтрує продукти, що належать до цієї категорії, але потім відбувається сортування операцій, яке потрібно зробити важким способом (без індексу).

Встановлене btree_ginрозширення, що дозволяє мені будувати індекс GIN з декількома стовпцями так:

CREATE INDEX idx_test2 ON products USING GIN (
  category_ids gin__int_ops, score, title) WHERE published;

Але Postgres не хоче використовувати це для сортування . Навіть коли я видаляю DESCспецифікатор у запиті.

Будь-які альтернативні підходи до оптимізації завдання дуже вітаються.


Додаткова інформація:

  • PostgreSQL 9.4, з розширенням intarray
  • загальна кількість продуктів на сьогодні становить 260 тис., але очікується, що вони значно зростуть (до 10 млн., це платформа електронної комерції для багатьох орендарів)
  • продуктів у категорії 1..10000 (може вирости до 100 тис.), середній показник нижче 100, але категорії з великою кількістю продуктів, як правило, залучають набагато більше запитів

Наступний план запитів отриманий із меншої тестової системи (4680 продуктів у вибраній категорії, 200 тис. Продуктів у таблиці):

Limit  (cost=948.99..948.99 rows=1 width=72) (actual time=82.330..82.341 rows=20 loops=1)
  ->  Sort  (cost=948.37..948.99 rows=245 width=72) (actual time=80.231..81.337 rows=4020 loops=1)
        Sort Key: score, title
        Sort Method: quicksort  Memory: 928kB
        ->  Bitmap Heap Scan on products  (cost=13.90..938.65 rows=245 width=72) (actual time=1.919..16.044 rows=4680 loops=1)
              Recheck Cond: ((category_ids @> '{292844}'::integer[]) AND published)
              Heap Blocks: exact=3441
              ->  Bitmap Index Scan on idx_test2  (cost=0.00..13.84 rows=245 width=0) (actual time=1.185..1.185 rows=4680 loops=1)
                    Index Cond: (category_ids @> '{292844}'::integer[])
Planning time: 0.202 ms
Execution time: 82.404 ms

Примітка №1 : 82 мс може виглядати не так страшно, але це тому, що буфер сортування вписується в пам'ять. Після того як я вибираю всі стовпці з таблиці продуктів ( SELECT * FROM ...а в реальному житті є близько 60 стовпців), це переходить до Sort Method: external merge Disk: 5696kBподвоєння часу виконання. І це лише для 4680 продуктів.

Точка дій № 1 (походить від Примітки №1): Щоб зменшити слід пам’яті операції сортування і, отже, трохи прискорити її, було б розумним спочатку отримати, сортувати та обмежити ідентифікатори продукту, а потім отримати повні записи:

SELECT * FROM products WHERE id IN (
  SELECT id FROM products WHERE published AND category_ids @> ARRAY[23465]
  ORDER BY score DESC, title LIMIT 20 OFFSET 8000
) ORDER BY score DESC, title;

Це повертає нас до Sort Method: quicksort Memory: 903kB~ 80 мс для 4680 продуктів. Ще можна повільно, коли кількість продуктів зросте до 100 тис.


На цій сторінці: hlinnaka.iki.fi/2014/03/28/… є коментар, що btree_gin не можна використовувати для сортування.
Младен Узелак

Гаразд, я перефразував заголовок, щоб дозволити більше варіантів.
Ярослав Ставничий

Ви завжди шукаєте одну категорію? Будь ласка, надайте ще основні відомості: Постгресова версія, кардинальності, рядки для категорії (хв / сер / макс). врахуйте інструкції в інформації про теги для postgresql-продуктивності . І: scoreможе бути NULL, але ви все одно сортуєте score DESC, не score DESC NULLS LAST. Одне чи інше здається
невірним

Я додав додаткову інформацію за запитом. Я завжди шукаю одну категорію. А scoreнасправді НЕ НУЛЬНИЙ - я виправив визначення таблиці.
Ярослав Ставничий

Відповіді:


10

Я багато експериментував, і ось мої висновки.

GIN та сортування

Наразі індекс GIN (станом на версію 9.4) не може допомогти замовити .

З типів індексів, які зараз підтримуються PostgreSQL, лише B-дерево може виробляти відсортований вихід - інші типи індексів повертають відповідні рядки у не визначеному порядку, залежному від реалізації.

work_mem

Дякую Крису за вказівку на цей параметр конфігурації . Він за замовчуванням до 4 Мб, і якщо ваш набір записів більший, збільшення work_memдо належного значення (можна знайти у EXPLAIN ANALYSE) може значно прискорити операції сортування.

ALTER SYSTEM SET work_mem TO '32MB';

Перезапустіть сервер, щоб зміни набрали чинності, а потім перевірте:

SHOW work_mem;

Оригінальний запит

Я заповнив свою базу даних 650 тис. Продуктів, у деяких категоріях розміщено до 40 тис. Продуктів. Я трохи спростив запит, видаливши publishedпункт:

SELECT * FROM products WHERE category_ids @> ARRAY [248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 30000;

Limit  (cost=2435.62..2435.62 rows=1 width=1390) (actual time=1141.254..1141.256 rows=10 loops=1)
  ->  Sort  (cost=2434.00..2435.62 rows=646 width=1390) (actual time=1115.706..1140.513 rows=30010 loops=1)
        Sort Key: score, title
        Sort Method: external merge  Disk: 29656kB
        ->  Bitmap Heap Scan on products  (cost=17.01..2403.85 rows=646 width=1390) (actual time=11.831..25.646 rows=41666 loops=1)
              Recheck Cond: (category_ids @> '{248688}'::integer[])
              Heap Blocks: exact=6471
              ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=10.140..10.140 rows=41666 loops=1)
                    Index Cond: (category_ids @> '{248688}'::integer[])
Planning time: 0.288 ms
Execution time: 1146.322 ms

Як ми бачимо, цього work_memбуло недостатньоSort Method: external merge Disk: 29656kB (у нас тут кількість приблизний, йому потрібно трохи більше 32 Мб для швидкості пам'яті в пам'яті).

Зменшити слід пам’яті

Не вибирайте повні записи для сортування, використовуйте ідентифікатори, застосовуйте сортування, зміщення та обмеження, а потім завантажуйте лише 10 потрібних нам записів:

SELECT * FROM products WHERE id in (
  SELECT id FROM products WHERE category_ids @> ARRAY[248688]
  ORDER BY score DESC, title LIMIT 10 OFFSET 30000
) ORDER BY score DESC, title;

Sort  (cost=2444.10..2444.11 rows=1 width=1390) (actual time=707.861..707.862 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2436.05..2444.09 rows=1 width=1390) (actual time=707.764..707.803 rows=10 loops=1)
        ->  HashAggregate  (cost=2435.63..2435.64 rows=1 width=4) (actual time=707.744..707.746 rows=10 loops=1)
              Group Key: products_1.id
              ->  Limit  (cost=2435.62..2435.62 rows=1 width=72) (actual time=707.732..707.734 rows=10 loops=1)
                    ->  Sort  (cost=2434.00..2435.62 rows=646 width=72) (actual time=704.163..706.955 rows=30010 loops=1)
                          Sort Key: products_1.score, products_1.title
                          Sort Method: quicksort  Memory: 7396kB
                          ->  Bitmap Heap Scan on products products_1  (cost=17.01..2403.85 rows=646 width=72) (actual time=11.587..35.076 rows=41666 loops=1)
                                Recheck Cond: (category_ids @> '{248688}'::integer[])
                                Heap Blocks: exact=6471
                                ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=9.883..9.883 rows=41666 loops=1)
                                      Index Cond: (category_ids @> '{248688}'::integer[])
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
              Index Cond: (id = products_1.id)
Planning time: 0.682 ms
Execution time: 707.973 ms

Примітка Sort Method: quicksort Memory: 7396kB . Результат набагато кращий.

ПРИЄДНАЙТЕ та додатковий індекс B-дерева

Як радив Кріс, я створив додатковий індекс:

CREATE INDEX idx_test7 ON products (score DESC, title);

Спочатку я спробував приєднатися так:

SELECT * FROM products NATURAL JOIN
  (SELECT id FROM products WHERE category_ids @> ARRAY[248688]
  ORDER BY score DESC, title LIMIT 10 OFFSET 30000) c
ORDER BY score DESC, title;

План запитів дещо відрізняється, але результат той самий:

Sort  (cost=2444.10..2444.11 rows=1 width=1390) (actual time=700.747..700.747 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2436.05..2444.09 rows=1 width=1390) (actual time=700.651..700.690 rows=10 loops=1)
        ->  HashAggregate  (cost=2435.63..2435.64 rows=1 width=4) (actual time=700.630..700.630 rows=10 loops=1)
              Group Key: products_1.id
              ->  Limit  (cost=2435.62..2435.62 rows=1 width=72) (actual time=700.619..700.619 rows=10 loops=1)
                    ->  Sort  (cost=2434.00..2435.62 rows=646 width=72) (actual time=697.304..699.868 rows=30010 loops=1)
                          Sort Key: products_1.score, products_1.title
                          Sort Method: quicksort  Memory: 7396kB
                          ->  Bitmap Heap Scan on products products_1  (cost=17.01..2403.85 rows=646 width=72) (actual time=10.796..32.258 rows=41666 loops=1)
                                Recheck Cond: (category_ids @> '{248688}'::integer[])
                                Heap Blocks: exact=6471
                                ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=9.234..9.234 rows=41666 loops=1)
                                      Index Cond: (category_ids @> '{248688}'::integer[])
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
              Index Cond: (id = products_1.id)
Planning time: 1.015 ms
Execution time: 700.918 ms

Граючи з різними компенсаціями та кількістю продуктів, я не зміг змусити PostgreSQL використовувати додатковий індекс B-дерева.

Тому я пішов класичним шляхом і створив розв'язковий стіл :

CREATE TABLE prodcats AS SELECT id AS product_id, unnest(category_ids) AS category_id FROM products;
CREATE INDEX idx_prodcats_cat_prod_id ON prodcats (category_id, product_id);

SELECT p.* FROM products p JOIN prodcats c ON (p.id=c.product_id)
WHERE c.category_id=248688
ORDER BY p.score DESC, p.title LIMIT 10 OFFSET 30000;

Limit  (cost=122480.06..122480.09 rows=10 width=1390) (actual time=1290.360..1290.362 rows=10 loops=1)
  ->  Sort  (cost=122405.06..122509.00 rows=41574 width=1390) (actual time=1264.250..1289.575 rows=30010 loops=1)
        Sort Key: p.score, p.title
        Sort Method: external merge  Disk: 29656kB
        ->  Merge Join  (cost=50.46..94061.13 rows=41574 width=1390) (actual time=117.746..182.048 rows=41666 loops=1)
              Merge Cond: (p.id = c.product_id)
              ->  Index Scan using products_pkey on products p  (cost=0.42..90738.43 rows=646067 width=1390) (actual time=0.034..116.313 rows=210283 loops=1)
              ->  Index Only Scan using idx_prodcats_cat_prod_id on prodcats c  (cost=0.43..1187.98 rows=41574 width=4) (actual time=0.022..7.137 rows=41666 loops=1)
                    Index Cond: (category_id = 248688)
                    Heap Fetches: 0
Planning time: 0.873 ms
Execution time: 1294.826 ms

Не використовуючи індекс B-дерева, набір результатів не підходив work_mem, отже, погані результати.

Але за певних обставин, маючи велику кількість продуктів і невелике зміщення PostgreSQL тепер вирішує використовувати індекс B-дерева:

SELECT p.* FROM products p JOIN prodcats c ON (p.id=c.product_id)
WHERE c.category_id=248688
ORDER BY p.score DESC, p.title LIMIT 10 OFFSET 300;

Limit  (cost=3986.65..4119.51 rows=10 width=1390) (actual time=264.176..264.574 rows=10 loops=1)
  ->  Nested Loop  (cost=0.98..552334.77 rows=41574 width=1390) (actual time=250.378..264.558 rows=310 loops=1)
        ->  Index Scan using idx_test7 on products p  (cost=0.55..194665.62 rows=646067 width=1390) (actual time=0.030..83.026 rows=108037 loops=1)
        ->  Index Only Scan using idx_prodcats_cat_prod_id on prodcats c  (cost=0.43..0.54 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=108037)
              Index Cond: ((category_id = 248688) AND (product_id = p.id))
              Heap Fetches: 0
Planning time: 0.585 ms
Execution time: 264.664 ms

Це насправді цілком логічно, оскільки індекс B-дерева тут не дає прямого результату, він використовується лише як керівництво для послідовного сканування.

Порівняємо із запитом GIN:

SELECT * FROM products WHERE id in (
  SELECT id FROM products WHERE category_ids @> ARRAY[248688]
  ORDER BY score DESC, title LIMIT 10 OFFSET 300
) ORDER BY score DESC, title;

Sort  (cost=2519.53..2519.55 rows=10 width=1390) (actual time=143.809..143.809 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2435.14..2519.36 rows=10 width=1390) (actual time=143.693..143.736 rows=10 loops=1)
        ->  HashAggregate  (cost=2434.71..2434.81 rows=10 width=4) (actual time=143.678..143.680 rows=10 loops=1)
              Group Key: products_1.id
              ->  Limit  (cost=2434.56..2434.59 rows=10 width=72) (actual time=143.668..143.670 rows=10 loops=1)
                    ->  Sort  (cost=2433.81..2435.43 rows=646 width=72) (actual time=143.642..143.653 rows=310 loops=1)
                          Sort Key: products_1.score, products_1.title
                          Sort Method: top-N heapsort  Memory: 68kB
                          ->  Bitmap Heap Scan on products products_1  (cost=17.01..2403.85 rows=646 width=72) (actual time=11.625..31.868 rows=41666 loops=1)
                                Recheck Cond: (category_ids @> '{248688}'::integer[])
                                Heap Blocks: exact=6471
                                ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=9.916..9.916 rows=41666 loops=1)
                                      Index Cond: (category_ids @> '{248688}'::integer[])
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
              Index Cond: (id = products_1.id)
Planning time: 0.630 ms
Execution time: 143.921 ms

Результат GIN набагато кращий. Я перевіряв різні комбінації кількості продуктів і компенсувати, ні в якому разі підхід до таблиці стику не був кращим .

Сила реального індексу

Для того, щоб PostgreSQL повною мірою використовував індекс для сортування, всі WHEREпараметри запиту , а також ORDER BYпараметри повинні міститись в одному індексі B-дерева. Для цього я скопіював поля сортування з продукту в таблицю з'єднання:

CREATE TABLE prodcats AS SELECT id AS product_id, unnest(category_ids) AS category_id, score, title FROM products;
CREATE INDEX idx_prodcats_1 ON prodcats (category_id, score DESC, title, product_id);

SELECT * FROM products WHERE id in (SELECT product_id FROM prodcats WHERE category_id=248688 ORDER BY score DESC, title LIMIT 10 OFFSET 30000) ORDER BY score DESC, title;

Sort  (cost=2149.65..2149.67 rows=10 width=1390) (actual time=7.011..7.011 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2065.26..2149.48 rows=10 width=1390) (actual time=6.916..6.950 rows=10 loops=1)
        ->  HashAggregate  (cost=2064.83..2064.93 rows=10 width=4) (actual time=6.902..6.904 rows=10 loops=1)
              Group Key: prodcats.product_id
              ->  Limit  (cost=2064.02..2064.71 rows=10 width=74) (actual time=6.893..6.895 rows=10 loops=1)
                    ->  Index Only Scan using idx_prodcats_1 on prodcats  (cost=0.56..2860.10 rows=41574 width=74) (actual time=0.010..6.173 rows=30010 loops=1)
                          Index Cond: (category_id = 248688)
                          Heap Fetches: 0
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.003..0.003 rows=1 loops=10)
              Index Cond: (id = prodcats.product_id)
Planning time: 0.318 ms
Execution time: 7.066 ms

І це найгірший сценарій з великою кількістю продуктів у вибраній категорії та великим зміщенням. При зміщенні = 300 час виконання - всього 0,5 мс.

На жаль, підтримка такого перехідного столу вимагає додаткових зусиль. Це може бути здійснено за допомогою індексованих матеріалізованих представлень, але це корисно лише тоді, коли ваші дані оновлюються рідко, тому що оновлення такого матеріалізованого перегляду є досить важкою операцією.

Таким чином, я поки що залишаюся з індексом GIN, із збільшенням work_memзапиту пам’яті та збільшенням пам’яті.


Вам не потрібно перезапускати для зміни загальних work_memпараметрів у postgresql.conf. Перезавантаження достатньо. І дозвольте мені застерегти від встановлення work_memзанадто високих глобальних показників у багатокористувацькому середовищі (не занадто низькому). Якщо у вас є якісь - то питання , що вимагають більш work_mem, встановіть його вище для сесії тільки з SET- або тільки угоди з SET LOCAL. Дивіться: dba.stackexchange.com/a/48633/3684
Erwin Brandstetter

Яка чудова відповідь. Допомогли мені дуже багато, зокрема з операцією сортування диска -> в пам'яті, швидка зміна для чудового виграшу, дякую!
Рікардо Вілламіль

4

Ось кілька коротких порад, які можуть допомогти покращити вашу ефективність. Почну з найпростішої підказки, яка з вашої сторони лежить майже без зусиль, і перейду до більш важкої підказки після першої.

1. work_mem

Отже, я бачу прямо, що сорт, повідомлений у вашому плані пояснення Sort Method: external merge Disk: 5696kB, споживає менше 6 Мб, але розливається на диск. Вам потрібно збільшити work_memналаштування у вашому postgresql.confфайлі, щоб бути достатньо великим, щоб сорт міг вписатись у пам'ять.

EDIT: Крім того, при подальшому огляді я бачу, що після використання індексу для перевірки catgory_idsвідповідності вашим критеріям сканування індексу растрових зображень змушене стати "втраченим" і має повторно перевірити стан під час читання рядків із відповідних сторінок купи . Зверніться до цієї публікації на postgresql.org для пояснення краще, ніж я дав. : P Головне, що ваш work_memшлях занадто низький. Якщо ви не налаштували налаштування за замовчуванням на своєму сервері, це не буде добре працювати.

Це виправлення не забере у вас фактично часу. Одна зміна postgresql.conf, і ви вимкнетесь! Докладніші поради див. На цій сторінці настройки продуктивності .

2. Зміна схеми

Отже, ви прийняли рішення у своїй схемі для денормалізації category_idsцілого масиву, який потім змушує вас використовувати індекс GIN або GIST для отримання швидкого доступу. На мій досвід, ваш вибір індексу GIN буде швидшим для читання, ніж GIST, тож у такому випадку ви зробили правильний вибір. Однак GIN - це несортований індекс; думати про нього , як ключ-значення, де предикати рівності є легко перевірити, але такі операції, як WHERE >, WHERE <або ORDER BYне полегшить індексом.

Гідним підходом було б нормалізувати дизайн за допомогою мостової таблиці / перехідної таблиці , яка використовується для визначення співвідношень багато-багато-багато в базах даних.

У цьому випадку у вас є багато категорій і набір відповідних цілих category_ids, і у вас є багато продуктів та їх відповідних product_ids. Замість стовпця таблиці продукту, який є цілим масивом category_ids, видаліть цей стовпчик масиву зі своєї схеми та створіть таблицю як

CREATE TABLE join_products_categories (product_id int, category_id int);

Потім ви можете генерувати індекси B-дерева на двох стовпцях таблиці містків,

CREATE INDEX idx_products_in_join_table ON join_products_categories (product_id);
CREATE INDEX idx_products_in_join_table ON join_products_categories (category_id);

Просто моя скромна думка, але ці зміни можуть для вас дуже змінити. Спробуйте цеwork_mem змінити в першу чергу, принаймні.

Удачі!

Редагувати:

Побудуйте додатковий індекс для сприяння сортуванням

Отже, якщо з часом ваша продуктова лінійка розширюється, певні запити можуть повернути багато результатів (тисячі, десятки тисяч?), Але це може бути лише невеликим підмножиною вашої загальної лінійки продуктів. У цих випадках сортування може бути навіть досить дорогим, якщо проводити його в пам'яті, але відповідний дизайн індексу може бути використаний для надання допомоги.

Дивіться офіційну документацію PostgreSQL, що описує індекси та ЗАМОВЛЕННЯ ПО .

Якщо ви створюєте індекс, що відповідає вашим ORDER BYвимогам

CREATE INDEX idx_product_sort ON products (score DESC, title);

то Postgres оптимізує і вирішить, чи використання індексу або проведення явного сортування буде більш економічним. Майте на увазі, що гарантій немає що Postgres використовуватиме індекс; він буде прагнути оптимізувати продуктивність та вибирати між використанням індексу або явного сортування. Якщо ви створюєте цей індекс, відслідковуйте його, щоб побачити, чи достатньо він використовується для виправдання його створення, і скиньте його, якщо більшість ваших сортів виконується явно.

І все-таки, на даний момент, ваш найбільший удар для імпорту долара, ймовірно, буде використовувати більше work_mem, але є випадки, коли індекс може підтримувати сортування.


Я також думав про те, щоб скористатися розв'язною таблицею, щоб уникнути GIN. Але ви не вказали, як це допоможе в сортуванні. Я думаю, це не допоможе. Я спробував об’єднати таблицю продуктів із набором ідентифікаторів продуктів, зібраних за допомогою GIN-запиту, який, на мою думку, є досить схожим на приєднання, яке ви пропонуєте, і ця операція не могла використовувати індекс b-tree на бал та заголовок. Можливо, я побудував неправильний індекс. Чи можете ви детальніше розібратися в цьому.
Ярослав Ставничий

Вибачення, можливо, я не пояснив достатньо чітко. Зміна вашої work_memконфігурації була призначена як виправлення вашої проблеми "сортування на диску", а також проблеми перезавантаження. Зі збільшенням кількості продуктів вам може знадобитися додатковий індекс для сортування. Будь ласка, дивіться мої зміни вище для пояснення.
Кріс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.