postgresql源码学习(56)—— explain是如何快速估算pg表行数的

news2025/1/17 14:03:03

当我们需要大致知道表行数,但又不需要很精确时,可以采用以下方法

一、 统计信息 pg_class.reltuples

最简便的方法是利用pg_class.reltuples,类似oracle的num_rows

postgres=# select reltuples::numeric from pg_class where relname='pgbench_accounts';
 reltuples 
-----------
  20000000
(1 row)

加::numeric是为了防止数字太大,变成科学计数法

postgres=# select reltuples from pg_class where relname='pgbench_accounts';
 reltuples 
-----------
     2e+07
(1 row)

但是这个字段的值需要收集统计信息后才有,如果统计信息过旧,也会不准确

create table tmp001(aid integer) WITH (autovacuum_enabled = off);
insert into tmp001 select aid from pgbench_accounts;

-- pg 14版本没有收集统计信息时,reltuples=-1
select reltuples::numeric from pg_class where relname='tmp001';
 reltuples 
-----------
         0
(1 row)

二、 执行计划 rows

如果没有统计信息或者比较旧了,又不想收集,可以使用explain

1. 用法测试

postgres=# EXPLAIN SELECT 1 FROM tmp001 limit 1;
                               QUERY PLAN                               
------------------------------------------------------------------------
 Limit  (cost=0.00..0.01 rows=1 width=4)
   ->  Seq Scan on tmp001  (cost=0.00..314160.80 rows=22566480 width=4)
(2 rows)

看到在完全没有统计信息的情况下,偏差大概在10%左右

收集之后,偏差明显减少

postgres=# analyze tmp001;
ANALYZE

postgres=# EXPLAIN SELECT 1 FROM tmp001 limit 1;
                               QUERY PLAN                               
------------------------------------------------------------------------
 Limit  (cost=0.00..0.01 rows=1 width=4)
   ->  Seq Scan on tmp001  (cost=0.00..288496.96 rows=20000096 width=4)
(2 rows)

但是注意不要EXPLAIN SELECT count(*),相差很大

postgres=# EXPLAIN SELECT count(*) FROM tmp001;
                                         QUERY PLAN                                         
--------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=193663.38..193663.39 rows=1 width=8)
   ->  Gather  (cost=193663.17..193663.38 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=192663.17..192663.18 rows=1 width=8)
               ->  Parallel Seq Scan on tmp001  (cost=0.00..171829.73 rows=8333373 width=0)
(5 rows)

为了方便获取预估值,可以将执行计划输出转为json格式

postgres=# EXPLAIN (FORMAT JSON) SELECT 1 FROM tmp001 limit 1;
                QUERY PLAN                 
-------------------------------------------
 [                                        +
   {                                      +
     "Plan": {                            +
       "Node Type": "Limit",              +
       "Parallel Aware": false,           +
       "Startup Cost": 0.00,              +
       "Total Cost": 0.01,                +
       "Plan Rows": 1,                    +
       "Plan Width": 4,                   +
       "Plans": [                         +
         {                                +
           "Node Type": "Seq Scan",       +
           "Parent Relationship": "Outer",+
           "Parallel Aware": false,       +
           "Relation Name": "tmp001",     +
           "Alias": "tmp001",             +
           "Startup Cost": 0.00,          +
           "Total Cost": 288496.96,       +
           "Plan Rows": 20000096,         +
           "Plan Width": 4                +
         }                                +
       ]                                  +
     }                                    +
   }                                      +
 ]
(1 row)

2. 按表名统计

创建函数,将Plan Rows转换成输出:

CREATE OR REPLACE FUNCTION countit(name,name)               
RETURNS float4           
LANGUAGE plpgsql AS          
$$DECLARE                                                              
    v_plan json;                      
BEGIN                            
    EXECUTE format('EXPLAIN (FORMAT JSON) SELECT 1 FROM %I.%I', $1,$2)                                     
        INTO v_plan;                                                                                                    
    RETURN v_plan #>> '{0,Plan,"Plan Rows"}';    
END;  
$$;

执行函数

postgres=# select countit('public','tmp001')::numeric;
 countit  
----------
 20011000
(1 row)


