Індекси для SQL запиту з умовою WHERE та GROUP BY


15

Я намагаюся визначити, які індекси використовувати для запиту SQL з WHEREумовою, а GROUP BYякий зараз працює дуже повільно.

Мій запит:

SELECT group_id
FROM counter
WHERE ts between timestamp '2014-03-02 00:00:00.0' and timestamp '2014-03-05 12:00:00.0'
GROUP BY group_id

Наразі таблиця має 32 000 000 рядків. Час виконання запиту значно збільшується, коли я збільшую часові рамки.

Розглянута таблиця виглядає приблизно так:

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id bigint NOT NULL
);

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

CREATE INDEX ts_index
  ON counter
  USING btree
  (ts);

CREATE INDEX group_id_index
  ON counter
  USING btree
  (group_id);

CREATE INDEX comp_1_index
  ON counter
  USING btree
  (ts, group_id);

CREATE INDEX comp_2_index
  ON counter
  USING btree
  (group_id, ts);

Запуск EXPLAIN на запиті дає такий результат:

"QUERY PLAN"
"HashAggregate  (cost=467958.16..467958.17 rows=1 width=4)"
"  ->  Index Scan using ts_index on counter  (cost=0.56..467470.93 rows=194892 width=4)"
"        Index Cond: ((ts >= '2014-02-26 00:00:00'::timestamp without time zone) AND (ts <= '2014-02-27 23:59:00'::timestamp without time zone))"

SQL Fiddle з прикладними даними: http://sqlfiddle.com/#!15/7492b/1

Питання

Чи можна покращити ефективність цього запиту шляхом додавання кращих індексів чи потрібно збільшити потужність обробки?

Редагуйте 1

Використовується PostgreSQL версії 9.3.2.

Редагувати 2

Я спробував пропозицію @Erwin з EXISTS:

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

Але, на жаль, це, схоже, не збільшило продуктивність. План запитів:

"QUERY PLAN"
"Nested Loop Semi Join  (cost=1607.18..371680.60 rows=113 width=4)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Bitmap Heap Scan on counter c  (cost=1607.18..158895.53 rows=60641 width=4)"
"        Recheck Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        ->  Bitmap Index Scan on comp_2_index  (cost=0.00..1592.02 rows=60641 width=0)"
"              Index Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

Правка 3

План запитів для запиту LATERAL від ypercube:

"QUERY PLAN"
"Nested Loop  (cost=8.98..1200.42 rows=133 width=20)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Result  (cost=8.98..8.99 rows=1 width=0)"
"        One-Time Filter: ($1 IS NOT NULL)"
"        InitPlan 1 (returns $1)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan using comp_2_index on counter c  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        InitPlan 2 (returns $2)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan Backward using comp_2_index on counter c_1  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

Скільки різних group_idзначень є на столі?
ypercubeᵀᴹ

Є 133 різних group_id.

Часові позначки складаються з 2011 по 2014 роки. Використовуються як секунди, так і мілісекунди.

Вас цікавить лише group_idні в якому разі?
Ервін Брандстеттер

@Erwin Нас цікавлять max () та (min) також на четвертому стовпчику, який не показаний у прикладі.
uldall

Відповіді:


6

Ще одна ідея, яка також використовує groupsтаблицю та конструкцію під назвоюLATERAL приєднатись (для шанувальників SQL-Server це майже ідентично OUTER APPLY). Перевага полягає в тому, що агрегати можуть бути обчислені в підзапиті:

SELECT group_id, min_ts, max_ts
FROM   groups g,                    -- notice the comma here, is required
  LATERAL 
       ( SELECT MIN(ts) AS min_ts,
                MAX(ts) AS max_ts
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2011-03-02 00:00:00'
                        AND timestamp '2013-03-05 12:00:00'
       ) x 
WHERE min_ts IS NOT NULL ;

Тест на SQL-Fiddle показує, що запит сканує індекс на(group_id, ts)індекс.

Подібні плани складаються з використанням 2 бічних з'єднань, одного для min і одного для max, а також з 2 вбудованими кореляційними підзапитами. Вони також можуть бути використані, якщо вам потрібно показати цілі counterрядки, окрім дати мінімуму та максимуму:

SELECT group_id, 
       min_ts, min_ts_id, 
       max_ts, max_ts_id 
FROM   groups g
  , LATERAL 
       ( SELECT ts AS min_ts, c.id AS min_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts ASC
         LIMIT 1
       ) xmin
  , LATERAL 
       ( SELECT ts AS max_ts, c.id AS max_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts DESC 
         LIMIT 1
       ) xmax
WHERE min_ts IS NOT NULL ;

@ypercube До оригінального питання я додав план запиту вашого запиту. Запит працює менше ніж 50 мс навіть на великих проміжках часу.
uldall

5

