每 15 分钟有效地查询一个巨大的时间序列表中的一行

Posted

技术标签:

【中文标题】每 15 分钟有效地查询一个巨大的时间序列表中的一行【英文标题】:Efficiently querying a huge time series table for one row every 15 minutes 【发布时间】:2012-10-16 18:57:02 【问题描述】:

我有两张桌子,conttagtable (t) 和 contfloattable (cf)。 T 有大约 43k 行。 CF拥有超过90亿。

我在两个表的tagindex 列上创建了一个索引。该列可以被认为是conttagtable 的唯一标识符,以及conttagtable 的外键confloattable。我没有在与另一个表相关的任何一个表上显式创建 PK 或外键,尽管此数据在逻辑上与两个表上的 tagindex 列相关,就好像 conttagtable.tagindexPRIMARY KEYcontfloattable.tagindex 其中一个FOREIGN KEY (tagindex) REFERENCES conttagtable(tagindex)。数据来自微软访问转储,我不知道我是否可以相信 tagindex 是唯一的,因此不强制执行“唯一性”。

数据本身非常庞大。

我需要为每个conttagtable.tagid 的每15 分钟contfloattable.dateandtime 间隔从contfloattable 中获取单个任意选择的行。因此,如果给定tagidcontfloattable 在 30 分钟内有 4000 个样本,我需要一个 0-14 分钟范围内的样本和一个 15-30 分钟范围内的样本。 15 分钟范围内的任何一个样品都是可以接受的;第一个,最后一个,随机的,随便什么。

简而言之,我需要每 15 分钟获取一个样本,但每个 t.tagname 只需要一个样本。现在的样本每 5 秒记录一次,数据跨越两年。就 sql 而言,这是一个大数据问题,而且超出了我的想象。我通过谷歌搜索或搜索 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

...等等等等...

按照 Clodoaldo 的建议,这是我最近的尝试,有什么建议可以加快速度吗?

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

【问题讨论】:

每个 t.tagname,15 分钟间隔 的一行。标记名只是描述设备的列。在这种情况下,设备是在某个时间点记录值的传感器。我被要求每 15 分钟为每个设备(标记名)提供一个离散值。 我不太了解您的查询要求。每个conttagtable 记录平均有大约200,000 条contfloattable 记录。您如何将每个 conttagtable.tagname 简化为一个输出行? @ruakh,是的,有很多记录。对不起,令人困惑的解释。假设我有 10 个标记名,每个标记名每 5 秒记录一个值。我只想要这些记录值中的 1 个,从 15 分钟的间隔中提取。所以对于 01:00:00 的时间值,我会看到 10 行。如果我想要两个时间值,01:00:00 和 01:15:00,我会看到 20 行。每 15 分钟间隔为每个标记名一行。是不是更清楚了? @user1598390 我没有我尝试过的所有方法,我将发布我在上面尝试过的一种方法。 @sneaky-wombat 请将您尝试过的查询放入问题中并格式化。 【参考方案1】:

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)

【讨论】:

谢谢。这在一个小数据集上效果很好(我从较大的数据集中提取了一些数据进行实验)。关于如何减少查询时间的任何想法?我在上面发布了一个新的解释。 @SneakyWombat 我发布了我怀疑你需要的索引。 是现有索引之外的索引吗?我认为是的。我开始了该索引的创建过程,但可能需要两个小时才能完成(或更长时间)。待命,感谢您的帮助。 @SneakyWombat 我怀疑 cf.tagindex 上的现有索引将不会被规划器单独使用,因为该列的基数非常低。所以你可以测试掉它。 该索引建议是否基于解释的此输出? “排序键:cf.tagindex,cf.dateandtime”?我在那里看了两次,很贵。【参考方案2】:

这是另一种表述。我很想知道它是如何在整个数据集上扩展的。先创建这个索引:

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;

Sneaky Wombat:从上面的 sql 解释完整数据集(没有建议的索引):http://explain.depesz.com/s/kGo

或者,在这种情况下,只需要一次连续通过contfloattable,将值收集到一个元组存储中,然后JOINed 以获取标签名称。它需要很多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;

Sneaky Wombat:从上面的 sql 解释完整数据集(没有建议的索引):http://explain.depesz.com/s/57q

如果它有效,您将希望在查询中尽可能多地抛出work_mem。你还没有提到你系统的 RAM,但你会想要相当大的一块;试试:

SET work_mem = '500MB';

... 如果您有至少 4GB 的 RAM 并且在 64 位 CPU 上,则更多。同样,我真的很想看看它是如何在完整数据集上工作的。

顺便说一句,为了这些查询的正确性,我建议你先ALTER TABLE conttagtable ADD PRIMARY KEY (tagindex); 然后DROP INDEX t_tagindex;。这将需要一些时间,因为它将建立一个唯一索引。这里提到的大多数查询都假设t.tagindexconttagtable 中是唯一的,并且确实应该强制执行。唯一索引可用于旧的非唯一t_tagindex 无法进行的其他优化,并且它产生更好的统计估计。

另外,在比较查询计划时,请注意cost 不一定与实际执行时间严格成比例。如果估计是好的,那么它应该大致相关,但估计只是那个。有时,由于行数估计或索引选择性估计错误、查询规划器推断关系的能力受限、意外相关性或成本参数(如@987654338)等原因,您会看到高成本计划比所谓的低成本计划执行得更快@ 和 seq_page_cost 与真实系统不匹配。

【讨论】:

@SneakyWombat explain.depesz.com 请避免使用冗长的查询计划使问题/答案变得臃肿。这些计划的双引号是怎么回事? 我会添加这个索引。不过需要一段时间 :( 待机,work_mem 也设置为 5000MB。系统是四核 E5520 @ 2.27GHz,产生 16 个内核。8Gig 内存,raid 5,1tb 存储。我知道raid 5 不适合写入,但我只会查询这些信息,从不写新行。 @SneakyWombat 嗯。第一个估计有 140 万行,第二个估计有 6300 万行。不过,第一个的成本估计要高出 10 倍,所以我不太确定哪个会更好。看着这些计划,我想知道您正在运行哪个 PostgreSQL 版本,以及您是否能够从 9.2 的仅索引扫描中受益;它应该使用仅索引扫描来获取 9.2 中的标记名。 抱歉,不知道那个网站。我将来会使用它。谢谢:D @SneakyWombat 如果您永远不会更新这些表,那么只需将查询结果插入汇总表并从那里查询。无需担心优化。

以上是关于每 15 分钟有效地查询一个巨大的时间序列表中的一行的主要内容,如果未能解决你的问题,请参考以下文章

如何有效地过滤 SwiftUI 中的长列表?

在巨大的数据库表中有效地维护不同项目的缓存

Pandas 将时间序列数据重新采样为 15 分钟和 45 分钟 - 使用多索引或列

索引巨大的文本文件

如何使用配置单元上下文有效地查询 Spark 中的配置单元表?

BigQuery 流式插入数据可用性延迟