3. 查询所有表

SELECT  
    relname AS table,  
    CASE WHEN relkind = 'r'  
        THEN reltuples::numeric
        ELSE countit(n.nspname,relname)::numeric
    END AS approximate_count
FROM  
    pg_catalog.pg_class c  
JOIN  
    pg_catalog.pg_namespace n ON (c.relkind IN ('r','v') AND c.relnamespace = n.oid)
ORDER BY 2 DESC;    
    
    
                     table             | approximate_count 
---------------------------------------+-------------------
 tmp001                                |          20000000
 test                                  |           1608000
 pgbench_branches                      |              5718

4. 按SQL语句统计

CREATE OR REPLACE FUNCTION countit(text)                    
RETURNS float4           
LANGUAGE plpgsql AS          
$$DECLARE               
    v_plan json;                
BEGIN                      
    EXECUTE 'EXPLAIN (FORMAT JSON) '||$1                                
        INTO v_plan;                                                                       
    RETURN v_plan #>> '{0,Plan,"Plan Rows"}';  
END;  
$$; 

用法测试

postgres=# create table t1234(id int, info text);  
CREATE TABLE  

postgres=# insert into t1234 select generate_series(1,1000000),'test';  
INSERT 0 1000000  

postgres=# analyze t1234;  
ANALYZE  

postgres=# select countit('select * from t1234 where id<1000');  
 countit   
---------  
     954  
(1 row)  

postgres=# select countit('select * from t1234 where id between 1 and 1000 or (id between 100000 and 101000)');  
 countit   
---------  
    1931  
(1 row)  

三、 源码学习

       做完前面测试之后有个疑问:explain中的rows是怎么估算的,在表没有和有统计信息时是否有区别?

1. explain中rows的估算位于哪个函数

通过Dtrace打印出所有调用的函数,可以参考:postgresql源码学习(50)—— 小白学习Dtrace追踪源码函数调用_Hehuyi_In的博客-CSDN博客

vi function.c
// 内容如下
probe process("/data/postgres/base/14.0/bin/postgres").function("*") {
        printf("%s: %s\n", execname(), ppfunc());
}

执行脚本

stap function.c > mylog_02.txt

虽然不知道预估行数的函数确切叫什么,但可以猜会带有row或者estimate,试一试

[root@linux01 ~]# cat mylog_02.txt |grep row
postgres: get_row_security_policies
postgres: preprocess_rowmarks
postgres: distribute_row_identity_vars
postgres: clamp_row_est
postgres: command_tag_display_rowcount

        这里看名字clamp_row_est有点像,但看内容会发现它不是。它的作用主要是避免rows出现过大或者过小的值,限在一个范围内。

        同时可以看到当rows<=1时,都设置=1,所以当rows=1,也有可能这个表是空表或者预估的行数有问题,这里跟oracle有点像。

/*
 * clamp_row_est
 *		Force a row-count estimate to a sane value.
 */
double
clamp_row_est(double nrows)
{
	/*
	 * Avoid infinite and NaN row estimates.  Costs derived from such values
	 * are going to be useless.  Also force the estimate to be at least one
	 * row, to make explain output look better and to avoid possible
	 * divide-by-zero when interpolating costs.  Make it an integer, too.
	 */
	if (nrows > MAXIMUM_ROWCOUNT || isnan(nrows))
		nrows = MAXIMUM_ROWCOUNT;
	else if (nrows <= 1.0)
		nrows = 1.0;
	else
		nrows = rint(nrows);

	return nrows;
}

[root@linux01 ~]# cat mylog_02.txt |grep estimate
postgres: estimate_rel_size
postgres: table_relation_estimate_size
postgres: heapam_estimate_rel_size
postgres: table_block_relation_estimate_size
postgres: set_baserel_size_estimates

这个看起来就比较像了,最终看主要是在table_block_relation_estimate_size函数中

