Эффективный запрос одной строки в огромной таблице временных рядов каждые 15 минут

У меня есть две таблицы, conttagtable (t) и contfloattable (cf). T имеет около 43 тыс. строк. CF имеет более 9 миллиардов долларов.

Я создал индекс для обеих таблиц в столбце tagindex в обеих таблицах. Этот столбец можно рассматривать как уникальный идентификатор для conttagtable и как внешний ключ в conttagtable для confloattable. Я не создавал явным образом PK или внешний ключ ни для одной из таблиц, связанных с другой, хотя эти данные логически связаны столбцом tagindex в обеих таблицах, как если бы conttagtable.tagindex было PRIMARY KEY, а contfloattable.tagindex было FOREIGN KEY (tagindex) REFERENCES conttagtable(tagindex). Данные были получены из дампа доступа Microsoft, и я не знал, могу ли я доверять уникальности tagindex, поэтому «уникальность» не применяется.

Сами данные чрезвычайно велики.

Мне нужно получить одну произвольно выбранную строку из contfloattable для каждого 15-минутного contfloattable.dateandtime интервала для каждого conttagtable.tagid. Таким образом, если contfloattable для данного tagid имеет 4000 выборок, охватывающих 30 минут, мне нужна выборка из диапазона 0-14 минут и выборка из диапазона 15-30 минут. Приемлем любой образец в пределах 15-минутного диапазона; 1-й, последний, случайный, любой.

В двух словах, мне нужно получать образец каждые 15 минут, но только один образец на t.tagname. Образцы прямо сейчас записываются каждые 5 секунд, а данные охватывают два года. Это проблема с большими данными, и с точки зрения sql мне это не по силам. Все решения временных интервалов, которые я пробовал при поиске в Google или поиске на SO, дали время запроса, которое настолько велико, что они непрактичны.

  • Достаточно ли моих индексов для быстрого соединения? (они выглядят так, если исключить часть временного интервала)
  • Выиграю ли я от добавления каких-либо других индексов?
  • Какой лучший/самый быстрый запрос для достижения вышеуказанных целей?

Вот SQLFiddle, содержащий схему и некоторые примеры данных: http://sqlfiddle.com/#!1/c7d2f/2

Схема:

        Table "public.conttagtable" (t)
   Column    |  Type   | Modifiers
-------------+---------+-----------
 tagname     | text    |
 tagindex    | integer |
 tagtype     | integer |
 tagdatatype | integer |
Indexes:
    "tagindex" btree (tagindex)


             Table "public.contfloattable" (CF)
   Column    |            Type             | Modifiers
-------------+-----------------------------+-----------
 dateandtime | timestamp without time zone |
 millitm     | integer                     |
 tagindex    | integer                     |
 Val         | double precision            |
 status      | text                        |
 marker      | text                        |
Indexes:
    "tagindex_contfloat" btree (tagindex)

Результат, который я хотел бы видеть, выглядит примерно так:

cf.dateandtime      |cf."Val"|cf.status|t.tagname
--------------------------------------------------
2012-11-16 00:00:02  45       S         SuperAlpha
2012-11-16 00:00:02  45       S         SuperBeta
2012-11-16 00:00:02  45       S         SuperGamma
2012-11-16 00:00:02  45       S         SuperDelta
2012-11-16 00:15:02  45       S         SuperAlpha
2012-11-16 00:15:02  45       S         SuperBeta
2012-11-16 00:15:02  45       S         SuperGamma
2012-11-16 00:15:02  45       S         SuperDelta
2012-11-16 00:30:02  45       S         SuperAlpha
2012-11-16 00:30:02  45       S         SuperBeta
2012-11-16 00:30:02  45       S         SuperGamma
2012-11-16 00:30:02  45       S         SuperDelta
2012-11-16 00:45:02  42       S         SuperAlpha

...и т.д. и т.п...

Как предложил Клодоальдо, это моя последняя попытка, есть предложения по ее ускорению?

with i as (
    select cf.tagindex, min(dateandtime) dateandtime
    from contfloattable cf
    group by
        floor(extract(epoch from dateandtime) / 60 / 15),
        cf.tagindex
)
select cf.dateandtime, cf."Val", cf.status, t.tagname
from
    contfloattable cf
    inner join
    conttagtable t on cf.tagindex = t.tagindex
    inner join
    i on i.tagindex = cf.tagindex and i.dateandtime = cf.dateandtime
order by floor(extract(epoch from cf.dateandtime) / 60 / 15), cf.tagindex

