具有WHERE条件和GROUP BY的SQL查询的索引


15

我试图确定要用于带有WHERE条件的SQL查询的索引,GROUP BY而当前正在运行的索引非常慢。

我的查询:

SELECT group_id
FROM counter
WHERE ts between timestamp '2014-03-02 00:00:00.0' and timestamp '2014-03-05 12:00:00.0'
GROUP BY group_id

该表当前有32.000.000行。当我增加时间范围时,查询的执行时间会增加很多。

有问题的表如下所示:

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id bigint NOT NULL
);

我目前有以下索引,但是性能仍然很慢:

CREATE INDEX ts_index
  ON counter
  USING btree
  (ts);

CREATE INDEX group_id_index
  ON counter
  USING btree
  (group_id);

CREATE INDEX comp_1_index
  ON counter
  USING btree
  (ts, group_id);

CREATE INDEX comp_2_index
  ON counter
  USING btree
  (group_id, ts);

在查询上运行EXPLAIN会得到以下结果:

"QUERY PLAN"
"HashAggregate  (cost=467958.16..467958.17 rows=1 width=4)"
"  ->  Index Scan using ts_index on counter  (cost=0.56..467470.93 rows=194892 width=4)"
"        Index Cond: ((ts >= '2014-02-26 00:00:00'::timestamp without time zone) AND (ts <= '2014-02-27 23:59:00'::timestamp without time zone))"

带有示例数据的SQL Fiddle:http://sqlfiddle.com/#!15 / 7492b / 1

问题

是否可以通过添加更好的索引来提高此查询的性能,还是必须提高处理能力?

编辑1

使用PostgreSQL版本9.3.2。

编辑2

我尝试了@Erwin的建议EXISTS

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

但不幸的是,这似乎并未提高性能。查询计划:

"QUERY PLAN"
"Nested Loop Semi Join  (cost=1607.18..371680.60 rows=113 width=4)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Bitmap Heap Scan on counter c  (cost=1607.18..158895.53 rows=60641 width=4)"
"        Recheck Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        ->  Bitmap Index Scan on comp_2_index  (cost=0.00..1592.02 rows=60641 width=0)"
"              Index Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

编辑3

ypercube的LATERAL查询的查询计划:

"QUERY PLAN"
"Nested Loop  (cost=8.98..1200.42 rows=133 width=20)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Result  (cost=8.98..8.99 rows=1 width=0)"
"        One-Time Filter: ($1 IS NOT NULL)"
"        InitPlan 1 (returns $1)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan using comp_2_index on counter c  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        InitPlan 2 (returns $2)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan Backward using comp_2_index on counter c_1  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

group_id桌子上有多少个不同的值?
ypercubeᵀᴹ

有133个不同的group_id。

时间戳范围是2011年到2014年。秒和毫秒都在使用中。

您是否只对group_id而不感兴趣感兴趣?
Erwin Brandstetter 2014年

@Erwin我们在示例中未显示的第四列上也对max()和(min)感兴趣。
2014年

Answers:


6

另一个想法,也使用groups表和称为LATERALjoin 的构造(对于SQL-Server风扇,这几乎与相同OUTER APPLY)。它具有可以在子查询中计算聚合的优点:

SELECT group_id, min_ts, max_ts
FROM   groups g,                    -- notice the comma here, is required
  LATERAL 
       ( SELECT MIN(ts) AS min_ts,
                MAX(ts) AS max_ts
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2011-03-02 00:00:00'
                        AND timestamp '2013-03-05 12:00:00'
       ) x 
WHERE min_ts IS NOT NULL ;

SQL-Fiddle上的测试表明查询对索引进行了(group_id, ts)索引扫描。

使用2个横向联接生成相似的计划,一个最小联接,一个最大联接,以及两个内联相关子查询。如果您需要显示counter最小和最大日期之外的全部行,也可以使用它们:

SELECT group_id, 
       min_ts, min_ts_id, 
       max_ts, max_ts_id 
FROM   groups g
  , LATERAL 
       ( SELECT ts AS min_ts, c.id AS min_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts ASC
         LIMIT 1
       ) xmin
  , LATERAL 
       ( SELECT ts AS max_ts, c.id AS max_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts DESC 
         LIMIT 1
       ) xmax
WHERE min_ts IS NOT NULL ;

@ypercube我将您查询的查询计划添加到原始问题。即使在较大的时间跨度上,查询也会在50毫秒内运行。
uldall 2014年

5

由于选择列表中没有聚合,因此group by与将a distinct放入选择列表几乎相同,对吗?

如果那是您想要的,您可以通过将其重写为使用递归查询,从而在comp_2_index上进行快速索引查找,如PostgreSQL Wiki上所述

进行查看以有效地返回不同的group_id:

create or replace view groups as
WITH RECURSIVE t AS (
             SELECT min(counter.group_id) AS group_id
               FROM counter
    UNION ALL
             SELECT ( SELECT min(counter.group_id) AS min
                       FROM counter
                      WHERE counter.group_id > t.group_id) AS min
               FROM t
              WHERE t.group_id IS NOT NULL
    )
     SELECT t.group_id
       FROM t
      WHERE t.group_id IS NOT NULL
UNION ALL
     SELECT NULL::bigint AS col
      WHERE (EXISTS ( SELECT counter.id,
                counter.ts,
                counter.group_id
               FROM counter
              WHERE counter.group_id IS NULL));

然后使用该视图代替Erwin的exists半联接中的查找表。


4

由于只有133 different group_id's,您可以对group_id 使用integer(甚至使用smallint)。但是,它不会给您带来多少好处,因为填充到8个字节会吃掉表中的其余部分以及可能的多列索引。但是,平原的处理integer应该更快一些。更多关于int主场迎战int2

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id int NOT NULL
);

@Leo:时间戳在现代安装中存储为8字节整数,并且可以完美地快速处理。 细节。

@ypercube:上的索引(group_id, ts)无济于事,因为上没有条件group_id查询中。

您的主要问题是必须处理的海量数据:

在计数器上使用ts_index进行索引扫描(cost = 0.56..467470.93 行= 194892宽度= 4)

我看到您只对存在感兴趣group_id,而对实际计数没有兴趣。而且,只有133个不同的group_ids。因此,您的查询可以满足gorup_id时间范围内的首次点击。因此,这建议使用EXISTS半联接的替代查询:

假设有一个用于组的查找表:

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

你的指数comp_2_index(group_id, ts)现在变成工具。

SQL提琴(在注释中由@ypercube提供的提琴上构建)

在这里,查询更喜欢在上建立索引(ts, group_id),但是我认为这是因为测试设置带有“聚集”时间戳。如果您删除带前导的索引ts更多有关此内容),那么计划者也会愉快地使用索引(group_id, ts)-特别是在“ 仅索引扫描”中

如果这样可行,则您可能不需要其他可能的改进:在物化视图中预聚合数据以大大减少行数。如果您还需要实际计数,则这尤其有意义。那你要花很多在更新mv时一次行。您甚至可以合并每日和每小时汇总(两个单独的表),并使查询适应于此。

您的查询中的时间范围是否任意?还是主要是整分钟/小时/天?

CREATE MATERIALIZED VIEW counter_mv AS
SELECT date_trunc('hour', ts) AS hour
     , group_id
     , count(*) AS ct
GROUP BY 1,2
ORDER BY 1,2;

在上面创建必要的索引,counter_mv并使查询适应于此条件...


1
我在SQL-Fiddle中尝试了几项类似的操作,具有10k行,但是都显示了一些顺序扫描。使用groups表格有区别吗?
ypercubeᵀᴹ

@ypercube:我是这样认为的。同样,ANALYZE有所作为。但是,只要我介绍该表,就counter不会使用索引。要点是,没有该表,无论如何都需要一个seqscan来构建可能的group_id的集合。我在答案中添加了更多内容。谢谢你的摆弄!ANALYZEgroups
Erwin Brandstetter 2014年

真奇怪 您是说Postgres的优化程序group_id甚至不会对SELECT DISTINCT group_id FROM t;查询使用索引吗?
ypercubeᵀᴹ

1
@ErwinBrandstetter这也是我的想法,很惊讶发现其他情况。如果不使用LIMIT 1,它可以选择位图索引扫描,该扫描不会从提早停止中受益,并且需要更长的时间。(但是,如果表是全新清理的,则它可能更喜欢仅使用indexindex扫描而不是位图扫描,因此,您看到的行为取决于表的清理状态)。
jjanes 2014年

1
@uldall:每日聚合将大大减少行数。这应该够了吧。但是一定要尝试一下EXISTS查询。这可能出奇地快。至少不会在最小/最大时间内起作用。不过,如果您愿意在这里放一行,我会对产生的性能感兴趣。
Erwin Brandstetter 2014年
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.