void
table_block_relation_estimate_size(Relation rel, int32 *attr_widths,
								   BlockNumber *pages, double *tuples,
								   double *allvisfrac,
								   Size overhead_bytes_per_tuple,
								   Size usable_bytes_per_page)
{
	BlockNumber curpages;
	BlockNumber relpages;
	double		reltuples;
	BlockNumber relallvisible;
	double		density;

	/* 从存储管理器smgr中获取表实际页数(例如根据表大小估算) */
	curpages = RelationGetNumberOfBlocks(rel);

	/* 从pg_class中获取页数、行数、可见页数等 */
	relpages = (BlockNumber) rel->rd_rel->relpages;
	reltuples = (double) rel->rd_rel->reltuples;
	relallvisible = (BlockNumber) rel->rd_rel->relallvisible;

	/*
	 * 如果表从未执行过vacuumed(包括统计信息收集),使用最小预估值为10 pages,因为通常这种表是新建的,非常小。
	 */
	if (curpages < 10 &&
		reltuples < 0 &&
		!rel->rd_rel->relhassubclass)
		curpages = 10;

	/* report estimated # pages,预估的页数 */
	*pages = curpages;

	/* quick exit if rel is clearly empty,若为空表,直接记为0退出 */
	if (curpages == 0)
	{
		*tuples = 0;
		*allvisfrac = 0;
		return;
	}

	/* estimate number of tuples from previous tuple density,计算元组密度。若统计信息中行数与页数均不为0,则密度=行数/页数,即每页有多少行 */
	if (reltuples >= 0 && relpages > 0)
		density = reltuples / (double) relpages;
	else
	{
		/*
		 * 如果没有统计信息数据,则根据数据类型和列宽度来估算
		 */
		int32		tuple_width;
        // 先根据数据类型算列宽度,例如int就是4
		tuple_width = get_rel_data_width(rel, attr_widths);
        // overhead_bytes_per_tuple包括tuple header和item pointer
        // 其定义为 #define HEAP_OVERHEAD_BYTES_PER_TUPLE (MAXALIGN(SizeofHeapTupleHeader) + sizeof(ItemIdData))
		tuple_width += overhead_bytes_per_tuple;

		/* usable_bytes_per_page是为每行元组预估的每页已用大小(不包括页头及特殊空间)  */
		density = usable_bytes_per_page / tuple_width;
	}

    // 最后行数预估均为 密度(每页有多少行)*当前页数
	*tuples = rint(density * (double) curpages);
...
}

2. 没有统计信息时的估算

下面的测试是后面补的,版本和数据量跟前面不一致,可以忽略,主要看原理。

create table tmp001(aid integer) WITH (autovacuum_enabled = off);
insert into tmp001 select aid from pgbench_accounts; -- 1000000行

-- pg 14版本没有收集统计信息时,reltuples=-1
select reltuples::numeric from pg_class where relname='tmp001';
 reltuples 
-----------
        -1
(1 row)

gdb跟踪一把

postgres=# EXPLAIN SELECT 1 FROM tmp001;
                           QUERY PLAN                           
----------------------------------------------------------------
 Seq Scan on tmp001  (cost=0.00..25730.40 rows=1848240 width=4)

(gdb) p curpages
$2 = 7248

(gdb) p reltuples
$3 = -1

(gdb) p tuple_width
$4 = 4
(gdb) p overhead_bytes_per_tuple
$6 = 28

(gdb) p tuple_width
$5 = 32
 
 (gdb) p density   (density = usable_bytes_per_page / tuple_width;)
$7 = 255
(gdb) p usable_bytes_per_page
$8 = 8168

(gdb) p *tuples
$11 = 1848240

*tuples = rint(density * (double) curpages);  1848240即255*7248

3. 有统计信息时的估算

postgres=# analyze tmp001;
ANALYZE
postgres=# EXPLAIN SELECT 1 FROM tmp001;
                           QUERY PLAN                           
----------------------------------------------------------------
 Seq Scan on tmp001  (cost=0.00..17248.00 rows=1000000 width=4)
(1 row)

(gdb) p curpages
$2 = 7248
(gdb) p relpages
$4 = 7248

(gdb)  p reltuples
$6 = 1000000

(gdb) p *tuples
$9 = 1000000  

       tuples=density*curpages=(reltuples/relpages)*curpages,而这里relpages=curpages,因此这里执行计划预估的行数跟reltuples是一致的。

       但是对于大表,由于统计信息也是抽样的,因此relpages与curpages可能一致,因此执行计划预估的行数并不总是等于reltuples(比如最前面2000万行那个例子它们就是不相等的)。

