病态 SQL 查询的秘诀

几个月前 我们宣布 解释.tensor.ru - 民众 用于解析和可视化查询计划的服务 到 PostgreSQL。

从那时起,您已经使用它超过 6000 次,但其中一项可能未被注意到的便利功能是 结构线索,看起来像这样:

病态 SQL 查询的秘诀

倾听他们的声音,您的要求将“变得如丝般顺滑”。 🙂

但严重的是,许多情况下请求在资源方面缓慢且“贪婪”, 具有典型性,可以通过计划的结构和数据识别.

在这种情况下,每个开发人员都不必自己寻找优化选项,完全依靠自己的经验——我们可以告诉他这里发生了什么,可能是什么原因,以及 如何提出解决方案. 这就是我们所做的。

病态 SQL 查询的秘诀

让我们仔细看看这些案例——它们是如何定义的以及它们带来了哪些建议。

为了更好地沉浸在主题中,您可以先从中收听相应的块 我在 PGConf.Russia 2020 的报告,然后才去详细分析每个例子:

#1:索引“undersorting”

什么时候出现

显示客户“LLC Kolokolchik”的最后一张发票。

如何鉴别

-> Limit
   -> Sort
      -> Index [Only] Scan [Backward] | Bitmap Heap Scan

建议

使用的索引 扩展排序字段.

示例:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "фактов"
, (random() * 1000)::integer fk_cli; -- 1K разных внешних ключей

CREATE INDEX ON tbl(fk_cli); -- индекс для foreign key

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 1 -- отбор по конкретной связи
ORDER BY
  pk DESC -- хотим всего одну "последнюю" запись
LIMIT 1;

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

你可以立即注意到索引减去了 100 多条记录,然后将这些记录全部排序,然后只剩下一条。

我们修复:

DROP INDEX tbl_fk_cli_idx;
CREATE INDEX ON tbl(fk_cli, pk DESC); -- добавили ключ сортировки

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

即使在这样一个原始样本上—— 读取速度提高 8.5 倍,读取次数减少 33 倍. 效果会更清晰,每个值的“事实”越多。 fk.

我注意到这样的索引将作为“前缀”索引使用,不会比其他查询的前一个索引差 fk, 排序依据 pk 过去不是现在也不是(你可以阅读更多关于这个 在我关于查找低效索引的文章中). 特别是,它将提供正常的 显式外键支持 通过这个领域。

#2:索引交集(BitmapAnd)

什么时候出现

显示代表“NJSC Lyutik”为客户“LLC Kolokolchik”签订的所有合同。

如何鉴别

-> BitmapAnd
   -> Bitmap Index Scan
   -> Bitmap Index Scan

建议

创建 综合指数 按来自两个源的字段或从第二个扩展现有字段之一。

示例:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "фактов"
, (random() *  100)::integer fk_org  -- 100 разных внешних ключей
, (random() * 1000)::integer fk_cli; -- 1K разных внешних ключей

CREATE INDEX ON tbl(fk_org); -- индекс для foreign key
CREATE INDEX ON tbl(fk_cli); -- индекс для foreign key

SELECT
  *
FROM
  tbl
WHERE
  (fk_org, fk_cli) = (1, 999); -- отбор по конкретной паре

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

我们修复:

DROP INDEX tbl_fk_org_idx;
CREATE INDEX ON tbl(fk_org, fk_cli);

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

此处增益较小,因为位图堆扫描本身非常有效。 但不管怎么说 读取速度提高 7 倍,读取次数减少 2.5 倍.

#3:合并索引(BitmapOr)

什么时候出现

显示前 20 个最旧的“自己的”或未分配的处理请求,优先处理自己的请求。

如何鉴别

-> BitmapOr
   -> Bitmap Index Scan
   -> Bitmap Index Scan

建议

使用 联合 [全部] 为每个条件 OR 块组合子查询。

示例:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "фактов"
, CASE
    WHEN random() < 1::real/16 THEN NULL -- с вероятностью 1:16 запись "ничья"
    ELSE (random() * 100)::integer -- 100 разных внешних ключей
  END fk_own;

