惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
SecWiki News
SecWiki News
酷 壳 – CoolShell
酷 壳 – CoolShell
爱范儿
爱范儿
量子位
M
MIT News - Artificial intelligence
GbyAI
GbyAI
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
TaoSecurity Blog
TaoSecurity Blog
博客园 - 【当耐特】
H
Heimdal Security Blog
腾讯CDC
The Last Watchdog
The Last Watchdog
Security Archives - TechRepublic
Security Archives - TechRepublic
Hacker News: Ask HN
Hacker News: Ask HN
S
Schneier on Security
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
博客园 - 司徒正美
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cybersecurity and Infrastructure Security Agency CISA
S
SegmentFault 最新的问题
大猫的无限游戏
大猫的无限游戏
Application and Cybersecurity Blog
Application and Cybersecurity Blog
F
Full Disclosure
有赞技术团队
有赞技术团队
T
Tailwind CSS Blog
Engineering at Meta
Engineering at Meta
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Threatpost
月光博客
月光博客
A
Arctic Wolf
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
雷峰网
雷峰网
T
Troy Hunt's Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The Cloudflare Blog
D
DataBreaches.Net
O
OpenAI News
L
LINUX DO - 最新话题
宝玉的分享
宝玉的分享
小众软件
小众软件
V
Vulnerabilities – Threatpost
A
About on SuperTechFans
人人都是产品经理
人人都是产品经理
T
The Exploit Database - CXSecurity.com
Martin Fowler
Martin Fowler
美团技术团队
P
Privacy International News Feed

Wener Live & Life Blog

工具 - 工 与 具 《我把我的矜持都给了她》:第二章 - 故事的重新开始 解密 ClassFinal 加密的 Java Jar 包 我记录思考的方式简单总结 第一次尝试机器学习 为什么选择 Alpine Linux? 基于 SNI 实现无感全局代理 恢复群晖数据盘 迁移阿里云 CDN 到 Cloudflare CRM 实现经历 大师之路 2022上海封城日记 企业建站的基本前提考量 Love the World 数据同步模式 组建你自己的 NAS 服务器 2021 总结 从新学习 系统盘恢复
PostgreSQL ORDER BY+LIMIT 时的索引选择
2021-10-11 · via Wener Live & Life Blog

当使用 ORDER BY+LIMIT 时 PostgreSQL 可能会选择更差的执行方式,数据量大时,执行效率相差成百上千倍。

索引选择问题

假设结构如下

create table organizations(
id bigserial primary key,
jgmc text,
clrq date
);

create index if not exists idx_organizations_clrq on ic.organizations (clrq);
create index if not exists idx_organizations_jgmc_fts on ic.organizations using pgroonga (jgmc);

查询通常基于 clrq 排序,但会 搜索 jgmc。

explain analyse verbose
select jgmc, clrq, id
from ic.organizations
where jgmc &@~ '百度'
and clrq is not null
order by clrq desc
limit 30 offset 0
;

使用了 clrq is not null 而非 nulls last

因为索引默认隐含 nulls last,也就是说 order by clrq asc 隐含 order by clrq asc nulls last。 当使用 order by clrq desc nulls last 时不会选择 idx_organizations_clrq 索引,因为 nulls 顺序不匹配。 因此使用 is not null 排除。

给出的执行计划和结果如下

Limit  (cost=0.43..33132.51 rows=30 width=1284) (actual time=4678.918..13104.109 rows=6 loops=1)
" Output: jgmc, clrq, id"
-> Index Scan Backward using idx_organizations_clrq on ic.organizations (cost=0.43..4069724.46 rows=3685 width=1284) (actual time=4678.916..13104.101 rows=6 loops=1)
" Output: jgmc, clrq, id"
Index Cond: (organizations.clrq IS NOT NULL)
Filter: (organizations.jgmc &@~ '百度'::text)
Rows Removed by Filter: 3685166
Planning Time: 0.826 ms
Execution Time: 13104.226 ms

