Postgres 正在执行顺序扫描而不是索引扫描

Cha*_*ark 10 postgresql performance index postgresql-9.4 query-performance

我有一个包含大约 1000 万行的表和一个日期字段的索引。当我尝试提取索引字段的唯一值时,即使结果集只有 26 个项目,Postgres 也会运行顺序扫描。为什么优化器会选择这个计划?我能做些什么来避免它?

从其他答案我怀疑这与查询和索引一样多。

explain select "labelDate" from pages group by "labelDate";
                              QUERY PLAN
-----------------------------------------------------------------------
 HashAggregate  (cost=524616.78..524617.04 rows=26 width=4)
   Group Key: "labelDate"
   ->  Seq Scan on pages  (cost=0.00..499082.42 rows=10213742 width=4)
(3 rows)
Run Code Online (Sandbox Code Playgroud)

表结构:

http=# \d pages
                                       Table "public.pages"
     Column      |          Type          |        Modifiers
-----------------+------------------------+----------------------------------
 pageid          | integer                | not null default nextval('...
 createDate      | integer                | not null
 archive         | character varying(16)  | not null
 label           | character varying(32)  | not null
 wptid           | character varying(64)  | not null
 wptrun          | integer                | not null
 url             | text                   |
 urlShort        | character varying(255) |
 startedDateTime | integer                |
 renderStart     | integer                |
 onContentLoaded | integer                |
 onLoad          | integer                |
 PageSpeed       | integer                |
 rank            | integer                |
 reqTotal        | integer                | not null
 reqHTML         | integer                | not null
 reqJS           | integer                | not null
 reqCSS          | integer                | not null
 reqImg          | integer                | not null
 reqFlash        | integer                | not null
 reqJSON         | integer                | not null
 reqOther        | integer                | not null
 bytesTotal      | integer                | not null
 bytesHTML       | integer                | not null
 bytesJS         | integer                | not null
 bytesCSS        | integer                | not null
 bytesHTML       | integer                | not null
 bytesJS         | integer                | not null
 bytesCSS        | integer                | not null
 bytesImg        | integer                | not null
 bytesFlash      | integer                | not null
 bytesJSON       | integer                | not null
 bytesOther      | integer                | not null
 numDomains      | integer                | not null
 labelDate       | date                   |
 TTFB            | integer                |
 reqGIF          | smallint               | not null
 reqJPG          | smallint               | not null
 reqPNG          | smallint               | not null
 reqFont         | smallint               | not null
 bytesGIF        | integer                | not null
 bytesJPG        | integer                | not null
 bytesPNG        | integer                | not null
 bytesFont       | integer                | not null
 maxageMore      | smallint               | not null
 maxage365       | smallint               | not null
 maxage30        | smallint               | not null
 maxage1         | smallint               | not null
 maxage0         | smallint               | not null
 maxageNull      | smallint               | not null
 numDomElements  | integer                | not null
 numCompressed   | smallint               | not null
 numHTTPS        | smallint               | not null
 numGlibs        | smallint               | not null
 numErrors       | smallint               | not null
 numRedirects    | smallint               | not null
 maxDomainReqs   | smallint               | not null
 bytesHTMLDoc    | integer                | not null
 maxage365       | smallint               | not null
 maxage30        | smallint               | not null
 maxage1         | smallint               | not null
 maxage0         | smallint               | not null
 maxageNull      | smallint               | not null
 numDomElements  | integer                | not null
 numCompressed   | smallint               | not null
 numHTTPS        | smallint               | not null
 numGlibs        | smallint               | not null
 numErrors       | smallint               | not null
 numRedirects    | smallint               | not null
 maxDomainReqs   | smallint               | not null
 bytesHTMLDoc    | integer                | not null
 fullyLoaded     | integer                |
 cdn             | character varying(64)  |
 SpeedIndex      | integer                |
 visualComplete  | integer                |
 gzipTotal       | integer                | not null
 gzipSavings     | integer                | not null
 siteid          | numeric                |
Indexes:
    "pages_pkey" PRIMARY KEY, btree (pageid)
    "pages_date_url" UNIQUE CONSTRAINT, btree ("urlShort", "labelDate")
    "idx_pages_cdn" btree (cdn)
    "idx_pages_labeldate" btree ("labelDate") CLUSTER
    "idx_pages_urlshort" btree ("urlShort")
Triggers:
    pages_label_date BEFORE INSERT OR UPDATE ON pages
      FOR EACH ROW EXECUTE PROCEDURE fix_label_date()
Run Code Online (Sandbox Code Playgroud)

ype*_*eᵀᴹ 8

这是一个关于 Postgres 优化的已知问题。如果不同的值很少 - 就像你的情况 - 而你是 8.4+ 版本,这里描述了一个使用递归查询的非常快速的解决方法:Loose Indexscan

您的查询可以重写(LATERAL需要 9.3+ 版本):

WITH RECURSIVE pa AS 
( ( SELECT labelDate FROM pages ORDER BY labelDate LIMIT 1 ) 
  UNION ALL
    SELECT n.labelDate 
    FROM pa AS p
         , LATERAL 
              ( SELECT labelDate 
                FROM pages 
                WHERE labelDate > p.labelDate 
                ORDER BY labelDate 
                LIMIT 1
              ) AS n
) 
SELECT labelDate 
FROM pa ;
Run Code Online (Sandbox Code Playgroud)

Erwin Brandstetter 在这个答案中有一个详尽的解释和查询的几个变体(在一个相关但不同的问题上):优化 GROUP BY 查询以检索每个用户的最新记录


Erw*_*ter 8

最佳查询很大程度上取决于数据分布

你每个日期有很多行,这已经建立。由于您的案例在结果中仅燃烧到 26 个值,因此一旦使用索引,以下所有解决方案都会非常快。
(对于更多不同的值,案例会变得更有趣。)

有没有必要让pageid 所有(如你评论)。

指数

您所需要的只是一个简单的 btree 索引"labelDate"
由于列中有多个 NULL 值,部分索引有更多帮助(并且更小):

CREATE INDEX pages_labeldate_nonull_idx ON big ("labelDate")
WHERE  "labelDate" IS NOT NULL;
Run Code Online (Sandbox Code Playgroud)

你后来澄清:

0% NULL 但只有在导入时修复之后。

部分索引可能仍然有助于排除具有 NULL 值的行的中间状态。将避免对索引进行不必要的更新(导致膨胀)。

询问

基于临时范围

如果您的日期出现在连续范围内且没有太多间隔,我们可以利用数据类型的性质date来发挥我们的优势。两个给定值之间只有有限的、可数的值。如果差距很小,这将是最快的:

SELECT d."labelDate"
FROM  (
   SELECT generate_series(min("labelDate")::timestamp
                        , max("labelDate")::timestamp
                        , interval '1 day')::date AS "labelDate"
   FROM   pages
   ) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");
Run Code Online (Sandbox Code Playgroud)

为什么要投timestampgenerate_series()?看:

可以从索引中廉价地选择最小值和最大值。如果您知道可能的最小和/或最大日期,它会便宜一点。例子:

SELECT d."labelDate"
FROM  (SELECT date '2011-01-01' + g AS "labelDate"
       FROM   generate_series(0, now()::date - date '2011-01-01' - 1) g) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");
Run Code Online (Sandbox Code Playgroud)

或者,对于不可变区间:

SELECT d."labelDate"
FROM  (SELECT date '2011-01-01' + g AS "labelDate"
       FROM generate_series(0, 363) g) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");
Run Code Online (Sandbox Code Playgroud)

松散索引扫描

这对于任何日期分布都表现得很好(只要我们每个日期有很多行)。基本上@ypercube 已经提供了什么。但是有一些优点,我们需要确保我们最喜欢的索引可以在任何地方使用。

WITH RECURSIVE p AS (
   ( -- parentheses required for LIMIT
   SELECT "labelDate"
   FROM   pages
   WHERE  "labelDate" IS NOT NULL
   ORDER  BY "labelDate"
   LIMIT  1
   ) 
   UNION ALL
   SELECT (SELECT "labelDate" 
           FROM   pages 
           WHERE  "labelDate" > p."labelDate" 
           ORDER  BY "labelDate" 
           LIMIT  1)
   FROM   p
   WHERE  "labelDate" IS NOT NULL
   ) 
SELECT "labelDate" 
FROM   p
WHERE  "labelDate" IS NOT NULL;
Run Code Online (Sandbox Code Playgroud)

旁白

不带引号的、合法的、小写的标识符让您的生活更轻松。
对表定义中的列进行排序以节省一些磁盘空间: