Який індекс використовувати з великою кількістю повторюваних значень?


14

Зробимо кілька припущень:

У мене є таблиця, яка виглядає приблизно так:

 a | b
---+---
 a | -1
 a | 17
  ...
 a | 21
 c | 17
 c | -3
  ...
 c | 22

Факти про мій набір:

  • Розмір всієї таблиці становить ~ 10 10 рядків.

  • У мене ~ 100k рядків зі значенням aу стовпці a, подібне до інших значень (наприклад c).

  • Це означає ~ 100k різних значень у стовпці 'a'.

  • Більшість моїх запитів буде читати всі або більшість значень для даного значення в а, наприклад select sum(b) from t where a = 'c'.

  • Таблиця написана таким чином, що послідовні значення є фізично близькими (або вони записані в порядку, або ми вважаємо, що CLUSTERвикористовувались у цій таблиці та стовпці a).

  • Таблиця рідко, якщо коли-небудь оновлюється, нас хвилює лише швидкість читання.

  • Таблиця порівняно вузька (скажімо, ~ 25 байт на кортеж, + 23 байти накладні витрати).

Тепер питання полягає в тому, який саме індекс я повинен використовувати? Я розумію:

  • BTree Моя проблема в тому , що індекс BTree буде величезна , так як, наскільки я знаю , він буде зберігати повторювані значення (він повинен, так як він не може припустити , таблиця фізично впорядкований). Якщо BTree є величезним, мені в кінцевому підсумку потрібно прочитати і індекс, і частини таблиці, на які вказує індекс. (Ми можемо використати fillfactor = 100для зменшення розміру індексу трохи.)

  • БРІН Моє розуміння полягає в тому, що я можу мати невеликий індекс тут за рахунок читання марних сторінок. Використання малого pages_per_rangeозначає, що індекс більший (що є проблемою з BRIN, оскільки мені потрібно прочитати весь індекс), маючи великий pages_per_rangeзасіб, що я прочитаю багато непотрібних сторінок. Чи є магічна формула, щоб знайти хорошу цінність, pages_per_rangeяка враховує ці компроміси?

  • GIN / GiST Не впевнені, що вони тут є актуальними, оскільки вони в основному використовуються для повнотекстового пошуку, але я також чую, що вони добре справляються із дублюючими ключами. Чи допоможе тут GINабо GiSTіндекс?

Інше питання: чи Postgres використовуватиме той факт, що таблиця CLUSTERредагується (не передбачаючи оновлень) у планувальнику запитів (наприклад, шляхом двійкового пошуку відповідних стартових та кінцевих сторінок)? Дещо пов'язане, чи можу я просто зберігати всі свої стовпці в BTree і взагалі опускати таблицю (або досягти чогось еквівалентного, я вважаю, що це кластеризовані індекси на SQL сервері)? Чи є якийсь гібридний індекс BTree / BRIN, який би тут допоміг?

Я вважаю за краще уникати використання масивів для зберігання своїх значень, оскільки мій запит закінчиться менш читабельним таким чином (я розумію, що це зменшить вартість 23-х байтів на кортеж, зменшивши кількість кортежів).


"в основному використовується для повнотекстового пошуку" GiST досить широко використовується PostGIS.
jpmc26

Відповіді:


15

BTree

Моя проблема тут полягає в тому, що індекс BTree буде величезним, оскільки afaict він буде зберігати повторювані значення (він також є, оскільки не може припустити, що таблиця фізично відсортована). Якщо BTree є величезним, мені в кінцевому підсумку доведеться читати і індекс, і частини таблиці, на які вказує і індекс ...

Не обов'язково - наявність індексу btree, який "охоплює", буде найшвидшим часом для читання, і якщо це все, що ви хочете (тобто, якщо ви можете дозволити собі додаткове сховище), то це ваша найкраща ставка.

БРІН

Я розумію, що я можу мати невеликий покажчик тут за рахунок читання непотрібних сторінок. Використання малого pages_per_rangeозначає, що індекс більший (що є проблемою з BRIN, оскільки мені потрібно прочитати весь індекс), маючи великий pages_per_rangeзасіб, що я прочитаю багато непотрібних сторінок.

Якщо ви не можете дозволити собі витрати на зберігання індексу покриття btree, BRIN ідеально підходить для вас, оскільки у вас вже є кластеризація (це важливо, щоб BRIN був корисним). Індекси BRIN невеликі , тому всі сторінки, ймовірно, будуть в пам'яті, якщо ви виберете відповідне значення pages_per_range.