План запроса из приведенного выше: http://explain.depesz.com/s/loR


person Sneaky Wombat    schedule 16.10.2012    source источник
comment
одна строка для каждого {t.tagname, 15-минутный интервал}. Тег — это просто столбец, описывающий устройство. Устройство в данном случае представляет собой датчик, который зафиксировал значение в определенный момент времени. Меня просят предоставлять дискретное значение для каждого устройства (имя тега) каждые 15 минут.   -  person Sneaky Wombat    schedule 16.10.2012
comment
Я не очень понимаю ваши требования к запросу. В среднем на каждую conttagtable запись приходится около 200 000 contfloattable записей. Как вы сведете это к одной выходной строке для каждого conttagtable.tagname?   -  person ruakh    schedule 16.10.2012
comment
@ruakh, да, записей много. Извините за запутанное объяснение. Допустим, у меня есть 10 тэгов, и каждый тэг записывает значение каждые 5 секунд. Мне нужно только 1 из этих записанных значений, взятых с 15-минутного интервала. Итак, для значения времени 01:00:00 я увижу 10 строк. Если бы мне нужны были два значения времени, 01:00:00 и 01:15:00, я бы увидел 20 строк. Одна строка для каждого тэга для каждого 15-минутного интервала. Это более понятно?   -  person Sneaky Wombat    schedule 16.10.2012
comment
@ user1598390 У меня нет всех способов, которые я пробовал, я опубликую один из способов, которые я пробовал выше.   -  person Sneaky Wombat    schedule 16.10.2012
comment
@sneaky-wombat Пожалуйста, укажите запросы, которые вы пробовали, в вопросе и отформатируйте его.   -  person Tulains Córdova    schedule 16.10.2012
comment
также, пожалуйста, предоставьте вывод объяснения анализа   -  person Thomas Berger    schedule 16.10.2012
comment
Я добавил пример того, что я пробовал. Это 5-минутный интервал, который мне не нужен, но я подумал, что если бы я мог заставить его работать быстро, я мог бы изменить его на правильный временной интервал в 15 минут.   -  person Sneaky Wombat    schedule 16.10.2012
comment
Из 180 сэмплов на 15-минутный сегмент, какой сэмпл вы хотите использовать? Нужно ли их каким-либо образом агрегировать/объединять? Выбрать лучшее? Выбрать случайный? Выбрать 1-й или последний во временном окне?   -  person Craig Ringer    schedule 17.10.2012
comment
@CraigRinger, отличный вопрос. Мне сказали, что это не имеет значения. Макс, мин, среднее, ранд. Что бы ни.   -  person Sneaky Wombat    schedule 17.10.2012
comment
Вот тот, с которым я возился. sqlfiddle.com/#!1/c7d2f/2   -  person Sneaky Wombat    schedule 17.10.2012


Ответы (2)


Для 15-минутных интервалов:

with i as (
    select cf.tagindex, min(dateandtime) dateandtime
    from contfloattable cf
    group by
        floor(extract(epoch from dateandtime) / 60 / 15),
        cf.tagindex
)
select cf.dateandtime, cf."Val", cf.status, t.tagname
from
    contfloattable cf
    inner join
    conttagtable t on cf.tagindex = t.tagindex
    inner join
    i on i.tagindex = cf.tagindex and i.dateandtime = cf.dateandtime
order by cf.dateandtime, t.tagname

Покажите вывод объяснения для этого запроса (если он работает), чтобы мы могли попытаться оптимизировать. Вы можете опубликовать это в этом ответе.

Объяснить вывод

"Sort  (cost=15102462177.06..15263487805.24 rows=64410251271 width=57)"
"  Sort Key: cf.dateandtime, t.tagname"
"  CTE i"
"    ->  HashAggregate  (cost=49093252.56..49481978.32 rows=19436288 width=12)"
"          ->  Seq Scan on contfloattable cf  (cost=0.00..38528881.68 rows=1408582784 width=12)"
"  ->  Hash Join  (cost=270117658.06..1067549320.69 rows=64410251271 width=57)"
"        Hash Cond: (cf.tagindex = t.tagindex)"
"        ->  Merge Join  (cost=270117116.39..298434544.23 rows=1408582784 width=25)"
"              Merge Cond: ((i.tagindex = cf.tagindex) AND (i.dateandtime = cf.dateandtime))"
"              ->  Sort  (cost=2741707.02..2790297.74 rows=19436288 width=12)"
"                    Sort Key: i.tagindex, i.dateandtime"
"                    ->  CTE Scan on i  (cost=0.00..388725.76 rows=19436288 width=12)"
"              ->  Materialize  (cost=267375409.37..274418323.29 rows=1408582784 width=21)"
"                    ->  Sort  (cost=267375409.37..270896866.33 rows=1408582784 width=21)"
"                          Sort Key: cf.tagindex, cf.dateandtime"
"                          ->  Seq Scan on contfloattable cf  (cost=0.00..24443053.84 rows=1408582784 width=21)"
"        ->  Hash  (cost=335.74..335.74 rows=16474 width=44)"
"              ->  Seq Scan on conttagtable t  (cost=0.00..335.74 rows=16474 width=44)"