Оскільки у списку вибору у вас немає сукупності, то group byце майже те саме, що і додавання distinctдо списку вибору, правда?

Якщо це те, що ви хочете, ви можете отримати швидкий пошук індексу на comp_2_index, переписавши це для використання рекурсивного запиту, як описано у вікі PostgreSQL .

Зробіть перегляд для ефективного повернення різних group_ids:

create or replace view groups as
WITH RECURSIVE t AS (
             SELECT min(counter.group_id) AS group_id
               FROM counter
    UNION ALL
             SELECT ( SELECT min(counter.group_id) AS min
                       FROM counter
                      WHERE counter.group_id > t.group_id) AS min
               FROM t
              WHERE t.group_id IS NOT NULL
    )
     SELECT t.group_id
       FROM t
      WHERE t.group_id IS NOT NULL
UNION ALL
     SELECT NULL::bigint AS col
      WHERE (EXISTS ( SELECT counter.id,
                counter.ts,
                counter.group_id
               FROM counter
              WHERE counter.group_id IS NULL));

А потім використовуйте цей вид замість таблиці пошуку в напівз'єднанні Ервіна exists.


4

Оскільки їх існує лише 133 different group_id'sви, ви можете використовувати integer(або навіть smallint) для group_id. Однак це не дуже придбає вас, тому що засип до 8 байт з'їсть решту вашої таблиці та можливі багатокольорові індекси. integerХоча обробка звичайної повинна бути трохи швидшою. Детальніше про intVS.int2 .

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id int NOT NULL
);

@Leo: часові позначки зберігаються у вигляді 8-байтних цілих чисел у сучасних установках і можуть бути оброблені ідеально швидко. Деталі.

@ypercube: індекс на (group_id, ts) не може допомогти, оскільки group_idв запиті немає жодної умови .

Ваша основна проблема - це величезна кількість даних, які підлягають обробці:

Сканування покажчика за допомогою ts_index на лічильнику (вартість = 0,56..467470,93 рядки = ширина 194892 = 4)

Я бачу, що вас цікавить лише існування group_id, а фактичного підрахунку немає. Також існує лише 133 різних group_idс. Тому ваш запит може бути задоволений першим зверненням за gorup_idчасовий період. Звідси ця пропозиція щодо альтернативного запиту з EXISTSнапівз'єднанням :

Припускаючи таблицю пошуку для груп:

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

Ваш показник comp_2_indexна (group_id, ts)тепер стає важливим.

SQL Fiddle (спираючись на скрипку, надану @ypercube в коментарях)

Тут запит надає перевагу індексу (ts, group_id), але я думаю, що це пов’язано з тестовою установкою із "кластерними" часовими позначками. Якщо ви видалите індекси з провідними ts( детальніше про це ), планувальник із задоволенням також використовуватиме індекс (group_id, ts), особливо в скануванні лише для індексів .

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

Чи довільні часові рамки у ваших запитах? Або здебільшого повні хвилини / години / дні?

CREATE MATERIALIZED VIEW counter_mv AS
SELECT date_trunc('hour', ts) AS hour
     , group_id
     , count(*) AS ct
GROUP BY 1,2
ORDER BY 1,2;

Створіть необхідний індекс (и) counter_mvта адаптуйте свій запит для роботи з ним ...


1
Я спробував кілька подібних речей у SQL-Fiddle , з 10k рядками, але всі показали певну послідовність сканування. Чи має значення використання groupsтаблиці?
ypercubeᵀᴹ

@ypercube: Я так думаю. Також ANALYZEмає значення. Але індекси counterнавіть звикають, ANALYZEяк тільки я ввожуgroups таблицю. Справа в тому, що без цієї таблиці все-таки потрібен seqscan для створення набору можливих group_id´s. Я додав ще до своєї відповіді. І дякую за вашу скрипку!
Ервін Брандстеттер

Це дивно. Ви говорите, що оптимізатор Postgres не використовуватиме індекс group_idнавіть для SELECT DISTINCT group_id FROM t;запиту?
ypercubeᵀᴹ

1
@ErwinBrandstetter Про це я і думав, і дуже здивувався, дізнавшись інакше. Без A LIMIT 1він може вибрати сканування індексу растрової карти, що не виграє від раннього зупинення і займає набагато довше. (Але якщо таблиця свіжо вакуумована, вона може віддавати перевагу індексальному скануванню над сканером растрових зображень, тому поведінка, яку ви бачите, залежить від стану вакууму таблиці).
jjanes

1
@uldall: Щоденні агрегати різко зменшать кількість рядків. Це повинно зробити трюк. Але не забудьте спробувати EXISTS-запит. Це може бути напрочуд швидко. Не працюватиме мінімум / макс додатково. Мені було б цікаво в результаті цього виступу, якщо ви будете настільки люб'язні, що сюди впустіть рядок.
Ервін Брандстеттер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.