Чи є магічна формула, щоб знайти гарне значення pages_per_range, яке враховує ці компроміси?

Ніякої магічної формули, але почніть з pages_per_range дещо меншого, ніж середній розмір (на сторінках), зайнятий середнім aзначенням. Ви, напевно, намагаєтеся мінімізувати: (кількість сканованих сторінок BRIN) + (кількість сканованих сторінок) для типового запиту. Шукайте Heap Blocks: lossy=nу плані виконання pages_per_range=1та порівняйте його з іншими значеннями pages_per_range- тобто подивіться, скільки скануючих непотрібних блоків купи.

GIN / GiST

Не впевнені, що вони тут є актуальними, оскільки вони в основному використовуються для повного пошуку тексту, але я також чую, що вони добре справляються із дублюючими ключами. Чи допоможе тут GIN/ a GiSTindex?

GIN, можливо, варто розглянути, але, мабуть, не GiST - однак, якщо природне кластеринг дійсно хороший, то BRIN, ймовірно, буде кращим.

Ось зразкове порівняння між різними типами індексу для фіктивних даних, схоже на ваше:

таблиця та індекси:

create table foo(a,b,c) as
select *, lpad('',20)
from (select chr(g) a from generate_series(97,122) g) a
     cross join (select generate_series(1,100000) b) b
order by a;
create index foo_btree_covering on foo(a,b);
create index foo_btree on foo(a);
create index foo_gin on foo using gin(a);
create index foo_brin_2 on foo using brin(a) with (pages_per_range=2);
create index foo_brin_4 on foo using brin(a) with (pages_per_range=4);
vacuum analyze;

відношення розмірів:

select relname "name", pg_size_pretty(siz) "size", siz/8192 pages, (select count(*) from foo)*8192/siz "rows/page"
from( select relname, pg_relation_size(C.oid) siz
      from pg_class c join pg_namespace n on n.oid = c.relnamespace
      where nspname = current_schema ) z;
назва | розмір | сторінки | рядки / сторінки
: ----------------- | : ------ | ----: | --------:
foo | 149 МБ | 19118 | 135
foo_btree_covering | 56 МБ | 7132 | 364
foo_btree | 56 МБ | 7132 | 364
foo_gin | 2928 кБ | 366 | 7103
foo_brin_2 | 264 кБ | 33 | 78787
foo_brin_4 | 136 кБ | 17 | 152941

покриття btree:

explain analyze select sum(b) from foo where a='a';
| ПИТАННЯ ПЛАНУ |
| : ------------------------------------------------- -------------------------------------------------- ------------------------------------------- |
| Сукупність (вартість = 3282,57..3282,58 рядків = 1 ширина = 8) (фактичний час = 45.942..45.942 рядки = 1 петля = 1) |
| -> Сканувати лише з індексом за допомогою foo_btree_covering на foo (вартість = 0,43..3017,80 рядків = 105907 ширина = 4) (фактичний час = 0,038..27,286 рядків = 100000 циклів = 1) |
| Індекс Cond: (a = 'a' :: текст) |
| Купа: 0 |
| Час планування: 0.099 мс |
| Час виконання: 45,968 мс |

звичайний btree:

drop index foo_btree_covering;
explain analyze select sum(b) from foo where a='a';
| ПИТАННЯ ПЛАНУ |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Сукупна (вартість = 4064.57..4064.58 рядків = 1 ширина = 8) (фактичний час = 54.242..54.242 рядки = 1 петля = 1) |
| -> Сканування покажчика за допомогою foo_btree on foo (вартість = 0,43..3799,80 рядків = 105907 ширина = 4) (фактичний час = 0,037..33,048 рядків = 100000 петель = 1) |
| Індекс Cond: (a = 'a' :: текст) |
| Час планування: 0,135 мс |
| Час виконання: 54.280 мс |

BRIN сторінки_per_range = 4:

