当我们运行一句最简单的 SQL :SELECT * FROM t;
时,查询结束后会将元组一下子打印到屏幕上。那么就会有一个问题,当元组数量非常多的时候,为什么 SELECT 语句卡一会后,会一下子打印出来?
背景介绍
从我们日常观察中不难发现,当元组较多时,会如下图所示。
一个屏幕装不下,按回车后才能继续浏览下面的元组。
底下通过源码分析和 gdb 调试,一步步探索实际情况下,元组是如何被 SELECT 出来的。
psql 的建立
先来介绍下 psql 大致的工作原理,其主要如下图所示。
当 PG 启动后,Server 端会有进程监听指定端口(默认5432),这时候就可以有多个 psql(客户端进程)连进来。
当在 psql 中输入 SELECT * FROM t;
时,会发生以下几步:
- psql 从输入读到
SELECT * FROM t;
这条语句; - psql 通过 libpq 将该语句发送到 Server 端;
- Server 端执行后将查询结果发还给 psql;
- 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 缓冲区的存在,它们也是一段一段显示出来的,只不过显示的太快了肉眼分辨不出来而已。