在一系列时间戳(一列)上优化查询


8

我正在通过Heroku使用Postgres 9.3。

我有一个表“ traffic”,其中有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' 

这是EXPLAIN ANALYZE:

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);

编辑:Ran解释(分析,详细,成本,缓冲),这些是结果:

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) ...可能会揭示更多。
Craig Ringer 2015年

这里缺少一条重要的信息:的确切表定义traffic。另外:为什么第二秒EXPLAIN显示从42秒下降到0.5秒?第一次运行的是冷缓存吗?
Erwin Brandstetter,2015年

刚刚在问题中添加了表定义。是的,42到0.5秒可能是由于缓存缓存不足引起的,但是由于更新太多,这很可能会很常见。我只是再次运行EXPLAIN ANALYZE,这一次花了56秒。我再次运行它,它下降到.4s。
Evan Appleby

可以安全地假设存在PK约束id。还有其他限制吗?我看到两列可以为NULL。每个中NULL值的百分比是多少?你得到什么呢?SELECT count(*) AS ct, count(campaign_id)/ count(*) AS camp_pct, count(dt_updated)/count(*) AS upd_pct FROM traffic;
Erwin Brandstetter 2015年

是的,ID具有PK约束,而uuid_self,uuid_partner和campaign_id具有FK约束。Campaign_id为99%+ NULL,而dt_updated为0%NULL。
埃文·阿普比

Answers:


3

这里有两件事奇怪:

  1. 该查询从具有1M +行的表中选择300k行。对于30%(或超过5%的任何值-取决于行大小和其他因素),通常根本不用支付使用索引的费用。我们应该看到顺序扫描

    唯一的例外是仅索引扫描,我在这里看不到。如果您从中获取仅索引扫描,则建议的多列索引@Craig将是最佳选择。像您提到的那样进行大量更新,可能无法解决问题,在这种情况下,最好不要使用其他列,而只需拥有索引即可。您可以通过针对表格进行更激进的自动真空设置使其适合您。您可以调整单个表的参数。

  2. 虽然Postgres将使用索引,但我当然希望看到有这么多行的位图索引扫描而不是普通索引扫描,这对于百分比的行通常是更好的选择。一旦Postgres期望每个数据页有多个匹配项(从其在表上的统计信息判断),通常它将切换到位图索引扫描。

从这一点来看,我怀疑您的费用设置不足(可能还有表格统计信息)。相对于,您可能设置了random_page_cost和/或设置得太低。按照链接阅读手册。cpu_index_tuple_cost seq_page_cost

正如我们在评论中得出的那样,这也符合冷缓存是大因素的观察。您是否正在访问长时间没有人接触过的(部分)表,或者您正在未填充缓存的测试系统上运行(还)?
否则,您只是没有足够的RAM来在数据库中缓存大多数相关数据。因此,当数据驻留在缓存中时,随机访问比顺序访问要昂贵得多。根据实际情况,您可能必须进行调整以获得更好的查询计划。

必须提及另一个因素,以使第一个只读响应速度较慢:提示位在Postgres Wiki和以下相关问题中阅读详细信息

否则非常肿,在这种情况下,进行索引扫描将很有意义,在您引用的上一个答案中,我将参考CLUSTER / pg_repack(或者只是VACUUM FULL)调查您的VACUUM设置。这些设置对您很重要many inserts and updates every day

根据不同的UPDATE模式,还应考虑将其设置为FILLFACTOR低于100。如果您主要仅更新新添加的行,请FILLFACTER 压缩表设置较低的行,以便仅新页面保留一些更新空间。

架构图

campaign_id为99%+ NULL,dt_updated为0%NULL。

稍微调整列的顺序,以节省每行8个字节(在99%的情况下campaign_id为NULL 的情况下):

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并进行报告的问题。
Evan Appleby

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;

谢谢你的建议。我尝试了覆盖索引,DB却忽略了它,但仍然使用其他索引。您是否建议删除其他索引并仅使用覆盖索引(或者,对于需要使用覆盖索引的每种情况,仅使用多个覆盖索引)?我还在原始问题中添加了解释(分析,细化,成本,缓冲)。
Evan Appleby

奇。如果计划程序看到多个汇总,则可能不够聪明,无法选择仅索引的扫描,但是我认为可以。尝试使用成本参数(random_page_cost等)。此外,为了测试目的只有看是否set enable_indexscan = offset enable_seqscan = off然后重新运行力的仅索引扫描,如果是这样,什么从它的成本估计讲解分析的。
Craig Ringer
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.