CREATE INDEX ON tbl(fk_own, pk); -- индекс с "вроде как подходящей" сортировкой

SELECT
  *
FROM
  tbl
WHERE
  fk_own = 1 OR -- свои
  fk_own IS NULL -- ... или "ничьи"
ORDER BY
  pk
, (fk_own = 1) DESC -- сначала "свои"
LIMIT 20;

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

我们修复:

(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own = 1 -- сначала "свои" 20
  ORDER BY
    pk
  LIMIT 20
)
UNION ALL
(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own IS NULL -- потом "ничьи" 20
  ORDER BY
    pk
  LIMIT 20
)
LIMIT 20; -- но всего - 20, больше и не надо

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

我们利用了第一个块中立即接收到所有 20 条必要记录的事实,所以第二个具有更“昂贵”的位图堆扫描的记录甚至没有被执行——结果 速度提高 22 倍,读取次数减少 44 倍!

关于这种优化方法的更详细的故事 具体例子 可以在文章中阅读 PostgreSQL 反模式:有害的 JOIN 和 OR и PostgreSQL 反模式:按名称迭代优化搜索或“来回优化”的故事.

通用版 通过几个键有序选择 (而不仅仅是一对 const / NULL)在文章中讨论 SQL HowTo:直接在查询中写一个while-loop,或者“初等三路”.

#4:我们读得太多了

什么时候出现

通常,当您想要“附加另一个过滤器”到现有请求时,它就会发生。

“而你没有相同的,但是 与珍珠母纽扣“? 电影《钻石之手》

例如,修改上面的任务,显示前 20 个最旧的“关键”处理请求,无论它们的目的如何。

如何鉴别

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && 5 × rows < RRbF -- отфильтровано >80% прочитанного
   && loops × RRbF > 100 -- и при этом больше 100 записей суммарно

建议

创造 [更多] 专业 带 WHERE 子句的索引 或在索引中包含其他字段。

如果过滤条件对于您的任务是“静态的”——即 不包括扩展 未来的值列表 - 最好使用 WHERE 索引。 各种布尔/枚举状态很适合这一类。

如果过滤条件 可以取不同的值,最好用这些字段扩展索引——就像 Bitmap 和上面的情况一样。

示例:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk -- 100K "фактов"
, CASE
    WHEN random() < 1::real/16 THEN NULL
    ELSE (random() * 100)::integer -- 100 разных внешних ключей
  END fk_own
, (random() < 1::real/50) critical; -- 1:50, что заявка "критичная"

CREATE INDEX ON tbl(pk);
CREATE INDEX ON tbl(fk_own, pk);

SELECT
  *
FROM
  tbl
WHERE
  critical
ORDER BY
  pk
LIMIT 20;

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

我们修复:

CREATE INDEX ON tbl(pk)
  WHERE critical; -- добавили "статичное" условие фильтрации

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

如您所见,计划中的过滤完全没有了,请求变成了 快 5 倍.

#5:稀疏表

什么时候出现

各种尝试让自己的任务处理队列,当大量更新/删除表上的记录导致出现大量“死”记录的情况。

如何鉴别

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && loops × (rows + RRbF) < (shared hit + shared read) × 8
      -- прочитано больше 1KB на каждую запись
   && shared hit + shared read > 64

建议

定期手动执行 真空[满] 或达到足够频繁的处理 自动真空 通过微调其参数,包括 对于特定的表.

在大多数情况下,此类问题是由业务逻辑调用时查询布局不佳引起的,例如在 PostgreSQL 反模式:与成群结队的“死人”作斗争.

但是我们必须明白,即使是 VACUUM FULL 也不能总是提供帮助。 对于这种情况,您应该熟悉本文中的算法。 DBA:当 VACUUM 通过时,我们手动清理表.

#6:从索引的“中间”读取

什么时候出现

