PG中文社区 /

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

原作者:施博文  创作时间:2021-09-14 09:08:54+08
wangliyun 发布于2021-09-14 09:08:54           评论: 0   浏览: 6702   顶: 943  踩: 946 

作者

施博文,PostgreSQL 爱好者,目前就职于腾讯

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

背景介绍

从我们日常观察中不难发现,当元组较多时,会如下图所示。

CENTER_PostgreSQL_Community

一个屏幕装不下,按回车后才能继续浏览下面的元组。

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

psql 的建立

先来介绍下 psql 大致的工作原理,其主要如下图所示。

CENTER_PostgreSQL_Community

当 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 收到这个标记后,终于能显示查询结果了。

下面这张图可以帮助大家更直观的理解

CENTER_PostgreSQL_Community

psql 端的处理

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

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

CENTER_PostgreSQL_Community

gdb 进入 printalignedtext 函数,其代码逻辑告诉我们是一行一行打印出来的。当已经 fpuc 出一些字符后,我们手动调用 call fflush(fout) ,强制刷出缓冲区,其显示结果如下图所示。

CENTER_PostgreSQL_Community

接下来我们再多打印一些元组,这时候左下角的 : 出现了,当前屏幕无法显示全部元组。因此疯狂按回车,直到显示不了为止。

CENTER_PostgreSQL_Community

题外话

上文提到 “当 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;

运行后的结果如下图所示。

CENTER_PostgreSQL_Community

然后发现 psql 进程直接被 Kill 掉了。

总结

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

PostgreSQL中文社区欢迎广大技术人员投稿

投稿邮箱:press@postgres.cn

CENTER_PostgreSQL_Community


评论:0   浏览: 6702                   顶: 943  踩: 946 

请在登录后发表评论,否则无法保存。


发表评论:
加入我们
QQ群1:5276420
QQ群2:3336901
QQ群3:254622631
文档群:150657323
文档翻译平台:按此访问
社区邮件列表:按此订阅
扫码关注
© PostgreSQL中文社区 ... (自2010年起)