实际执行了 13s,选择了 idx_organizations_clrq 索引。

但如果去掉 LIMIT:

explain analyse verbose
select jgmc, clrq, id
from ic.organizations
where jgmc &@~ '百度'
and clrq is not null
order by clrq desc
-- limit 30 offset 0
;

给出的计划如下

Sort  (cost=40479.53..40488.74 rows=3685 width=1284) (actual time=19.771..19.774 rows=6 loops=1)
" Output: jgmc, clrq, id"
Sort Key: organizations.clrq DESC
Sort Method: quicksort Memory: 36kB
-> Index Scan using idx_organizations_jgmc_fts on ic.organizations (cost=0.00..40261.24 rows=3685 width=1284) (actual time=19.716..19.737 rows=6 loops=1)
" Output: jgmc, clrq, id"
Index Cond: (organizations.jgmc &@~ '百度'::text)
Filter: (organizations.clrq IS NOT NULL)
Planning Time: 0.706 ms
Execution Time: 21.824 ms

实际执行只需要 21ms,选择了 idx_organizations_jgmc_fts 索引。

PostgreSQL 在这样的场景下选择了错误的索引,导致查询时间相差 600 倍。

索引选择原因

ORDER BY+LIMIT 让查询有 提前结束的可能

例如 实际数据 10k,但 limit 10, 只需使用索引扫描 10 条数据便可以停止执行,而不需要先判断 其他 条件。

因此 PostgreSQL 的 optimizer 有让这种选择优先的逻辑 pathnode.c#L3633-L3753

  • create_limit_path 针对 LIMIT/OFFSET 创建执行计划
    • adjust_limit_rows_costs 调整此时的 rows costs

adjust_limit_rows_costs

*total_cost = *startup_cost +
(input_total_cost - input_startup_cost)
* count_rows / input_rows;
varmeaning
total_cost总 cost
startup_cost初始 cost
input_total_cost输入总 cost
input_startup_cost输入初始总 cost
count_rowslimit
input_rows输入 行

在这个调整逻辑里会将现在的过程 cost input_total_cost - input_startup_cost 乘以 系数 count_rows / input_rows。 这个系数便会使得 ORDER BY+LIMIT 时优先选择索引。

因为两个计划的 rows 相同,优先了 排序索引则会选择排序索引方案。

解决问题

  1. 使用 noop function 避免索引
explain verbose
select clrq, jgmc, id
from ic.organizations
where jgmc &@~ '百度'
and clrq is not null
order by coalesce(clrq) desc
limit 30 offset 0
;
  1. 使用 expression 避免索引
explain verbose
select clrq, jgmc, id
from ic.organizations
where jgmc &@~ '百度'
and clrq is not null
order by clrq+0 desc
limit 30 offset 0
;
  1. 添加 nulls 顺序避免索引
  • 因为 index 默认 asc nulls last 因此 asc 和 desc 选择不同 nulls 顺序
  • where is not null
  • desc nulls last
  • asc nulls first
explain verbose
select clrq, jgmc, id
from ic.organizations
where jgmc &@~ '百度'
and clrq is not null
order by clrq desc nulls last
limit 30 offset 0
;
  1. 调整 STATISTICS
  • 在两者统计 rows 不同的情况下 - 这里不适用
  • SET STATISTICS
  • CREATE STATISTICS

STATISTICS 场景

STATISTICS 影响 n_distinct,n_distinct 影响 inputs_rows,多个计划相同,可增加 STATISTICS 避免错误选择索引。

SELECT attname,
n_distinct,
null_frac
FROM pg_stats
WHERE tablename = 'organizations'
and attname in ('clrq', 'jgmc');
attnamen_distinctnull_frac
clrq79140.00030666665
jgmc-0.994411950.0030133333
SELECT relname, reltuples, relpages
FROM pg_class
WHERE relname LIKE 'organizations';
relnamereltuplesrelpages
organizations36863761116103

参考