看起来他们读了一点,所有的东西都被索引了,他们没有额外过滤任何人——但是,阅读的页面仍然比我们想要的多得多。

如何鉴别

-> Index [Only] Scan [Backward]
   && loops × (rows + RRbF) < (shared hit + shared read) × 8
      -- прочитано больше 1KB на каждую запись
   && shared hit + shared read > 64

建议

仔细查看所用索引的结构和查询中指定的关键字段 - 很可能, 未设置索引部分. 您很可能需要创建一个类似的索引,但没有前缀字段,或者 学习迭代他们的价值观.

示例:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "фактов"
, (random() *  100)::integer fk_org  -- 100 разных внешних ключей
, (random() * 1000)::integer fk_cli; -- 1K разных внешних ключей

CREATE INDEX ON tbl(fk_org, fk_cli); -- все почти как в #2
-- только вот отдельный индекс по fk_cli мы уже посчитали лишним и удалили

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 999 -- а fk_org не задано, хотя стоит в индексе раньше
LIMIT 20;

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

一切似乎都很好,即使就索引而言,但有些可疑 - 对于读取的 20 条记录中的每一条,必须减去 4 页数据,每条记录 32KB - 这不是大胆的吗? 是和索引名称 tbl_fk_org_fk_cli_idx 导致思想。

我们修复:

CREATE INDEX ON tbl(fk_cli);

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

突然 - 阅读速度提高 10 倍,阅读时间减少 4 倍!

有关索引使用效率低下的更多示例,请参阅文章 DBA:找到无用的索引.

#7:CTE×CTE

什么时候出现

应要求 得分“胖”CTE 来自不同的表,然后决定在它们之间做 JOIN.

该案例与低于 v12 的版本或带有 WITH MATERIALIZED.

如何鉴别

-> CTE Scan
   && loops > 10
   && loops × (rows + RRbF) > 10000
      -- слишком большое декартово произведение CTE

建议

仔细分析请求 这里是否需要 CTE? 如果是,那么 在 hstore/json 中应用“字典” 根据描述的模型 PostgreSQL 反模式:字典 Hit Heavy JOIN.

#8:交换到磁盘(临时写入)

什么时候出现

大量记录的一次性处理(排序或唯一化)不适合为此分配的内存。

如何鉴别

-> *
   && temp written > 0

建议

如果操作使用的内存量没有大大超过参数的设置值 工作内存, 应该纠正。 你可以立即在每个人的配置中,或者你可以通过 SET [LOCAL] 对于特定的请求/交易。

示例:

SHOW work_mem;
-- "16MB"

SELECT
  random()
FROM
  generate_series(1, 1000000)
ORDER BY
  1;

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

我们修复:

SET work_mem = '128MB'; -- перед выполнением запроса

病态 SQL 查询的秘诀
[看看 explain.tensor.ru]

出于显而易见的原因,如果只使用内存而不使用磁盘,那么查询的执行速度会快得多。 同时,部分负载也从硬盘上卸下。

但是你需要明白,分配大量内存也总是行不通的——这对每个人来说都是不够的。

#9:不相关的统计数据

什么时候出现

一下子往基地里倒了很多,但他们没有时间把它赶走 ANALYZE.

如何鉴别

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && ratio >> 10

建议

花同样的钱 ANALYZE.

这种情况在中有更详细的描述 PostgreSQL 反模式:统计是一切之首.

#10:“出了点问题”

什么时候出现

有锁在等待竞争请求,或者没有足够的 CPU/管理程序硬件资源。

如何鉴别

-> *
   && (shared hit / 8K) + (shared read / 1K) < time / 1000
      -- RAM hit = 64MB/s, HDD read = 8MB/s
   && time > 100ms -- читали мало, но слишком долго

建议

使用外部 监视系统 服务器阻塞或异常资源消耗。 我们已经讨论过我们为数百台服务器组织此过程的版本。 这里 и 这里.

病态 SQL 查询的秘诀
病态 SQL 查询的秘诀

来源: habr.com

添加评论