drop index foo_btree;
explain analyze select sum(b) from foo where a='a';
| ПИТАННЯ ПЛАНУ |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Сукупна (вартість = 21595.38..21595.39 рядів = 1 ширина = 8) (фактичний час = 52.455..52.455 рядів = 1 петля = 1) |
| -> Bitmap Heap Scan on foo (вартість = 888,78..21330,61 рядків = 105907 ширина = 4) (фактичний час = 2,738..31,967 рядків = 100000 петель = 1) |
| Перевірте умову: (a = 'a' :: текст) |
| Рядки видалені за допомогою повторної перевірки індексу: 96 |
| Купи блоків: втрата = 736 |
| -> Сканування індексу Bitmap на foo_brin_4 (вартість = 0,00..862,30 рядків = 105907 ширина = 0) (фактичний час = 2,720..2,720 рядків = 7360 циклів = 1) |
| Індекс Cond: (a = 'a' :: текст) |
| Час планування: 0,101 мс |
| Час виконання: 52.501 мс |

BRIN сторінки_per_range = 2:

drop index foo_brin_4;
explain analyze select sum(b) from foo where a='a';
| ПИТАННЯ ПЛАНУ |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Сукупна (вартість = 21659.38..21659.39 рядів = 1 ширина = 8) (фактичний час = 53.971..53.971 ряди = 1 петля = 1) |
| -> Bitmap Heap Scan on foo (вартість = 952,78..21394,61 рядків = 105907 ширина = 4) (фактичний час = 5.286..33.492 рядки = 100000 петель = 1) |
| Перевірте умову: (a = 'a' :: текст) |
| Рядки видалені за допомогою повторної перевірки індексу: 96 |
| Купи блоків: втрата = 736 |
| -> Сканування індексу растрових зображень на foo_brin_2 (вартість = 0,00..926,30 рядків = 105907 ширина = 0) (фактичний час = 5,275..5,275 рядків = 7360 циклів = 1) |
| Індекс Cond: (a = 'a' :: текст) |
| Час планування: 0,095 мс |
| Час виконання: 54.016 мс |

GIN:

drop index foo_brin_2;
explain analyze select sum(b) from foo where a='a';
| ПИТАННЯ ПЛАНУ |
| : ------------------------------------------------- -------------------------------------------------- ------------------------------ |
| Сукупна (вартість = 21687.38..21687.39 рядків = 1 ширина = 8) (фактичний час = 55.331..55.331 рядів = 1 петля = 1) |
| -> Bitmap Heap Scan on foo (вартість = 980,78..21422,61 рядків = 105907 ширина = 4) (фактичний час = 12,377..33,956 рядків = 100000 петель = 1) |
| Перевірте умову: (a = 'a' :: текст) |
| Купи блоків: точні = 736 |
| -> Сканування індексу растрових зображень на foo_gin (вартість = 0,00..954,30 рядків = 105907 ширина = 0) (фактичний час = 12,271..12,271 рядків = 100000 петель = 1) |
| Індекс Cond: (a = 'a' :: текст) |
| Час планування: 0,118 мс |
| Час виконання: 55.366 мс |

dbfiddle тут


Отже, індекс покриття пропустив би читання таблиці взагалі за рахунок місця на диску? Здається, це гарний компроміс. Я думаю, що ми маємо на увазі те саме, що і для індексу BRIN, "прочитавши весь індекс" (виправте мене, якщо я помиляюся), я мав на увазі сканування всього індексу BRIN, на який я думаю, що відбувається в dbfiddle.uk/… , ні?
foo

@foo про "(це теж, оскільки він не може припустити, що таблиця фізично відсортована)." Фізичний порядок (кластерний чи ні) таблиці не має значення. Індекс має значення в правильному порядку. Але індекси B-дерева Postgres повинні зберігати всі значення (і так, кілька разів). Ось так вони розроблені. Збереження кожного окремого значення лише один раз було б гарною особливістю / удосконаленням. Ви можете запропонувати це розробникам Postgres (і навіть допомогти в його реалізації.) Джек повинен прокоментувати, я думаю, що реалізація b-дерев Oracle це робить.
ypercubeᵀᴹ

1
@foo - ви абсолютно правильні, сканування індексу BRIN завжди сканує весь індекс ( pgcon.org/2016/schedule/attachments/… , другий останній слайд) - хоча це не показано у плані пояснення у скрипці , є це?
Джек каже, спробуйте topanswers.xyz

2
@ ypercubeᵀᴹ ви можете використовувати COMPRESS в Oracle, який зберігає кожен окремий префікс один раз на блок.
Джек каже, спробуйте topanswers.xyz

@JackDouglas Я читав Bitmap Index Scanяк означає "прочитати весь індекс брину", але, можливо, це неправильне читання. Оракул COMPRESSвиглядає як щось, що було б корисно тут, оскільки це зменшило б розмір дерева B, але я застряг з pg!
foo