Похоже, вам нужен этот индекс:

create index cf_tag_datetime on contfloattable (tagindex, dateandtime)

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

Обновить

Я добавил индекс cf_tag_datetime (tagindex, dateandtime) и вот новое объяснение:

"Sort  (cost=15349296514.90..15512953953.25 rows=65462975340 width=57)"
"  Sort Key: cf.dateandtime, t.tagname"
"  CTE i"
"    ->  HashAggregate  (cost=49093252.56..49490287.76 rows=19851760 width=12)"
"          ->  Seq Scan on contfloattable cf  (cost=0.00..38528881.68 rows=1408582784 width=12)"
"  ->  Hash Join  (cost=270179293.86..1078141313.22 rows=65462975340 width=57)"
"        Hash Cond: (cf.tagindex = t.tagindex)"
"        ->  Merge Join  (cost=270178752.20..298499296.08 rows=1408582784 width=25)"
"              Merge Cond: ((i.tagindex = cf.tagindex) AND (i.dateandtime = cf.dateandtime))"
"              ->  Sort  (cost=2803342.82..2852972.22 rows=19851760 width=12)"
"                    Sort Key: i.tagindex, i.dateandtime"
"                    ->  CTE Scan on i  (cost=0.00..397035.20 rows=19851760 width=12)"
"              ->  Materialize  (cost=267375409.37..274418323.29 rows=1408582784 width=21)"
"                    ->  Sort  (cost=267375409.37..270896866.33 rows=1408582784 width=21)"
"                          Sort Key: cf.tagindex, cf.dateandtime"
"                          ->  Seq Scan on contfloattable cf  (cost=0.00..24443053.84 rows=1408582784 width=21)"
"        ->  Hash  (cost=335.74..335.74 rows=16474 width=44)"
"              ->  Seq Scan on conttagtable t  (cost=0.00..335.74 rows=16474 width=44)"

