PostgreSQL Bitmap scan 和 Index scan 优化一个SQL ,结果非常不一样!
❝开头还是介绍一下群,如果感兴趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis, OceanBase, Sql Server等有问题,有需求都可以加群群内有各大数据库行业大咖,可以解决你的问题。加群请联系 liuaustin3 ,(共3400人左右 1 + 2 + 3 + 4 +5 + 6 + 7 + 8 +9)(1 2 3 4 5 6 7 8, 9群 300+,开10群PolarDB专业学习群100+)
最近发生一件事情,之前我没有细想,这个基于POSTGRESQL的数据库优化问题,就是关于建立了索引,在高并发的问题。
工作比较忙,事情比较多其实很多事情就无暇顾及,这点还是得说,给更多的时间和人力在优化数据库上,是可以降低成本的,但这样的方式因为不明显,或者并不被注意到,而无人理会。
这就是数据库添加了索引和数据库SQL优化是两个概念,Bitmap Index Scan 和 index scan在高并发的情况下,那个更好的问题。
我们做一个练习先来把这个问题复现一下。
test=#
test=# DROP TABLE IF EXISTS users CASCADE;
NOTICE: table "users" does not exist, skipping
bigserial PRIMARY KEY,
user_id bigint NOT NULL,
status text NOT NULL,
created_at timestamptz NOT NULL,
amount numeric
);DROP TABLE
test=#
test=# CREATE TABLE users (
test(# user_id bigint PRIMARY KEY,
test(# user_name text
test(# );
CREATE TABLE
test=#
test=# INSERT INTO users
test-# SELECT id, 'user_' || id
test-# FROM generate_series(1, 1000) id;
INSERT 0 1000
test=#
test=#
test=# DROP TABLE IF EXISTS orders;
NOTICE: table "orders" does not exist, skipping
DROP TABLE
test=#
test=# CREATE TABLE orders (
test(# order_id bigserial PRIMARY KEY,
test(# user_id bigint NOT NULL,
test(# status text NOT NULL,
test(# created_at timestamptz NOT NULL,
test(# amount numeric
test(# );
CREATE TABLE
test=#
test=#
test=#
test=# INSERT INTO orders (user_id, status, created_at, amount)
test-# SELECT
test-# (random() * 999 + 1)::int,
test-# CASE WHEN random() < 0.7 THEN 'PAID' ELSE 'NEW' END,
test-# now() - (random() * interval '30 days'),
test-# random() * 1000
test-# FROM generate_series(1, 1000000);
INSERT 0 1000000
test=#
test=#
test=# \timing
Timing is on.
test=# EXPLAIN (ANALYZE, BUFFERS)
test-# SELECT
test-# u.user_name,
test-# count(*)
test-# FROM
test-# orders o
test-# JOIN users u ON o.user_id = u.user_id
test-# WHERE
test-# o.user_id = 42
test-# AND o.status = 'PAID'
test-# GROUP BY u.user_name;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
------------------
Finalize GroupAggregate (cost=16619.87..16622.31 rows=1 width=16) (actual time=27.053..29.310 rows=1.00 loops=1)
Group Key: u.user_name
Buffers: shared hit=9382
-> Gather Merge (cost=16619.87..16622.29 rows=2 width=16) (actual time=27.006..29.271 rows=3.00 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=9382
-> Partial GroupAggregate (cost=15619.84..15622.03 rows=1 width=16) (actual time=22.965..23.018 rows=1.00 loops=3)
Group Key: u.user_name
Buffers: shared hit=9382
-> Sort (cost=15619.84..15620.57 rows=291 width=8) (actual time=20.692..21.811 rows=235.67 loops=3)
Sort Key: u.user_name
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=9382
Worker 0: Sort Method: quicksort Memory: 25kB
Worker 1: Sort Method: quicksort Memory: 25kB
-> Nested Loop (cost=0.28..15607.93 rows=291 width=8) (actual time=0.116..19.546 rows=235.67 loops=3)
Buffers: shared hit=9366
-> Parallel Seq Scan on orders o (cost=0.00..15596.00 rows=291 width=8) (actual time=0.041..12.387 rows=235.6
7 loops=3)
Filter: ((user_id = 42) AND (status = 'PAID'::text))
Rows Removed by Filter: 333098
Buffers: shared hit=9346
-> Materialize (cost=0.28..8.30 rows=1 width=16) (actual time=0.005..0.010 rows=1.00 loops=707)
Storage: Memory Maximum Storage: 17kB
Buffers: shared hit=20
-> Index Scan using users_pkey on users u (cost=0.28..8.29 rows=1 width=16) (actual time=0.027..0.037 r
ows=1.00 loops=3)
Index Cond: (user_id = 42)
Index Searches: 3
Buffers: shared hit=20
Planning:
Buffers: shared hit=60
Planning Time: 0.257 ms
Execution Time: 29.372 ms
(33 rows)
Time: 30.414 ms
test=#
test=#
test=#
test=#
test=# CREATE INDEX idx_orders_user
test-# ON orders(user_id);
CREATE INDEX
Time: 251.459 ms
test=#
test=# CREATE INDEX idx_orders_status
test-# ON orders(status);
CREATE INDEX
Time: 302.121 ms
test=#
test=# EXPLAIN (ANALYZE, BUFFERS)
SELECT
u.user_name,
count(*)
FROM
orders o
JOIN users u ON o.user_id = u.user_id
WHERE
o.user_id = 42
AND o.status = 'PAID'
GROUP BY u.user_name;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
--
HashAggregate (cost=2929.47..2929.48 rows=1 width=16) (actual time=20.371..20.414 rows=1.00 loops=1)
Group Key: u.user_name
Batches: 1 Memory Usage: 32kB
Buffers: shared hit=930 read=3
-> Nested Loop (cost=12.35..2925.97 rows=699 width=8) (actual time=0.465..15.479 rows=707.00 loops=1)
Buffers: shared hit=930 read=3
-> Index Scan using users_pkey on users u (cost=0.28..8.29 rows=1 width=16) (actual time=0.017..0.029 rows=1.00 loops=1)
Index Cond: (user_id = 42)
Index Searches: 1
Buffers: shared hit=3
-> Bitmap Heap Scan on orders o (cost=12.08..2910.69 rows=699 width=8) (actual time=0.423..6.631 rows=707.00 loops=1)
Recheck Cond: (user_id = 42)
Filter: (status = 'PAID'::text)
Rows Removed by Filter: 279
Heap Blocks: exact=927
Buffers: shared hit=927 read=3
-> Bitmap Index Scan on idx_orders_user (cost=0.00..11.90 rows=997 width=0) (actual time=0.183..0.188 rows=986.00 loops=1
)
Index Cond: (user_id = 42)
Index Searches: 1
Buffers: shared read=3
Planning:
Buffers: shared hit=29 read=2
Planning Time: 0.383 ms
Execution Time: 20.485 ms
(24 rows)
Time: 21.646 ms
test=#
test=#
test=#
test=# DROP INDEX idx_orders_user;
DROP INDEX
Time: 1.940 ms
test=# DROP INDEX idx_orders_status;
DROP INDEX
Time: 1.755 ms
test=#
test=# CREATE INDEX idx_orders_user_status
test-# ON orders (user_id, status);
CREATE INDEX
Time: 331.547 ms
test=#
test=#
test=# EXPLAIN (ANALYZE, BUFFERS)
test-# SELECT
test-# u.user_name,
test-# count(*)
test-# FROM
test-# orders o
test-# JOIN users u ON o.user_id = u.user_id
test-# WHERE
test-# o.user_id = 42
test-# AND o.status = 'PAID'
test-# GROUP BY u.user_name;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
----------------
HashAggregate (cost=37.18..37.19 rows=1 width=16) (actual time=13.835..13.870 rows=1.00 loops=1)
Group Key: u.user_name
Batches: 1 Memory Usage: 32kB
Buffers: shared hit=4 read=3
-> Nested Loop (cost=0.70..33.69 rows=699 width=8) (actual time=0.065..10.329 rows=707.00 loops=1)
Buffers: shared hit=4 read=3
-> Index Scan using users_pkey on users u (cost=0.28..8.29 rows=1 width=16) (actual time=0.012..0.023 rows=1.00 loops=1)
Index Cond: (user_id = 42)
Index Searches: 1
Buffers: shared hit=3
-> Index Only Scan using idx_orders_user_status on orders o (cost=0.42..18.41 rows=699 width=8) (actual time=0.038..3.787 rows=
707.00 loops=1)
Index Cond: ((user_id = 42) AND (status = 'PAID'::text))
Heap Fetches: 0
Index Searches: 1
Buffers: shared hit=1 read=3
Planning:
Buffers: shared hit=22 read=1
Planning Time: 0.212 ms
Execution Time: 13.919 ms
(19 rows)
Time: 14.738 ms
test=#
test=#
这里我们分析一下
Bitmap index
idx_orders_user
↓
Bitmap Index Scan
↓ (构建 bitmap)
Bitmap Heap Scan
↓ (回表 + Filter status)
Nested Loop
↓
HashAggregate
Buffers: shared hit=930 read=3
Heap Blocks: exact=927
Memory Usage: 32kB
index scan
idx_orders_user_status
↓
Index Only Scan
↓
Nested Loop
↓
HashAggregate
Buffers: shared hit=4 read=3 Heap Fetches: 0
其实从上面看一个关键的问题,使用了bitmap会浪费更多的内存,每个进程都要产生930 Buffer hit ,而使用index only scan 只用了 4 buffer hit
差距很大,这个问题在单一的查询中并不是一个关键尤其对现在大内存的数据库服务器,而如果并发超高的情况下就不痛,如果我们有100个并发的情况下,那么区别就比较大了
93000 和 400 的区别,这个内存的区别就变得越来越大了,
93,000 pages × 8 KB ≈ 744,000 KB ≈ 726 MB
400 pages × 8 KB ≈ 3,200 KB ≈ 3.1 MB
同时会产生更多的CPU的消耗,这里就不赘述了,所以在PostgreSQL优化的过程中,要注意查询中是否因为单独建立索引而导致走了bitmap 而因为没有建立联合索引去走index scan.
其实这里还有另一个问题,就是PostgreSQL或者其他的数据库产品都不会考虑你的一个SQL运行的并发性,他们仅仅是针对一次的操作来判断COST,因为bitmap调用IO更少,在数据库中更少的IO是SQL运行中希望看到的,同时我们也可以看到,下面的PG的COST的计算模式。
Total Cost =
Seq/Index Page Cost
+ CPU tuple cost
+ CPU operator cost
但在并发高的情况下,的确更少的CPU的计算,更少的内存更有利,但是,如果是一个有充足的CPU和内存的数据库服务器呢?
其实也是一样,当buffer hit 越多的情况下,会发生内存方面的轻量级的锁,而锁本身也会消耗内存,和CPU。
所以对一些SQL的优化还是要细致一些,我觉得这才是AI优化SQL对数据库重要的。
和架构师沟通那种“一坨”的系统,推荐只能是OceanBase,Why ?
OceanBase Hybrid search 能力测试,平换MySQL的好选择
写了3750万字的我,在2000字的OB白皮书上了一课--记 《OceanBase 社区版在泛互场景的应用案例研究
OceanBase 6大学习法--OBCA视频学习总结第六章
OceanBase 6大学习法--OBCA视频学习总结第五章--索引与表设计
OceanBase 6大学习法--OBCA视频学习总结第五章--开发与库表设计
OceanBase 6大学习法--OBCA视频学习总结第四章 --数据库安装
OceanBase 6大学习法--OBCA视频学习总结第三章--数据库引擎
OceanBase 架构学习--OB上手视频学习总结第二章 (OBCA)
OceanBase 6大学习法--OB上手视频学习总结第一章
没有谁是垮掉的一代--记 第四届 OceanBase 数据库大赛
跟我学OceanBase4.0 --阅读白皮书 (OB分布式优化哪里了提高了速度)
跟我学OceanBase4.0 --阅读白皮书 (4.0优化的核心点是什么)
跟我学OceanBase4.0 --阅读白皮书 (0.5-4.0的架构与之前架构特点)
跟我学OceanBase4.0 --阅读白皮书 (旧的概念害死人呀,更新知识和理念)
OceanBase 学习记录-- 建立MySQL租户,像用MySQL一样使用OB
“合体吧兄弟们!”——从浪浪山小妖怪看OceanBase国产芯片优化《OceanBase “重如尘埃”之歌》
MongoDB “升级项目” 大型连续剧(3)-- 自动校对代码与注意事项
MongoDB “升级项目” 大型连续剧(2)-- 到底谁是"der"
MongoDB “升级项目” 大型连续剧(1)-- 可“生”可不升
MongoDB 大俗大雅,上来问分片真三俗 -- 4 分什么分
MongoDB 大俗大雅,高端知识讲“庸俗” --3 奇葩数据更新方法
MongoDB 大俗大雅,高端的知识讲“通俗” -- 2 嵌套和引用
MongoDB 大俗大雅,高端的知识讲“低俗” -- 1 什么叫多模
MongoDB 合作考试报销活动 贴附属,MongoDB基础知识速通
MongoDB 使用网上妙招,直接DOWN机---清理表碎片导致的灾祸 (送书活动结束)
MongoDB 2023年度纽约 MongoDB 年度大会话题 -- MongoDB 数据模式与建模
MongoDB 麻烦专业点,不懂可以问,别这么用行吗 ! --TTL
免费PolarDB云原生课程,听课“争”礼品,重塑云上知识,提高专业能力
非“厂商广告”的PolarDB课程:用户共创的新式学习范本--7位同学获奖PolarDB学习之星
“当复杂的SQL不再需要特别的优化”,邪修研究PolarDB for PG 列式索引加速复杂SQL运行
“PostgreSQL” 高性能主从强一致读写分离,我行,你没戏!
POLARDB 添加字段 “卡” 住---这锅Polar不背
PolarDB 版本差异分析--外人不知道的秘密(谁是绵羊,谁是怪兽)
PolarDB 答题拿-- 飞刀总的书、同款卫衣、T恤,来自杭州的Package(活动结束了)
PolarDB for MySQL 三大核心之一POLARFS 今天扒开它--- 嘛是火
PostgreSQL 新版本就一定好--由培训现象让我做的实验
说我PG Freezing Boom 讲的一般的那个同学,专帖给你,看看这次可满意
PostgreSQL 无服务 Neon and Aurora 新技术下的新经济模式 (翻译)
“PostgreSQL” 高性能主从强一致读写分离,我行,你没戏!
全世界都在“搞” PostgreSQL ,从Oracle 得到一个“馊主意”开始
PostgreSQL 加索引系统OOM 怨我了--- 不怨你怨谁
PostgreSQL “我怎么就连个数据库都不会建?” --- 你还真不会!
PostgreSQL 稳定性平台 PG中文社区大会--杭州来去匆匆
PostgreSQL 分组查询可以不进行全表扫描吗?速度提高上千倍?
POSTGRESQL --Austindatabaes 历年文章整理
PostgreSQL 查询语句开发写不好是必然,不是PG的锅
这个 PostgreSQL 让我有资本找老板要 鸡腿 鸭腿 !!
MySQL相关文章
一篇为MySQL用户,分析版本核心差异的文章--8.028-8.4的差异
那个MySQL大事务比你稳定,主从延迟低,为什么? Look my eyes! 因为宋利兵宋老师