参考
https://github.com/digoal/blog/blob/master/201509/20150919_02.md

还有个遗留的问题:为什么explain select count(*) 差距会特别大,暂时没找到答案,留待发现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/678330.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VUE 2X 表单数据过滤器 ⑨

目录 文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持✨ V u e j s Vuejs Vuejs收集表单数据过滤器 使用 C o o k i e Cookie Cookie 影响总结 文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹…

【计算机组成原理】RISC-V模型机的有限状态控制器设计

目录 一、RISC-V模型机的目标指令集 二、RISC-V模型机的部件设计 三、运算及传送指令的数据通路设计 四、访存指令的数据通路设计 五、转移类指令的数据通路设计 六、RISC-V模型机控制单元CU的有限状态机设计 一、RISC-V模型机的目标指令集 取指令并译码&#xff1a;根据…

编译原理笔记16:自下而上语法分析(3)构造 DFA、DFA 对下一步分析的指导(有效项目)

目录 由 NFA 用子集法构造 DFA由 LR(0) 项目直接构造识别活前缀的 DFA构造 DFA求拓广文法 GCLOSURE & GO例&#xff1a; 构造 DFA DFA 指导下一步分析有效项目 看了前面的内容&#xff0c;我们已经了解到&#xff1a;分析表和驱动器算法&#xff0c;是 LR 分析器的核心。 …

实训四:索引与视图 - SQL视图(teachingdb数据库)

SQL视图的定义与操纵 第1关&#xff1a;创建视图任务描述相关知识视图的定义创建视图 编程要求测试说明参考代码 第2关&#xff1a;创建视图-练习一任务描述相关知识编程要求测试说明参考代码 第1关&#xff1a;创建视图 任务描述 本关任务&#xff1a;建立计算机系的学生的视…

团体程序设计天梯赛-练习集L1篇⑧

&#x1f680;欢迎来到本文&#x1f680; &#x1f349;个人简介&#xff1a;Hello大家好呀&#xff0c;我是陈童学&#xff0c;一个与你一样正在慢慢前行的普通人。 &#x1f3c0;个人主页&#xff1a;陈童学哦CSDN &#x1f4a1;所属专栏&#xff1a;PTA &#x1f381;希望各…

C语言scanf/fscanf/sscnaf和printf/fprintf/sprintf的区别

总结 1.scanf/printf 是标准输入输出流函数(键盘、屏幕)。 2.fscanf/fprintf 适用于所有输入输出流(文件、键盘、屏幕…)。 3.sscanf/sprintf 是把格式化的数据写入某个字符串中&#xff0c;从某个字符串中读取格式化的数据。 第一组&#xff1a;scanf/printf scanf/printf是…

Oracle数据库从入门到精通系列之十八:详细总结Oracle数据库核心知识点

Oracle数据库从入门到精通系列之十八&#xff1a;详细总结Oracle数据库核心知识点 一、Oracle数据库核心概念二、Oracle非容器数据库三、Oracle容器数据库四、容器数据库和非容器数据库的区别五、Oracle数据库多租户六、Oracle数据库多租户数据库模型七、Oracle数据库类型八、O…

实训四:索引与视图 - MySQL开发技巧 - 索引

MySQL开发技巧 - 索引 任务描述相关知识索引是什么索引的分类索引的创建和删除查询表中索引 编程要求测试说明代码参考&#xff1a; 任务描述 本关任务&#xff1a;按照要求完成索引的创建。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a; 索引是什么&#…

【Leetcode60天带刷】day31回溯算法——455.分发饼干 ,376. 摆动序列 , 53. 最大子序和

​ 题目&#xff1a; 455. 分发饼干 假设你是一位很棒的家长&#xff0c;想要给你的孩子们一些小饼干。但是&#xff0c;每个孩子最多只能给一块饼干。 对每个孩子 i&#xff0c;都有一个胃口值 g[i]&#xff0c;这是能让孩子们满足胃口的饼干的最小尺寸&#xff1b;并且每块…

