PostgreSQL 中 SELECT 的元组是如何出现的

当我们运行一句最简单的 SQL :SELECT * FROM t; 时,查询结束后会将元组一下子打印到屏幕上。那么就会有一个问题,当元组数量非常多的时候,为什么 SELECT 语句卡一会后,会一下子打印出来?

背景介绍

从我们日常观察中不难发现,当元组较多时,会如下图所示。
在这里插入图片描述
一个屏幕装不下,按回车后才能继续浏览下面的元组。

底下通过源码分析和 gdb 调试,一步步探索实际情况下,元组是如何被 SELECT 出来的。

psql 的建立

先来介绍下 psql 大致的工作原理,其主要如下图所示。
在这里插入图片描述
当 PG 启动后,Server 端会有进程监听指定端口(默认5432),这时候就可以有多个 psql(客户端进程)连进来。
当在 psql 中输入 SELECT * FROM t; 时,会发生以下几步:

  1. psql 从输入读到 SELECT * FROM t; 这条语句;
  2. psql 通过 libpq 将该语句发送到 Server 端;
  3. Server 端执行后将查询结果发还给 psql;
  4. psql 将查询结果打印到屏幕上。

底下先来研究下 Server 端执行器部分的代码。

Server 端的执行

当 Server 端接收到 SQL 语句后,会经过词法分析、语法分析、语义分析、生成执行计划,这时候执行器会执行生成的执行计划,得到最终的结果。
而 PostgreSQL 的执行器采用的是火山模型,这里不详细展开,我们只需要知道元组是扫描到一条就向上吐一条,然后再去扫描下一条
为了便于理解,我们来分析下删减版的执行器的主要函数 ExecutePlan

static void
ExecutePlan()
{
	TupleTableSlot *slot;

	for (;;)
	{
		/* 调用下层函数获取一条元组,其结果就是一个 slot */
		slot = ExecProcNode(planstate);
		
		/* 如果 slot 为空,说明所有元组都已经获取完了,跳出死循环 */
		if (TupIsNull(slot))
			break;
			
		/* 标记该元组要发出去,比如发给 psql 客户端 */
		if (sendTuples)
		{
			/* 该函数指针会调用 printtup 函数,而 printtup 会把这个 slot 
			   通过 libpq 发送给 psql 客户端
			 */
			if (!dest->receiveSlot(slot, dest))
				break;
		}
	}

}

如果在该函数运行中打上断点,我们会发现 psql 客户端无论等待多久,都不会将结果打印到屏幕。因此可以推出一个初步结论:扫描到的元组并不会扫一部分显示一部分,而是等所有元组都扫描完后一下子显示出来。
继续顺着函数走下去,当 Server 端执行器运行完毕,执行 EndCommand 函数时,会调用 pq_putmessage('C',...) 标记语句运行完成。然而这时候 psql 并没有显示出结果。

为了验证是否 psql 收到这个标记后就会直接显示结果,我们在 gdb 中 call PqCommMethods->flush() 函数,将这个 message 发过去,但依然没有显示

Server 端再走下去,到 ReadyForQuery 时,会发送一个 ‘Z’ 标记,标识这时候 Server 已经可以接受新的语句了,当 psql 收到这个标记后,终于能显示查询结果了。

下面这张图可以帮助大家更直观的理解
在这里插入图片描述

psql 端的处理

上一章我们主要讲述了 Server 端的执行流程,这一章将讲述客户端 psql 的对应处理过程。

psql 的代码放在 src/bin/psql 文件夹下,其 main 函数位于 startup.c 中。当 Server 端发来元组时, psql 会进行对应的解析处理,并将它们全部存到内存中(这在最后一章会提到),这里我们不做详细讲解。可以理解为,当收到结束标记时,所有的元组均被存到一个数组中

具体的打印函数调用堆栈如下图所示。可以看出,print_aligned_text 函数就是最终的打印函数。其上层函数为其增加方格线、列名称等。
在这里插入图片描述
gdb 进入 print_aligned_text 函数,其代码逻辑告诉我们是一行一行打印出来的。当已经 fpuc 出一些字符后,我们手动调用 call fflush(fout) ,强制刷出缓冲区,其显示结果如下图所示。
在这里插入图片描述
接下来我们再多打印一些元组,这时候左下角的 : 出现了,当前屏幕无法显示全部元组。因此疯狂按回车,直到显示不了为止。
在这里插入图片描述

题外话

上文提到 “当 Server 端发来元组时, psql 会进行对应的解析处理,并将它们全部存到内存中”。这时候就衍生出了一个问题——如果有一个内存为 2G 的机器,里面的 PG 里有一张表 t 存了 3G 的数据,那么运行 SELECT * FROM t; 会发生什么现象?

为了验证这个问题,我特地找了个 1 核 2G 的docker,在里面装上PG并插入了约 3GB 的数据。然后执行如下语句

SELECT pg_size_pretty(pg_relation_size('table_name')); --查看表 t 的大小

SELECT * FROM t;

运行后的结果如下图所示。
在这里插入图片描述
然后发现 psql 进程直接被 Kill 掉了。

总结

本文主要研究了 PostgreSQL 中运行 Select 语句后元组是如何出现的,也算是解决了困惑我很久的一个问题。当 psql 终端开始显示元组时,说明所有的元组都已经被扫描完毕存在内存里。这时候所要做的只是把它们打印到屏幕上,而由于 IO 缓冲区的存在,它们也是一段一段显示出来的,只不过显示的太快了肉眼分辨不出来而已。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

总想玩世不恭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值