Postgres:按日期时间优化查询

Posted

技术标签:

【中文标题】Postgres:按日期时间优化查询【英文标题】:Postgres: Optimizing querying by datetime 【发布时间】:2013-05-14 10:01:04 【问题描述】:

我有一个包含日期时间字段“updated_at”的表。我的很多查询将使用范围查询来查询此字段,例如 update_at > 某个日期的行。

我已经为 updated_at 添加了索引,但我的大多数查询仍然很慢,即使我对返回的行数有限制。

我还能做些什么来优化查询日期时间字段的查询?

【问题讨论】:

你能发布解释计划,总行数和“非常慢”的确切值吗? 请阅读***.com/tags/postgresql-performance/info(以及链接的SlowQueryQuestions wiki 页面),然后使用合适的explain analyze 结果更新您的问题并反馈。由于您使用的是查询生成器,您可能需要使用 auto_explain 或记录查询并手动重新执行它们。 请发布慢速查询的架构和类型。所表述的问题无法得到合理的回答...... 【参考方案1】:

通常数据库优化器不会选择对开放范围使用索引,例如updated_at > somedate

但是,在许多情况下,数据时间列不会超过“现在”,因此您可以通过像这样使用 between 将条件转换为 范围 来保留 > somedate 的语义:

where updated_at between somedate and current_timestamp

between 谓词更有可能导致优化器选择使用索引。


如果这种方法提高了您的查询性能,请发布。

【讨论】:

PostgreSQL 真的是这样吗?我认为优化器会通过 pg_statistics 查看相关列中的值范围,并为谓词生成估计的结果集的基数。如果最大值小于或等于 current_timestamp ,那么我认为不会有太大差异。不过,对 Henley 进行测试是很有趣的——解释计划会揭示一切。 @DavidAldridge 以我的经验,> 只是没有得到很好的优化。我也喜欢哈维发布结果。 Postgres 在有用的情况下使用> 的索引。不需要between:请参阅此处以获取示例sqlfiddle.com/#!12/e3142/3 这一切都取决于 - 像往常一样使用索引 - 使用索引的成本是否低于其他东西 > 在 Postgres 中得到了很好的优化,并在适当的时候根据表统计信息使用 BTree 索引。 @a_horse_with_no_name 不错的小提琴。我被卖了。我仍然会留下答案,直到 OP 发布结果【参考方案2】:

假设正在使用索引但性能仍然很差,我能想到的唯一补救方法是按该索引对表进行聚类:http://www.postgresql.org/docs/9.1/static/sql-cluster.html

这会将具有相同 update_at 值的行移动到物理上位于同一位置,从而提高通过索引访问该表的查询的性能,尤其是对于大范围扫描。

但请注意文档中的警告,并注意随着行的更新,不会保留集群。

还有:

当一个表被集群时,会在它上面获取一个 ACCESS EXCLUSIVE 锁。这可以防止任何其他数据库操作(读取和写入)在 CLUSTER 完成之前对表进行操作。

基于这些限制,它可能不是您的案例的可行解决方案,但可能对其他人有用。

【讨论】:

【参考方案3】:

对于任何给定的查询,索引的使用取决于与顺序扫描相比使用该索引的成本

开发人员经常认为,因为有索引,所以查询应该运行得更快,如果查询运行缓慢,索引就是解决方案。这通常是查询将返回几个元组的情况。但是随着结果中元组数量的增加,使用索引的成本可能会增加。