Android 13(T) - binder阅读(3)- binder相关的类

原先准备在binder阅读&#xff08;3&#xff09;中记录ServiceManager的使用&#xff0c;但是写着写着发现&#xff0c;如果不了解和binder相关的类&#xff0c;那么阅读起来将会由很多困惑&#xff0c;所以就先来记录binder相关的类了。记录完发现特别凌乱…先就这样吧。 1 UM…

【致敬未来的攻城狮计划】打卡3:点亮LED

点亮LED 本文主要参考文章&#xff1a;【致敬未来的攻城狮计划】— 连续打卡第十一天&#xff1a;FSP固件库开发点亮第一个灯。_嵌入式up的博客-CSDN博客 在32阶段我们已经接触过类似做法了。初始化引脚模式&#xff08;可以手动库函数&#xff0c;或者在工具包图形化界面里配…

实训四:索引与视图 - MySQL开发技巧 - 视图

MySQL开发技巧 - 视图 任务描述相关知识视图的定义创建视图操作视图删除视图 编程要求测试说明参考代码 任务描述 本关任务&#xff1a;通过学习视图&#xff0c;创建一个单表视图和一个多表视图。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a; 视图的定义…

工地扬尘智能监测系统 yolov7

工地扬尘智能监测系统通过yolov7网络算法模型技术&#xff0c;实时监测工地施工中的扬尘情况。工地扬尘智能监测系统利用AI视频智能分析技术&#xff0c;并将数据传输到数据中心进行分析。YOLOv7 的发展方向与当前主流的实时目标检测器不同&#xff0c;研究团队希望它能够同时支…

数据库管理-第八十四期 X10M来了(20230624)

数据库管理 2023-06-24 第八十四期 X10M来了1 Intel -> AMD2 PMEM -> XRMEM3 DDR4 -> DDR54 Flash cards总结 第八十四期 X10M来了 在第四十三期的时候&#xff0c;我曾经憧憬过Exadata X10M的到来&#xff0c;Oracle于6月22日正式公布Exadata X10M系列。其实5月已经…

chatgpt赋能python:Python在电气行业中的应用——从数据分析到自动化控制

Python在电气行业中的应用——从数据分析到自动化控制 介绍 Python语言作为一种高级编程语言&#xff0c;越来越受到电气行业的关注。随着互联网、物联网以及大数据时代的到来&#xff0c;电气行业需要将传统的工业控制与现代化的数据分析、智能决策等技术相结合&#xff0c;…

Java——《面试题——Dobbo篇》

前文 java——《面试题——基础篇》 Java——《面试题——JVM篇》 Java——《面试题——多线程&并发篇》 Java——《面试题——Spring篇》 Java——《面试题——SpringBoot篇》 Java——《面试题——MySQL篇》​​​​​​ Java——《面试题——SpringCloud》 目录…

springboot+mybatis笔记学习

1.环境搭建 1.引入pom依赖 <dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version> </dependency> <dependency><groupId>org…

人工智能(2):机器学习算法分类

根据数据集组成不同&#xff0c;可以把机器学习算法分为&#xff1a; 监督学习无监督学习半监督学习强化学习 1 监督学习 定义&#xff1a; 输入数据是由输入特征值和目标值所组成。 函数的输出可以是一个连续的值(称为回归&#xff09;&#xff0c;或是输出是有限个离散值&…

07- c语言字符串 (C语言)

一 字符串的定义及基本使用 1、什么是字符串 被双引号引用的字符集合&#xff01;例如&#xff1a;”hello” 、”world”&#xff0c;或者是以 \0 结尾的字符数组&#xff01;&#xff01;&#xff01; 比如&#xff1a;char ch[] {h, e, \0} 注意&#xff1a;”hello” 中…

Win10同时安装MYSQL5.7和MYSQL8.0版本

一、准备好两个MySQL版本的压缩包 官网下载网址&#xff1a;https://dev.mysql.com/downloads/ 二、安装 MYSQL5.7 2.1、解压文件夹&#xff0c;然后新建一个 my.ini文件 my.ini文件内容: [mysql] # 设置mysql客户端默认字符集 default-character-setutf8 port 3305 [mysq…