Индексы для 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))"
uldall
источник
Сколько разных group_idзначений есть в таблице?
ypercubeᵀᴹ
Есть 133 разных group_id.
Временные метки варьируются от 2011 до 2014 года. Используются секунды и миллисекунды.
Вас интересует только, group_idа не в каком количестве?
Эрвин Брандштеттер,
@Erwin Нас интересуют также max () и (min) для четвертого столбца, не показанного в примере.
Ullall

Ответы:

6

Другая идея, которая также использует groupsтаблицу и конструкцию, называемую LATERALjoin (для фанатов 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 боковых объединений, одного для минимального и одного для максимального, а также с 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ᵀᴹ
источник
@ypercube Я добавил план запроса для вашего запроса в исходный вопрос. Запрос выполняется менее чем за 50 мс даже на больших промежутках времени.
Ульдол
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.

jjanes
источник
4

Поскольку есть только 133 different group_id's, вы можете использовать integer(или даже smallint) для group_id. Однако это не принесет вам большой пользы, поскольку заполнение до 8 байт съест остаток вашей таблицы и возможные многоколонные индексы. Обработка равнины integerдолжна быть немного быстрее, хотя. Больше на intпротивint2 .

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

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

@ypercube: индекс on (group_id, ts)не может помочь, так как group_idв запросе нет условия on .

Ваша главная проблема - это огромный объем данных, которые необходимо обработать:

Сканирование индекса с использованием 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 , с 10 тыс. Строк, но все показали некоторое последовательное сканирование. Имеет ли значение использование groupsтаблицы?
ypercubeᵀᴹ
@ypercube: я так думаю. Кроме того, ANALYZEимеет значение. Но индексы counterдаже привыкнут, ANALYZEкак только я представлю groupsтаблицу. Дело в том, что без этой таблицы в любом случае необходим seqscan для построения набора возможных group_id. Я добавил больше к своему ответу. И спасибо за вашу скрипку!
Эрвин Брандштеттер,
Странно. Вы говорите, что оптимизатор Postgres не будет использовать индекс group_idдаже для SELECT DISTINCT group_id FROM t;запроса?
ypercubeᵀᴹ
1
@ErwinBrandstetter Я тоже так думал, и был очень удивлен, узнав иначе. Без LIMIT 1него можно выбрать сканирование индекса растрового изображения, которое не выигрывает от ранней остановки и занимает намного больше времени. (Но если таблица только что очищена пылесосом, она может предпочесть сканирование только по индексу, а не растровое сканирование, поэтому поведение, которое вы видите, зависит от состояния вакуума в таблице).
Джанес
1
@uldall: ежедневные агрегаты значительно уменьшат количество строк. Это должно делать свое дело. Но обязательно попробуйте EXISTS-запрос. Это может быть удивительно быстро. Не будет работать на мин / макс дополнительно. Мне было бы интересно узнать результат, если бы вы были так любезны, чтобы подвести черту здесь.
Эрвин Брандштеттер,