每 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.tagindex
是 PRIMARY KEY
和 contfloattable.tagindex
其中一个FOREIGN KEY (tagindex) REFERENCES conttagtable(tagindex)
。数据来自微软访问转储,我不知道我是否可以相信 tagindex 是唯一的,因此不强制执行“唯一性”。
数据本身非常庞大。
我需要为每个conttagtable.tagid
的每15 分钟contfloattable.dateandtime
间隔从contfloattable
中获取单个任意选择的行。因此,如果给定tagid
的contfloattable
在 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
,将值收集到一个元组存储中,然后JOIN
ed 以获取标签名称。它需要很多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.tagindex
在conttagtable
中是唯一的,并且确实应该强制执行。唯一索引可用于旧的非唯一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 分钟有效地查询一个巨大的时间序列表中的一行的主要内容,如果未能解决你的问题,请参考以下文章
Pandas 将时间序列数据重新采样为 15 分钟和 45 分钟 - 使用多索引或列