6

Окрім btree і brin, які здаються найбільш розумними варіантами, ще деякі екзотичні варіанти, які, можливо, варто вивчити - вони можуть бути корисними чи не у вашому випадку:

  • INCLUDEпокажчики . Вони будуть - сподіваємось - у наступній великій версії (10) Postgres, десь близько вересня 2017 року. Індекс на (a) INCLUDE (b)тій же структурі, що й індекс, (a)але містить на листкових сторінках усі значення b(але не упорядковані). Що означає, що ви не можете використовувати його, наприклад, для SELECT * FROM t WHERE a = 'a' AND b = 2 ;. Індекс може бути використаний, але хоча (a,b)індекс знайде відповідні рядки за допомогою одного пошуку, індекс включення повинен буде пройти через (можливо, 100 К, як у вашому випадку) значення, які відповідають a = 'a'і перевіряють bзначення.
    З іншого боку, індекс дещо менш широкий, ніж (a,b)індекс, і вам не потрібно замовлення bдля запиту для обчислення SUM(b). Ви можете також мати, наприклад(a) INCLUDE (b,c,d) які можна використовувати для запитів, подібних до ваших, які збираються у всіх 3 стовпцях.

  • Фільтровані (часткові) індекси . Пропозиція, яка спочатку може здатися трохи божевільною * :

    CREATE INDEX flt_a  ON t (b) WHERE (a = 'a') ;
    ---
    CREATE INDEX flt_xy ON t (b) WHERE (a = 'xy') ;

    По одному індексу для кожного aзначення. У вашому випадку близько 100K індексів. Хоча це звучить дуже багато, врахуйте, що кожен індекс буде дуже маленьким, як за розміром (кількістю рядків), так і по ширині (оскільки він буде зберігати лише bзначення). Однак у всіх інших аспектах він (100K індекси разом) буде виконувати роль b-дерева-індексу (a,b)при використанні простору (b)індексу.
    Недоліком є ​​те, що вам доведеться створювати та підтримувати їх самостійно, кожен раз, коли aв таблицю додається нове значення . Оскільки ваша таблиця досить стабільна, без багатьох (або будь-яких) вставок / оновлень, це не здається проблемою.

  • Зведені таблиці. Оскільки таблиця досить стабільна, ви завжди можете створити та заповнити підсумкову таблицю найпоширенішими агрегатами, які вам знадобляться ( sum(b), sum(c), sum(d), avg(b), count(distinct b)тощо). Він буде невеликим (всього 100 К рядків) і його потрібно буде заповнити лише один раз та оновити лише тоді, коли рядки будуть вставлені / оновлені / видалені в головній таблиці.

*: Ідея скопійована від цієї компанії, яка працює у виробничій системі 10 мільйонів індексів: The Heap: Запуск 10 мільйонів Postgresql індексів у виробництві (і підрахунок) .


1 цікаво, але, як ви зазначаєте, стор. 10 ще не вийшло. 2 робить звук з розуму (або , по крайней мере , проти «загальної мудрості»), у мене буде читати з моменту , як ви вказуєте, що може працювати з моєї майже не пише робочий процес. 3. Не підійде для мене, я використовував SUMяк приклад, але на практиці мої запити не можуть бути попередньо обчислені (вони більше схожі на select ... from t where a = '?' and ??wjere ??було б якесь інше визначене користувачем умова.
foo

1
Ну, ми не можемо допомогти, якщо не знаємо, що ??таке;)
ypercubeᵀᴹ

Ви згадуєте відфільтровані індекси. Що з розділенням таблиці?
jpmc26

@ jpmc26 смішно, я думав додати у відповідь, що пропозиція відфільтрованих індексів у певному розумінні є формою розподілу. Тут також може бути корисно розділення, але я не впевнений. Це призведе до отримання малих індексів / таблиць.
ypercubeᵀᴹ

2
Я очікую, що часткове покриття btree-індексів стане королем ефективності тут, оскільки дані майже ніколи не оновлюються. Навіть якщо це означає 100k індексів. Загальний розмір індексу є найменшим (за винятком індексу BRIN, але там Postgres має читати і фільтрувати купу сторінок додатково). Генерація індексу може бути автоматизована за допомогою динамічного SQL. Приклад DOтвердження у цій відповідній відповіді .
Ервін Брандстеттер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.