【问题标题】:PostgreSQL - Query OptimizationPostgreSQL - 查询优化
【发布时间】:2014-06-13 21:24:40
【问题描述】:

我有以下查询,运行大约需要 15-20 秒。

cte0 为 ( 选择 标签, 日期, 案子 什么时候 Lead(label || date || "number") OVER (PARTITION BY label || date || "number" ORDER BY "label", "date", "number", "time") IS NULL 然后 '1':: 数字 别的 '0':: 数字 END 作为“唯一” FROM 表数据 LEFT JOIN table_mapper ON table_mapper."type" = table_data."type" 日期在 date_trunc('month', current_date - 1) 和 current_date - 1 之间的日期 ) 选择“MTD”作为“标签”,round(sum(“unique”) / count(“unique”) *100,1) 作为“value” FROM cte0 WHERE “date” BETWEEN date_trunc('month', current_date - 1)和 current_date -1 联合所有 SELECT 'Week' 作为“标签”,round(sum(“unique”) / count(“unique”) *100,1) 作为“value” FROM cte0 WHERE “date” BETWEEN date_trunc('week', current_date - 1)和 current_date -1 联合所有 选择“FTD”作为“标签”,round(sum(“unique”) / count(“unique”) *100,1) 作为“值” FROM cte0 WHERE “date” = current_date -1

table_data 表中,我在date 列上有一个索引。

创建索引 ix_cli_date ON 表数据 使用 btree (日期);