您正在使用 postgres。 Postgres 不支持围绕给定属性进行聚类。这意味着 postgres 在遇到范围查询(类型为 att > a 和 att

您可以通过运行检查此决定

EXPLAIN ANALYZE <query>; 

在 psql 中。它会告诉你它是否使用索引。

如果您真的非常想使用索引而不是顺序扫描(有时需要它)并且您真的知道自己在做什么,您可以在规划器常量中更改顺序扫描的成本或禁用顺序扫描扫描支持任何其他方法。详情见本页:

http://www.postgresql.org/docs/9.1/static/runtime-config-query.html

确保您浏览了正确版本的文档。

--dmg

【讨论】:

【参考方案4】:

我在一个有近 100 万行的表中遇到了类似的情况。

所以我在visited_at(日期时间字段)上创建了一个索引b-tree并尝试了 查询所有行:

explain analyze select mes,count(usuario) as usuarios
from (
   SELECT distinct coalesce(usuario, ip) as usuario, (extract(year from visited_at), extract(month from visited_at))   AS mes
     FROM pageview 
     ) as usuarios
group by 1
order by 1

我明白了:

GroupAggregate (cost=445468.78..451913.54 rows=200 width=64) (actual time=31027.876..31609.754 rows=8 loops=1)
-> Sort (cost=445468.78..447616.37 rows=859035 width=64) (actual time=31013.501..31439.350 rows=358514 loops=1)
Sort Key: usuarios.mes
Sort Method: external merge Disk: 24000kB
-> Subquery Scan on usuarios (cost=247740.16..263906.75 rows=859035 width=64) (actual time=23121.403..28200.175 rows=358514 loops=1)
-> Unique (cost=247740.16..255316.40 rows=859035 width=48) (actual time=23121.400..28129.538 rows=358514 loops=1)
-> Sort (cost=247740.16..250265.57 rows=1010166 width=48) (actual time=23121.399..27559.241 rows=1010702 loops=1)
Sort Key: (COALESCE(pageview.usuario, (pageview.ip)::text)), (ROW(date_part('year'::text, pageview.visited_at), date_part('month'::text, pageview.visited_at)))
Sort Method: external merge Disk: 66944kB
-> Seq Scan on pageview (cost=0.00..84842.49 rows=1010166 width=48) (actual time=0.012..1909.324 rows=1010702 loops=1)
Total runtime: 31632.012 ms

这意味着对索引之前的查询没有任何改进。

但是我将行数减少到 current_date-31

explain analyze select mes,count(usuario) as usuarios
from (
   SELECT distinct coalesce(usuario, ip) as usuario, (extract(year from visited_at), extract(month from visited_at))   AS mes
     FROM pageview 
     where visited_at > current_date - 31
     ) as usuarios
group by 1
order by 1

得到了

 -> Sort (cost=164735.62..165310.93 rows=230125 width=64) (actual time=9532.343..9602.743 rows=90871 loops=1)
Sort Key: usuarios.mes
Sort Method: external merge Disk: 5872kB
-> Subquery Scan on usuarios (cost=122598.79..126929.62 rows=230125 width=64) (actual time=7251.344..9178.901 rows=90871 loops=1)
-> Unique (cost=122598.79..124628.37 rows=230125 width=48) (actual time=7251.343..9157.837 rows=90871 loops=1)
-> Sort (cost=122598.79..123275.32 rows=270610 width=48) (actual time=7251.341..8932.541 rows=294915 loops=1)
Sort Key: (COALESCE(pageview.usuario, (pageview.ip)::text)), (ROW(date_part('year'::text, pageview.visited_at), date_part('month'::text, pageview.visited_at)))
Sort Method: external merge Disk: 18864kB
-> Bitmap Heap Scan on pageview (cost=5073.60..81528.85 rows=270610 width=48) (actual time=111.950..1877.603 rows=294915 loops=1)
Recheck Cond: (visited_at > (('now'::cstring)::date - 31))
Rows Removed by Index Recheck: 338268
-> Bitmap Index Scan on visited_at_index (cost=0.00..5005.94 rows=270610 width=0) (actual time=109.874..109.874 rows=294915 loops=1)
Index Cond: (visited_at > (('now'::cstring)::date - 31))
Total runtime: 9687.460 ms

到目前为止,我在转换日期时间方面有一个小的改进 (visited_at::date)

explain analyze select mes,count(usuario) as usuarios
from (
   SELECT distinct coalesce(usuario, ip) as usuario, (extract(year from visited_at::date), extract(month from visited_at::date))   AS mes
     FROM pageview 
     where visited_at::date > current_date - 31
     ) as usuarios
group by 1
order by 1

得到了

GroupAggregate (cost=201976.97..204126.56 rows=200 width=64) (actual time=9040.196..9102.098 rows=2 loops=1)
-> Sort (cost=201976.97..202692.83 rows=286345 width=64) (actual time=9035.624..9058.457 rows=88356 loops=1)
Sort Key: usuarios.mes
Sort Method: external merge Disk: 5704kB
-> Subquery Scan on usuarios (cost=149102.66..154491.53 rows=286345 width=64) (actual time=7511.231..8840.270 rows=88356 loops=1)
-> Unique (cost=149102.66..151628.08 rows=286345 width=48) (actual time=7511.229..8823.647 rows=88356 loops=1)
-> Sort (cost=149102.66..149944.47 rows=336722 width=48) (actual time=7511.227..8666.667 rows=287614 loops=1)
Sort Key: (COALESCE(pageview.usuario, (pageview.ip)::text)), (ROW(date_part('year'::text, ((pageview.visited_at)::date)::timestamp without time zone), date_part('month'::text, ((pageview.visited_at)::date)::timestamp without time zone)))
Sort Method: external merge Disk: 18408kB
-> Seq Scan on pageview (cost=0.00..97469.57 rows=336722 width=48) (actual time=0.018..1946.139 rows=287614 loops=1)
Filter: ((visited_at)::date > (('now'::cstring)::date - 31))
Rows Removed by Filter: 722937
Total runtime: 9108.644 ms

这是对我有用的调整:

1) 索引b-tree(主要) 2) 铸造至今(小差异)

10 秒仍然是响应用户的重要时间。

所以我的解决方案是创建表 month_users 并使用一次

insert from month_users select mes,count(usuario) as usuarios
from (
   SELECT distinct coalesce(usuario, ip) as usuario, (extract(year from visited_at), extract(month from visited_at))   AS mes
     FROM pageview 
     ) as usuarios
group by 1
order by 1

并使用

select * from month_users

结果:

Seq Scan on usuarios_mes (cost=0.00..21.30 rows=1130 width=42) (actual time=0.302..0.304 rows=8 loops=1)
Total runtime: 0.336 ms

现在是可以接受的结果!

最终的解决方案还是要考虑如何定期更新表格结果。

【讨论】:

以上是关于Postgres:按日期时间优化查询的主要内容,如果未能解决你的问题,请参考以下文章

Postgres tsrange,按日期和时间过滤

日期字段上的 Postgres DESC 索引

如何在Postgres中按年月按最大(日期)组获取行?

Laravel 按日期获取以前的记录(优化)

选择查询优化

如何按日期返回每个字段的最新行