Оптимизация запросов по диапазону временных отметок (один столбец)

8

Я использую Postgres 9.3 через Heroku.

У меня есть таблица «трафик» с 1M + записями, которая имеет много вставок и обновлений каждый день. Мне нужно выполнить операции SUM по всей этой таблице за разные промежутки времени, и эти вызовы могут занять до 40 секунд, и я хотел бы услышать предложения о том, как это улучшить.

У меня есть следующий индекс в этой таблице:

CREATE INDEX idx_traffic_partner_only ON traffic (dt_created) WHERE campaign_id IS NULL AND uuid_self <> uuid_partner;

Вот пример оператора SELECT:

SELECT SUM("clicks") AS clicks, SUM("impressions") AS impressions
FROM "traffic"
WHERE "uuid_self" != "uuid_partner"
AND "campaign_id" is NULL
AND "dt_created" >= 'Sun, 29 Mar 2015 00:00:00 +0000'
AND "dt_created" <= 'Mon, 27 Apr 2015 23:59:59 +0000' 

И это ОБЪЯСНИТЬ АНАЛИЗ:

Aggregate  (cost=21625.91..21625.92 rows=1 width=16) (actual time=41804.754..41804.754 rows=1 loops=1)
  ->  Index Scan using idx_traffic_partner_only on traffic  (cost=0.09..20085.11 rows=308159 width=16) (actual time=1.409..41617.976 rows=302392 loops=1)
      Index Cond: ((dt_created >= '2015-03-29'::date) AND (dt_created <= '2015-04-27'::date))
Total runtime: 41804.893 ms

http://explain.depesz.com/s/gGA

Этот вопрос очень похож на другой в SE, но тот, который использовал индекс по двум временным диапазонам столбцов, и планировщик индекса для этого запроса имел оценки, которые были далеко не верны. Основное предложение заключалось в том, чтобы создать отсортированный многостолбцовый индекс, но для одностолбечных индексов это не имеет большого эффекта. Другие предложения состояли в том, чтобы использовать индексы CLUSTER / pg_repack и GIST, но я еще не пробовал их, так как я хотел бы посмотреть, есть ли лучшее решение, использующее обычные индексы.

Оптимизация запросов по диапазону временных отметок (два столбца)

Для справки я попробовал следующие индексы, которые не использовались БД:

INDEX idx_traffic_2 ON traffic (campaign_id, uuid_self, uuid_partner, dt_created);
INDEX idx_traffic_3 ON traffic (dt_created);
INDEX idx_traffic_4 ON traffic (uuid_self);
INDEX idx_traffic_5 ON traffic (uuid_partner);

РЕДАКТИРОВАТЬ : Run EXPLAIN (АНАЛИЗ, VERBOSE, РАСХОДЫ, БУФЕРЫ), и это были результаты:

Aggregate  (cost=20538.62..20538.62 rows=1 width=8) (actual time=526.778..526.778 rows=1 loops=1)
  Output: sum(clicks), sum(impressions)
  Buffers: shared hit=47783 read=29803 dirtied=4
  I/O Timings: read=184.936
  ->  Index Scan using idx_traffic_partner_only on public.traffic  (cost=0.09..20224.74 rows=313881 width=8) (actual time=0.049..431.501 rows=302405 loops=1)
      Output: id, uuid_self, uuid_partner, impressions, clicks, dt_created... (other fields redacted)
      Index Cond: ((traffic.dt_created >= '2015-03-29'::date) AND (traffic.dt_created <= '2015-04-27'::date))
      Buffers: shared hit=47783 read=29803 dirtied=4
      I/O Timings: read=184.936
Total runtime: 526.881 ms

http://explain.depesz.com/s/7Gu6

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

CREATE TABLE traffic (
    id              serial,
    uuid_self       uuid not null,
    uuid_partner    uuid not null,
    impressions     integer NOT NULL DEFAULT 1,
    clicks          integer NOT NULL DEFAULT 0,
    campaign_id     integer,
    dt_created      DATE DEFAULT CURRENT_DATE NOT NULL,
    dt_updated      TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
)