表定义(\d table_data

表“public.table_data” 专栏 |类型 |修饰符 ------------------+------------------------+------ ----- 日期 |日期 |不为空 号码 |大整数 |不为空 时间 |没有时区的时间|不为空 结束时间 |没有时区的时间|不为空 持续时间 |整数 |不为空 时间1 |整数 |不为空 时间2 |整数 |不为空 时间3 |整数 |不为空 时间4 |整数 |不为空 时间5 |整数 |不为空 时间6 |整数 |不为空 时间7 |整数 |不为空 类型 |正文 |不为空 姓名 |正文 |不为空 id1 |整数 |不为空 id2 |整数 |不为空 关键 |整数 |不为空 状态 |正文 |不为空 索引: “ix_cli_date” btree(日期)

表定义 (\d table_mapper)

表“public.table_mapper” 专栏 |类型 |修饰符 ------------+------+------------ 类型 |正文 |不为空 标签 |正文 |不为空 标签2 |正文 |不为空 标签3 |正文 |不为空 标签4 |正文 |不为空 标签5 |正文 |不为空

解释查询的分析

结果(成本=184342.66..230332.86 行=3 宽度=64)(实际时间=23377.923..25695.478 行=3 循环=1)" 热膨胀系数 cte0" -> WindowAgg(成本=121516.06..156751.65 行=612793 宽度=23)(实际时间=14578.000..18985.958 行=696157 循环=1)” -> 排序(成本=121516.06..123048.04 行=612793 宽度=23)(实际时间=14577.975..17084.405 行=696157 循环=1)” 排序键:(((table_mapper.label || (table_data.date)::text) || (table_data."number")::text)), table_mapper.label, table_data.date, table_data."number", table_data 。“时间”” 排序方法:外部合并磁盘:39480kB" -> Hash Left Join (cost=11.96..37474.21 rows=612793 width=23) (实际时间=1.449..3308.718 rows=696157 loops=1)" 哈希条件:(table_data."type" = table_mapper."type")" -> 在 table_data 上使用 ix_cli_date 进行索引扫描(成本=0.02..29036.36 行=612793 宽度=38)(实际时间=0.141..946.648 行=696157 循环=1)" 索引条件:((date >= date_trunc('month'::text, ((('now'::text)::date - 1))::timestamp with time zone)) AND (date Hash (cost=7.53 ..7.53 行=353 宽度=25) (实际时间=1.275..1.275 行=336 循环=1)" 存储桶:1024 批次:1 内存使用量:15kB" -> table_mapper 上的 Seq Scan (cost=0.00..7.53 rows=353 width=25) (实际时间=0.020..0.589 rows=336 loops=1)" -> 追加(成本=27591.00..73581.21 行=3 宽度=64)(实际时间=23377.920..25695.467 行=3 循环=1)” -> 聚合(成本=27591.00..27591.02 行=1 宽度=32)(实际时间=23377.917..23377.918 行=1 循环=1)” -> cte0上的CTE扫描(成本=0.00..27575.68行=3064宽度=32)(实际时间=14578.052..22335.236行=696157循环=1)” 过滤器:((date = date_trunc('month'::text, ((('now'::text)::date - 1))::timestamp with time zone)))" -> 聚合(成本=27591.00..27591.02 行=1 宽度=32)(实际时间=1741.509..1741.510 行=1 循环=1)” -> cte0上的CTE扫描(成本=0.00..27575.68行=3064宽度=32)(实际时间=20.009..1522.352行=168261循环=1)” 过滤器:((date = date_trunc('week'::text, ((('now'::text)::date - 1))::timestamp with time zone)))" -> 聚合(成本=18399.11..18399.13 行=1 宽度=32)(实际时间=576.029..576.030 行=1 循环=1)” -> cte0上的CTE扫描(成本=0.00..18383.79行=3064宽度=32)(实际时间=9.308..546.735行=23486循环=1)” 过滤器:(date = (('now'::text)::date - 1))" 总运行时间:25710.506 ms"

说明:

我从 table_data 获取唯一计数和重复计数,而 LEAD 帮助我解决了我为列的最后一个重复值赋予值 0 的位置。

假设我在一列中有 3 个x。我将1 的值赋予前2 个x,第三个x 赋予0。

实际上,通过cte,我从表table_data 中取出整行,并使用前导进行一些计算,并在定义的日期范围内连接字符串,其中每行10 的值为根据标准定义。

如果前导为空,则计为 1,如果不为空,则计为 0。

我分别返回 3 行 MTDCurrent WeekFTD,并计算我从领先者那里得到的 sum()count(*) 整行。

对于 MTD,我有当月的总和和计数。

一周 - 这是本周,FTD 是昨天。

【问题讨论】:

  • 您执行此字符串连接:Lead(label || date || "number") OVER (PARTITION BY label || date || "number" ORDER BY "label", ... 仅检测组的开头?为什么不使用 raw 字段呢?
  • 我在您的表定义中没有看到主键。是否缺少任何其他约束或索引?最好在psql 中展示您使用\d table_data 得到的结果,而不是一些手工制作的代理。
  • 其实我是个新手。我需要在 SQL 编辑器中运行 \d table_data 吗?顺便说一句,我没有为主列创建任何列。
  • psql 是 PostgreSQL 的默认命令行界面。每个表都应该有一个primary key。另外,最好不要使用reserved words作为标识符。
  • 好。缺少的是描述。请添加一些解释查询应该实现的目标。

标签: sql database performance postgresql query-optimization


【解决方案1】:

在编写查询时,您指的是 CTE 三次。相反,如果您愿意将值放在三列而不是三行中,则可以使用条件聚合:

SELECT round(sum("date" BETWEEN date_trunc('month', current_date - 1) AND current_date -1 then "unique" else 0 END)) /
             sum("date" BETWEEN date_trunc('month', current_date - 1) AND current_date -1 then 1 else 0 END)) *100,1) as mtd
     . . .
FROM CTE

这可能会加快查询速度。此外,您还可以将此逻辑合并到 CTE 查询本身中,同时消除具体化步骤。

【讨论】:

  • 我尝试了你回答我的方法。但是如果我运行查询。我收到错误syntax error at or near "then"
【解决方案2】:
WITH cte AS (
   SELECT d.thedate
        , lead(m.label) OVER (PARTITION BY m.label, d.thedate, d.number
                              ORDER BY d.thetime) AS leader
   FROM   table_data d
   LEFT   JOIN table_mapper m USING (type)
   WHERE  thedate BETWEEN date_trunc('month', current_date - 1)
                  AND current_date - 1
   )

SELECT 'MTD' AS label, round(count(leader)::numeric / count(*) * 100, 1) AS val
FROM   cte

UNION ALL
SELECT 'Week', round(count(leader)::numeric / count(*) * 100, 1)
FROM   cte
WHERE  thedate BETWEEN date_trunc('week', current_date - 1) AND current_date - 1

UNION ALL
SELECT 'FTD', round(count(leader)::numeric / count(*) * 100, 1)
FROM   cte
WHERE  thedate = current_date - 1;
  • CTE 适用于大表,因此您只需扫描一次。对于较小的表,如果没有 ... 可能会更快。

  • 使用thedate 代替保留字date(在标准SQL 中)。 thetimeuni 而不是 timeunique。等等。

  • 简化了lead() 调用。您会为前导行获得一个值或 NULL。这似乎是唯一相关的信息。
    window functionORDER BY 子句中重复PARTITION 子句中的列也是没有意义的。

  • 在此基础上,count(leader) / count(*) 而不是 sum(uni) / count(uni)。这有点快。 count(column) 只计算非空值,而count(*) 计算所有行。

  • UNION 查询的第一段的条件是多余的。

  • 有关问题的 cmets 中数据定义的更多建议和链接。

表格设计/索引

你应该有主键。我建议serial 列作为table_data 的代理pk:

ALTER TABLE table_data ADD COLUMN table_data_id serial PRIMARY KEY;

使type 成为table_mapper 的主键(以下fk 约束也需要):

ALTER TABLE table_mapper ADD CONSTRAINT table_mapper_pkey (type);

type添加外键约束以保证引用完整性。比如:

ALTER TABLE table_data ADD CONSTRAINT table_data_type_fkey
  FOREIGN KEY (type) REFERENCES table_mapper (type)
  ON UPDATE CASCADE ON DELETE NO ACTION;

为了获得最终的读取性能(以一定的写入成本),添加一个多列索引以可能允许index-only scans 用于上述查询:

CREATE INDEX table_data_foo_idx ON table_data (thedate, number, thetime);

【讨论】:

  • 我需要将date 列更改为todate 或者我可以像“日期”一样简单地使用它作为“日期”?它确实将时间缩短到了 12 秒。
  • 我建议对不是保留字(或基本类型名称)的列使用 any 其他名称:ALTER TABLE table_data RENAME "date" TO my_new_column_name;Details in the manual.
  • 非常感谢。我会做的。
猜你喜欢
  • 2022-01-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-12
  • 2018-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多