SELECT 查询中未使用索引

Jam*_*Hay 5 postgresql performance index database-design postgresql-performance

我有一个大约 3.25M 行的表格,在 Postgres 9.4.1 中遵循以下格式

CREATE TABLE stats
(
    id serial NOT NULL,
    type character varying(255) NOT NULL,
    "references" jsonb NOT NULL,
    path jsonb,
    data jsonb,
    "createdAt" timestamp with time zone NOT NULL,
    CONSTRAINT stats_pkey PRIMARY KEY (id)
)
WITH (
    OIDS=FALSE
);
Run Code Online (Sandbox Code Playgroud)

type是一个简单的字符串长度不超过50个字符。

references列是一个带有键值列表的对象。基本上任何简单的键值列表,只有 1 级深,值总是字符串。它可能是

{
    "fruit": "plum"
    "car": "toyota"
}
Run Code Online (Sandbox Code Playgroud)

或者它可能是

{
    "project": "2532"
}
Run Code Online (Sandbox Code Playgroud)

createdAt时间戳并不总是从数据库中生成的(但它会默认如果值不被提供)

我目前使用的表格只有测试数据。在此数据中,每一行都有一个project键作为参考。所以有 3.25M 行有一个项目键。project引用正好有 400,000 个不同的值。该type字段只有 5 个不同的值,在生产中可能不会超过几百个。

所以我试图索引表以快速执行以下查询:

SELECT
  EXTRACT(EPOCH FROM (MAX("createdAt") - MIN("createdAt"))) 
FROM
  stats
WHERE
  stats."references"::jsonb ? 'project' AND
  (
    stats."type" = 'event1' OR
    (
      stats."type" = 'event2' AND
      stats."createdAt" > '2015-11-02T00:00:00+08:00' AND
      stats."createdAt" < '2015-12-03T23:59:59+08:00'
    )
  )
GROUP BY stats."references"::jsonb->> 'project'
Run Code Online (Sandbox Code Playgroud)

该查询基于具有相同引用的两个统计信息行返回两个事件之间的时间距离。在这种情况下project。每个type选定的reference值只有 1 行,但也可能没有行,在这种情况下返回的结果为 0(稍后在较大查询的不同部分进行平均)。

我在createdAt typereferences列上创建了一个索引,但查询执行计划似乎正在执行完整扫描。

指数

CREATE INDEX "stats_createdAt_references_type_idx"
    ON stats
    USING btree
    ("createdAt", "references", type COLLATE pg_catalog."default");
Run Code Online (Sandbox Code Playgroud)

执行计划:

 HashAggregate  (cost=111188.31..111188.33 rows=1 width=38) 
                (actual time=714.499..714.499 rows=0 loops=1)
   Group Key: ("references" ->> 'project'::text)
      ->  Seq Scan on stats  (cost=0.00..111188.30 rows=1 width=38) 
                             (actual time=714.498..714.498 rows=0 loops=1)
          Filter: (
              (("references" ? 'project'::text) 
               AND ((type)::text = 'event1'::text)) OR 
              (((type)::text = 'event2'::text) 
               AND ("createdAt" > '2015-11-02 05:00:00+13'::timestamp with time zone) 
               AND ("createdAt" < '2015-12-04 04:59:59+13'::timestamp with time zone)))

Rows Removed by Filter: 3258680
Planning time: 0.163 ms
Execution time: 714.534 ms
Run Code Online (Sandbox Code Playgroud)

我真的不太了解索引和查询执行计划,所以如果有人能指出我正确的方向,那就太好了。

编辑

正如 Erwin 所指出的,似乎即使我确实有正确的索引,由于查询返回的表部分非常大,表扫描仍然会发生。这是否意味着对于这组数据,这是我可以获得的最快查询时间?我假设如果我在没有项目引用的情况下添加了 60M 多个不相关的行,它可能会使用索引(如果我有正确的索引),但我不知道如何通过添加更多数据来加速查询。也许我错过了什么。