id - это первичный ключ, а uuid_self, uuid_partner и campaign_id - все внешние ключи. Поле dt_updated обновляется функцией postgres.

Эван Эпплби
источник
explain (buffers, analyze, verbose) ...может пролить больше света.
Крейг Рингер
Здесь отсутствует одна важная часть информации: точное определение таблицы traffic. Также: почему второй EXPLAINпоказывает падение с 42 секунд до 0,5 секунд? Был ли первый запуск с холодным кешем?
Эрвин Брандстеттер
Просто добавил определение таблицы к вопросу. Да, от 42 до 0,5 сек, вероятно, были вызваны холодным кешем, но поскольку обновлений так много, это, вероятно, было бы довольно распространенным явлением. Я снова запустил EXPLAIN ANALYZE, и на этот раз потребовалось 56 секунд. Я запустил его еще раз, и он снизился до .4s.
Эван Эпплби
Можно с уверенностью предположить, что есть ограничение PK id? Любые другие ограничения? Я вижу два столбца, которые могут быть NULL. Какой процент значений NULL в каждом? Что вы получаете за это? SELECT count(*) AS ct, count(campaign_id)/ count(*) AS camp_pct, count(dt_updated)/count(*) AS upd_pct FROM traffic;
Эрвин Брандштеттер
Да, у ID есть ограничение PK, а у uuid_self, uuid_partner и campaign_id есть ограничения FK. Campaign_id равен 99% + NULL, а dt_updated равно 0% NULL.
Эван Эпплби

Ответы:

3

Две вещи, которые здесь очень странные:

  1. Запрос выбирает 300 тыс. Строк из таблицы с 1 млн. Строк. Для 30% (или более 5% - зависит от размера строки и других факторов) обычно не стоит использовать индекс вообще. Мы должны увидеть последовательное сканирование .

    Исключение составляют только сканирование по индексу, которого я здесь не вижу. Многоколонный индекс, предложенный @Craig, будет наилучшим вариантом, если вы получите из него только сканирование по индексу. Со многими обновлениями, как вы упомянули, это может не сработать, и в этом случае вам лучше без дополнительных столбцов - и только с индексом, который у вас уже есть. Возможно, вам удастся заставить его работать с более агрессивными настройками автоочистки для стола. Вы можете настроить параметры для отдельных таблиц.

  2. В то время как Postgres собирается использовать индекс, я, конечно, ожидал бы увидеть сканирование индекса растрового изображения для такого количества строк, а не простое сканирование индекса, которое обычно является лучшим выбором для небольшого процента строк. Как только Postgres ожидает нескольких обращений на страницу данных (судя по статистике в таблице), он обычно переключается на сканирование растровых индексов.

Исходя из этого, я подозреваю, что ваши настройки стоимости неадекватны (и, возможно, статистика таблицы тоже). Возможно, вы установили random_page_costи / или слишком низко относительно . Перейдите по ссылкам и прочитайте инструкцию.cpu_index_tuple_cost seq_page_cost

Это также согласуется с наблюдением, что холодный кеш является большим фактором, как мы выяснили в комментариях. Либо вы обращаетесь к (частям) таблицам, к которым никто не обращался в течение длительного времени, или вы работаете в тестовой системе, где кэш не заполнен (пока)?
В противном случае вам просто не хватает оперативной памяти для кэширования большей части соответствующих данных в вашей БД. Следовательно, произвольный доступ намного дороже, чем последовательный доступ, когда данные находятся в кеше. В зависимости от реальной ситуации вам, возможно, придется скорректировать, чтобы получить лучшие планы запросов.

Еще один фактор должен быть упомянут для медленного ответа только при первом чтении: биты подсказки . Читайте подробности в Postgres Wiki и этот связанный вопрос:

Или таблица очень раздутая , и в этом случае сканирование индекса имеет смысл, и я бы сослался наCLUSTER / pg_repackв моем предыдущем ответе, который вы цитировали. (Или простоVACUUM FULL)И изучите вашиVACUUMнастройки. Это важно сmany inserts and updates every day.

В зависимости от UPDATEшаблонов также учитывайте значение FILLFACTORниже 100. Если вы в основном обновляете только новые добавленные строки, установите меньшее значение FILLFACTER после сжатия таблицы, чтобы только на новых страницах оставалось место для покачиваний.

схема

campaign_id99% + NULL и dt_updated0% NULL.

Немного измените последовательность столбцов, чтобы сохранить 8 байтов на строку (в 99% случаев, когда campaign_idNULL):

CREATE TABLE traffic (
    uuid_self       uuid not null REFERENCES ... ,
    uuid_partner    uuid not null REFERENCES ... ,
    id              serial PRIMARY KEY,
    impressions     integer NOT NULL DEFAULT 1,
    clicks          integer NOT NULL DEFAULT 0,
    campaign_id     integer,
    dt_created      DATE DEFAULT CURRENT_DATE NOT NULL,
    dt_updated      TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
);

Подробное объяснение и ссылки на более:

Измерять:

Эрвин Брандштеттер
источник
Спасибо за предложение. В настоящее время я полагаюсь на встроенную автоматическую очистку через Heroku, и таблица трафика очищается почти каждый день. Я посмотрю больше на изменение статистики таблицы и коэффициента заполнения, использование pg_repack и отчет.
Эван Эпплби
2

Мне кажется, что вы запрашиваете много данных в большом индексе, так что это медленно. Ничего особенно плохого там нет.

Если вы используете PostgreSQL 9.3 или 9.4, вы можете попытаться проверить, можно ли получить сканирование только по индексу, превратив его в покровный индекс.

CREATE INDEX idx_traffic_partner_only 
ON traffic (dt_created, clicks, impressions)
WHERE campaign_id IS NULL 
  AND uuid_self <> uuid_partner;

PostgreSQL не имеет истинных покрывающих индексов или поддержки индексных терминов, которые являются просто значениями, а не частью b-дерева, поэтому это медленнее и дороже, чем могло бы быть с этими функциями. Это может быть победа над обычным индексным сканированием, если вакуум запускается достаточно часто, чтобы обновлять карту видимости.


В идеале PostgreSQL будет поддерживать поля вспомогательных данных в индексе, как в MS-SQL Server ( этот синтаксис НЕ РАБОТАЕТ в PostgreSQL ):

-- This will not work in PostgreSQL (at least 9.5)
-- it's an example of what I wish did work. Don't
-- comment to say it doesn't work.
--
CREATE INDEX idx_traffic_partner_only 
ON traffic (dt_created)
INCLUDING (clicks, impressions) -- auxillary data columns
WHERE campaign_id IS NULL 
  AND uuid_self <> uuid_partner;
Крейг Рингер
источник
Спасибо за предложение. Я попробовал закрывающий индекс, и БД проигнорировала его, и все еще использовал другой индекс. Вы бы предложили удалить другой индекс и использовать только индекс покрытия (или, альтернативно, использовать только несколько индексов покрытия для каждой ситуации, которая этого требует)? Я также добавил EXPLAIN (ANALYZE, VERBOSE, COSTS, BUFFERS) в исходный вопрос.
Эван Эпплби
Странный. Возможно, планировщик не достаточно умен, чтобы выбрать сканирование только по индексу, если он видит более одного агрегата, но я бы подумал, что может. Попробуйте поиграть с параметрами стоимости (и random_page_costт. Д.). Кроме того , для целей тестирования только увидеть , если set enable_indexscan = offи set enable_seqscan = offзатем повторно запустив силы индекса только сканирование, и если да, то каково его оценка затрат из анализа объясняет это.
Крейг Рингер