Кажется, время увеличилось :( Однако, если я удалю предложение order by (не совсем то, что мне нужно, но сработает), вот что произойдет, большое сокращение:

"Hash Join  (cost=319669581.62..1127631600.98 rows=65462975340 width=57)"
"  Hash Cond: (cf.tagindex = t.tagindex)"
"  CTE i"
"    ->  HashAggregate  (cost=49093252.56..49490287.76 rows=19851760 width=12)"
"          ->  Seq Scan on contfloattable cf  (cost=0.00..38528881.68 rows=1408582784 width=12)"
"  ->  Merge Join  (cost=270178752.20..298499296.08 rows=1408582784 width=25)"
"        Merge Cond: ((i.tagindex = cf.tagindex) AND (i.dateandtime = cf.dateandtime))"
"        ->  Sort  (cost=2803342.82..2852972.22 rows=19851760 width=12)"
"              Sort Key: i.tagindex, i.dateandtime"
"              ->  CTE Scan on i  (cost=0.00..397035.20 rows=19851760 width=12)"
"        ->  Materialize  (cost=267375409.37..274418323.29 rows=1408582784 width=21)"
"              ->  Sort  (cost=267375409.37..270896866.33 rows=1408582784 width=21)"
"                    Sort Key: cf.tagindex, cf.dateandtime"
"                    ->  Seq Scan on contfloattable cf  (cost=0.00..24443053.84 rows=1408582784 width=21)"
"  ->  Hash  (cost=335.74..335.74 rows=16474 width=44)"
"        ->  Seq Scan on conttagtable t  (cost=0.00..335.74 rows=16474 width=44)"

Я еще не пробовал этот индекс ... хотя сделаю это. ожидать.

Теперь, глядя на это снова, я думаю, что обратный индекс может быть даже лучше, поскольку его можно использовать не только в Merge Join, но и в конечном Sort:

create index cf_tag_datetime on contfloattable (dateandtime, tagindex)
person Clodoaldo Neto    schedule 16.10.2012
comment
Спасибо. Это прекрасно работает с небольшим набором данных (я взял некоторые данные из большего набора для экспериментов). Любые идеи о том, как уменьшить время запроса на нем? Я разместил новое объяснение выше. - person Sneaky Wombat; 17.10.2012
comment
@SneakyWombat Я разместил индекс, который, как я подозреваю, вам нужен. - person Clodoaldo Neto; 17.10.2012
comment
это индекс в дополнение к существующему? Я предполагаю, что да. Я начал процесс создания этого индекса, но он, вероятно, займет два часа (или больше). Ожидание, и спасибо за помощь. - person Sneaky Wombat; 17.10.2012
comment
@SneakyWombat Я подозреваю, что существующий индекс только для cf.tagindex не будет использоваться планировщиком, поскольку кардинальность этого столбца очень мала. Таким образом, вы можете проверить его падение. - person Clodoaldo Neto; 17.10.2012
comment
Основано ли это предложение индекса на этом выводе объяснения? Ключ сортировки: cf.tagindex, cf.dateandtime? Я видел это дважды там, и это довольно дорого. - person Sneaky Wombat; 17.10.2012
comment
Поможет ли вообще добавление PK в эти таблицы? Или это ненужно? - person Sneaky Wombat; 17.10.2012
comment
я вижу, вы добавили еще один индекс - просто для ясности, это в дополнение к предыдущему или вместо предыдущего предложения? - person Sneaky Wombat; 17.10.2012
comment
@SneakyWombat Вместо этого. Просто как тестовое предложение. - person Clodoaldo Neto; 17.10.2012

Вот еще одна формулировка. Мне будет очень любопытно посмотреть, как он масштабируется на полном наборе данных. Сначала создайте этот индекс:

CREATE INDEX contfloattable_tag_and_timeseg
ON contfloattable(tagindex, (floor(extract(epoch FROM dateandtime) / 60 / 15) ));

затем запустите это с таким количеством work_mem, которое вы можете себе позволить:

SELECT 
  (first_value(x) OVER (PARTITION BY x.tagindex, floor(extract(epoch FROM x.dateandtime) / 60 / 15))).*,
  (SELECT t.tagname FROM conttagtable t WHERE t.tagindex = x.tagindex) AS tagname
FROM contfloattable x ORDER BY dateandtime, tagname;

Подлый вомбат: Объясните приведенный выше sql для полного набора данных (без предлагаемого индекса): http://explain.depesz.com/s/kGo

В качестве альтернативы, здесь требуется только один последовательный проход по contfloattable со значениями, собранными в хранилище кортежей, которое затем JOIN проверяется, чтобы получить имя тега. Требуется много work_mem:

SELECT cf.dateandtime, cf.dataVal, cf.status, t.tagname
FROM 
  (
    SELECT (first_value(x) OVER (PARTITION BY x.tagindex, floor(extract(epoch FROM x.dateandtime) / 60 / 15))).*
    FROM contfloattable x
  ) cf
  INNER JOIN
  conttagtable t ON cf.tagindex = t.tagindex
ORDER BY cf.dateandtime, t.tagname;

Подлый вомбат: Объясните приведенный выше sql для полного набора данных (без предлагаемого индекса): http://explain.depesz.com/s/57q

Если это сработает, вам захочется использовать столько work_mem, сколько вы можете себе позволить. Вы не упомянули оперативную память вашей системы, но вам понадобится приличная ее часть; пытаться:

SET work_mem = '500MB';

... или больше, если у вас не менее 4 ГБ ОЗУ и 64-разрядный процессор. Опять же, мне было бы очень интересно посмотреть, как это работает с полным набором данных.

Кстати, для корректности этих запросов я бы посоветовал вам ALTER TABLE conttagtable ADD PRIMARY KEY (tagindex);, затем DROP INDEX t_tagindex;. Это займет некоторое время, так как будет создан уникальный индекс. Большинство упомянутых здесь запросов предполагают, что t.tagindex уникален в conttagtable, и это действительно следует применять. Уникальный индекс можно использовать для дополнительной оптимизации, которую не может использовать старый неуникальный индекс t_tagindex, и он дает гораздо лучшие статистические оценки.

Кроме того, при сравнении планов запросов обратите внимание, что cost не обязательно строго пропорционально реальному времени выполнения. Если оценки хорошие, то должно примерно коррелировать, но оценки и только. Иногда вы увидите, что план с высокими затратами выполняется быстрее, чем предположительно недорогой план, из-за таких вещей, как плохие оценки количества строк или оценки избирательности индекса, ограничения способности планировщика запросов выводить отношения, неожиданные корреляции или параметры стоимости, такие как random_page_cost и seq_page_cost, которые не соответствуют реальной системе.

person Craig Ringer    schedule 17.10.2012
comment
@SneakyWombat explain.depesz.com пожалуйста, избегайте раздувания вопросов/ответов подробными планами запросов. Что за двойное цитирование этих планов? - person Craig Ringer; 17.10.2012
comment
Я добавлю этот индекс. Однако это займет некоторое время :( В режиме ожидания также установлено значение work_mem 5000 МБ. Система представляет собой четырехъядерный процессор E5520 @ 2,27 ГГц с 16 ядрами. 8 ГБ оперативной памяти, рейд 5, 1 ТБ хранилища. Я знаю, что рейд 5 плохо подходит для записи, но я буду запрашивать только эту информацию, никогда не записывая новые строки. - person Sneaky Wombat; 17.10.2012
comment
@SneakyWombat Хм. 1-й оценивает 1,4 миллиона строк, 2-й оценивает 63 миллиона строк. Тем не менее, 1-й имеет в 10 раз более высокую оценку стоимости, поэтому я не слишком уверен, что будет лучше. Глядя на эти планы, мне интересно, на какой версии PostgreSQL вы работаете, и можете ли вы извлечь выгоду из сканирования только индекса 9.2 или нет; для получения тэгов в версии 9.2 следует использовать сканирование только по индексу. - person Craig Ringer; 17.10.2012
comment
извините, не знал об этом сайте. я буду использовать это в будущем. Спасибо - person Sneaky Wombat; 17.10.2012
comment
Я использую 9.1.3. нарушитель сделки? - person Sneaky Wombat; 17.10.2012
comment
@SneakyWombat RAID 5 также не очень хорош для чтения: S, и, что наиболее важно, если PostgreSQL придется разливать временные файлы на диске для сортировки, ваша производительность будет абсолютно снижена до удивительной степени. SET log_temp_files = 0 перед выполнением больших запросов (или установите его в postgresql.conf) и посмотрите, использует ли он временные файлы для сортировки. Если это так, вам может потребоваться переместить местоположение вашего временного файла в хранилище с более быстрой записью. Кроме того, 16 ядер не принесут вам никакой пользы, боюсь, все это будет работать на одном ядре. - person Craig Ringer; 17.10.2012
comment
@SneakyWombat ... и 9.1.3 в порядке, хотя 9.1.6 полностью совместима, и вам следует подумать об обновлении, поскольку оно содержит некоторые исправления ошибок. См. примечания к выпуску для версии 9.1: postgresql.org/docs/9.1/static/release. .html . Вы можете получить значительный прирост от сканирования только индекса 9.2 (michael.otacoo.com/postgresql-2/), но тогда... возможно, вы тоже этого не сделаете, и обновление вашей большой базы данных будет немного болезненным. - person Craig Ringer; 17.10.2012
comment
@SneakyWombat Кстати, быстрый тест с SET enable_seqscan = off предполагает, что индекс, упомянутый в моем посте, также должен принести пользу запросу Клодоальдо или, по крайней мере, может быть использован для него. - person Craig Ringer; 17.10.2012
comment
Пока что @Clodoaldo кажется самым быстрым, но с использованием предложенного вами сложного напольного индекса. Однако он возвращает две строки для каждого 15-минутного интервала, и я не знаю, почему. Это огромное улучшение! Очень взволнованный. - person Sneaky Wombat; 17.10.2012
comment
@SneakyWombat Если вы никогда не будете обновлять эти таблицы, просто вставьте результат запроса в сводную таблицу и выполните запрос оттуда. Не нужно беспокоиться об оптимизации. - person Clodoaldo Neto; 17.10.2012
comment
Разве оптимизации не помогают выполнять запрос быстрее? Я запускаю запрос по всему набору данных прямо сейчас, он выполняется около 35 минут. Не уверен, сколько времени это займет. Я ценю помощь! Сначала я попытался сбросить это в базу данных mysql, но это довольно быстро закончилось плохо. Примерно при 2 миллиардах записей таблица автоматически усеклась. да здравствует постгреск! - person Sneaky Wombat; 17.10.2012