Erw*_*ter 4

根据您当前的解释,索引对您当前的查询不会有太大帮助(如果有的话)。

因此,有 325 万行带有项目密钥。

这也是总行数,因此该谓词适用true于(几乎)每一行……并且根本没有选择性。jsonb但该列没有有用的索引"references"。将其包含在btree索引中("createdAt", "references", type)是毫无意义的。

即使你有一个更有用的 GIN 索引,"reference"比如:

CREATE INDEX stats_references_gix ON stats USING gin ("references");
Run Code Online (Sandbox Code Playgroud)

...Postgres 仍然没有关于列内各个键的有用统计信息jsonb


只有 5 个不同的值type

您的查询选择一种类型的全部以及另一种类型的未知部分。估计占所有行的 20-40%。顺序扫描肯定是最快的计划。索引开始对所有行的大约 5% 或更少有意义。

要进行测试,您可以通过在会话中出于调试目的进行设置来强制使用可能的索引:

SET enable_seqscan = off;
Run Code Online (Sandbox Code Playgroud)

重置为:

RESET enable_seqscan;
Run Code Online (Sandbox Code Playgroud)

你会看到更慢的查询......


您按项目值分组:

GROUP BY "references"->> 'project'

和:

正好有 400,000 个不同的值可供项目参考。

平均每个项目有 8 行。如果我们只在横向子查询中选择每个项目的最小值和最大值,根据值频率,我们仍然需要检索所有行的估计 3 - 20% ...

试试这个索引,它比你现在拥有的更有意义:

CREATE INDEX stats_special_idx ON stats (type, ("references" ->> 'project'), "createdAt")
WHERE "references" ? 'project';
Run Code Online (Sandbox Code Playgroud)

Postgres 可能仍会退回到顺序扫描......

可以通过规范化模式/更具选择性的标准/仅选择最小值和最大值的更智能查询来完成更多工作"createdAt"......

询问

我会这样写你的查询:

SELECT EXTRACT(EPOCH FROM (MAX("createdAt") - MIN("createdAt"))) 
FROM   stats
WHERE  "references" ? 'project'
AND   (type = 'event1' OR
       type = 'event2'
   AND "createdAt" >= '2015-11-02 00:00:00+08:00'  -- I guess you want this
   AND "createdAt" <  '2015-12-04 00:00:00+08:00'
      )
GROUP  BY "references"->> 'project';  -- don't cast
Run Code Online (Sandbox Code Playgroud)

笔记

  • 不要在这里投射:

    stats."references"::jsonb ? 'project'

    专栏jsonb已经存在了,你什么也得不到。如果谓词选择性的,则强制转换可能会禁止索引使用。

  • 您的谓词在下限和上限上"createdAt"可能不正确。要包括全天,请考虑我建议的替代方案。

  • references是一个保留字,所以你必须始终用双引号它。不要将其用作标识符。与双引号 CaMeL 大小写名称类似"createdAt"。允许,但容易出错,不必要的复杂化。

  • type

    type character varying(255) NOT NULL,

    该类型是一个不超过 50 个字符的简单字符串。

    type 字段只有 5 个不同的值,在生产中可能不会超过几百个。

    这些似乎都没有道理。

    • varchar(255)其本身几乎没有任何意义。255 个字符是任意限制,在 Postgres 中没有意义。
    • 如果长度不超过 50 个字符,那么 255 个字符的限制就更没有意义了。
    • 在正确规范化的设计中,您将拥有一个小integertype_id(引用一个小type表),每行仅占用 4 个字节,并使索引更小、更快。
  • 理想情况下,您将有一个project表,列出所有项目和另一个小整数 FKproject_idstats。将使任何此类查询更快。对于选择性标准,即使没有建议的标准化,也可以实现更快的查询沿着这些思路:

  • 优化 GROUP BY 查询以检索每个用户的最新记录