优化“WHERE x BETWEEN a AND b GROUP BY y”查询

SGr*_*SGr 6 postgresql performance index optimization postgresql-9.4

CREATE TABLE test_table
(
  id uuid NOT NULL,
  "RefId" uuid NOT NULL,
  "timestampCol" timestamp without time zone NOT NULL,
  "bigint1" bigint NOT NULL,
  "bigint2" bigint NOT NULL,
  "int1" integer NOT NULL,
  "int2" integer NOT NULL,
  "bigint3" bigint NOT NULL,
  "bigint4" bigint NOT NULL,
  "bigint5" bigint NOT NULL,
  "hugeText" text NOT NULL,
  "bigint6" bigint NOT NULL,
  "bigint7" bigint NOT NULL,
  "bigint8" bigint NOT NULL,
  "denormalizedData" jsonb NOT NULL,
  "textCol" text NOT NULL,
  "smallText" text NOT NULL,
  "createdAt" timestamp with time zone NOT NULL,
  "updatedAt" timestamp with time zone NOT NULL,
  CONSTRAINT test_pkey PRIMARY KEY (id)
);

SELECT "textCol", SUM("bigint1"), SUM("bigint2") -- etc, almost every single column gets aggregated
FROM "test_table"
WHERE "timestampCol" BETWEEN '2016-06-12' AND '2016-06-17'
GROUP BY "textCol"
ORDER BY SUM("bingint2"), SUM("bigint3") DESC -- the ORDER BY columns are dynamic, but there's only 4 possible combination of columns.
LIMIT 50;
Run Code Online (Sandbox Code Playgroud)

请纠正我的理解不正确的地方。在 Postgres 中,我可以在timestampCol或 上利用索引textCol但不能同时利用两个索引我粘贴的查询计划旨在显示仅由 Postgres 选择的算法。真正的表有几百万行,而不仅仅是约 66,000。

  1. CREATE INDEX timestamp_col_index on test_table using btree ("timestampCol")
    
    Run Code Online (Sandbox Code Playgroud)

    索引 (btree) on"timestampCol"意味着查询计划器将对整个数据集进行切片,以仅在使用 a或 a将行分组之前'2016-06-12''2016-06-17'之前保留行。Hash JoinSort + GroupAggregatetextCol

    GroupAggregate  (cost=3925.50..4483.19 rows=22259 width=41) (actual time=80.764..125.342 rows=22663 loops=1)
      Group Key: "textCol"
      ->  Sort  (cost=3925.50..3981.45 rows=22380 width=41) (actual time=80.742..84.915 rows=22669 loops=1)
            Sort Key: "textCol"
            Sort Method: quicksort  Memory: 2540kB
            ->  Index Scan using timestamp_col_index on test_table  (cost=0.29..2308.56 rows=22380 width=41) (actual time=0.053..13.939 rows=22669 loops=1)
                  Index Cond: (("timestampCol" >= '2016-06-12 00:00:00'::timestamp without time zone) AND ("timestampCol" <= '2016-06-17 00:00:00'::timestamp without time zone))
    
    Run Code Online (Sandbox Code Playgroud)
  2. CREATE INDEX text_col_index on test_table using btree ("textCol")
    
    Run Code Online (Sandbox Code Playgroud)

    索引 (btree) ontextCol意味着查询计划器已经将行“预先分组”,但它必须遍历索引中的每一行以过滤掉那些不匹配的行timestampCol BETWEEN timestamp1 AND timestamp2

    GroupAggregate  (cost=0.42..16753.91 rows=22259 width=41) (actual time=0.281..127.047 rows=22663 loops=1)
      Group Key: "textCol"
      ->  Index Scan using text_col_index on test_table  (cost=0.42..16252.18 rows=22380 width=41) (actual time=0.235..76.182 rows=22669 loops=1)
            Filter: (("timestampCol" >= '2016-06-12 00:00:00'::timestamp without time zone) AND ("timestampCol" <= '2016-06-17 00:00:00'::timestamp without time zone))
            Rows Removed by Filter: 43719
    
    Run Code Online (Sandbox Code Playgroud)
  3. 创建这两个索引意味着 Postgres 将运行成本分析来决定它认为 1. 和 2. 中的哪个最快。但它永远不会同时利用两个索引。

  4. 创建多列索引无济于事。从我的测试Postgres将不会改变其查询计划无论是("textCol", "timestampCol")("timestampCol", "textCol")

  5. 我已经尝试过btree_ginbtree_gist扩展,但我从来没有能够让查询规划器保持行“预先分组”或利用大约 4,000,000 行规模的可观速度增益,与 1. 和 2 相比。也许我是没有正确使用它们?我将如何构建这些索引并使我的查询适应它?

请让我知道我可能有什么误解。如何针对包含几百万行的表优化此类查询?

关于数据结构的重要信息:

  • BETWEEN 中使用的时间戳有 99% 的时间类似于“过去 2 周”或“上个月”。在某些情况下,BETWEEN 最终会选择多达 99% 的行,但很少会选择 100% 的行。

  • textCol列可以非常多样或非常规则。在某些情况下,假设有 300 万行,就会有 290 万个唯一textCol值。在其他情况下,对于相同数量的行,只有 30,000-100,000 个唯一textCol值。

我使用的是 Postgres 9.4,但只要性能提升可以证明它的合理性,升级到 9.5 是可行的。

Erw*_*ter 4

想法1

从它们的名称来看,这些列"denormalizedData"似乎"hugeText"相对较大,可能是查询中涉及的列的许多倍。对于像这样的大查询来说,大小很重要。非常大的值(> 2kb)textjsonb得到“烤”,这可以避免最坏的情况。但即使是内联存储的余数或更小的值也可能是与查询相关的列的几倍大,这些列的大小约为 100 个字节。

有关的:

将与查询相关的列拆分到单独的 1:1 表中可能会大有帮助。(取决于完整的情况。您为另一个行标题和另一个 PK 添加一些存储开销,并且写入表会变得更加复杂和昂贵。)

想法2

此外(正如您所确认的)只有 4 列与确定前 50 名相关。

您可能有一个更小的物化视图(MV)的角度,该视图仅包含这些列以及"timestampCol"数据。对 MV 运行快速查询以识别前 50 行,并仅从大表中检索这些行。或者,准确地说,只是MV 中未包含的附加列 - 您将在第一步中获得这些列的总和。"textCol""last 2 weeks" or "last month""textCol"

("textCol")您只需要为大表建立索引。另一种用于("timestampCol")MV - 它仅用于带有选择性子句的查询实例WHERE。否则,顺序扫描整个 MV 会更便宜。

如果您的许多查询涵盖相同的时间段,您可能会更进一步:在"textCol"MV 中仅保存一行以及预先聚合的总和(对于几个经常使用的时间段,可能有两个或更多 MV)。你明白了。那应该要快得多。

您甚至可以使用整个结果集创建 MV,并在当天的第一个新查询之前刷新。

根据具体数字,您可以将这两种想法结合起来。