PostgreSQL源码分析

前端之家收集整理的这篇文章主要介绍了PostgreSQL源码分析前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

Postgresql源码结构



Postgresql的使用形态


Postgresql采用C/S(客户机/服务器)模式结构。应用层通过INET或者Unix Socket利用既定的协议与数据库服务器进行通信。
另外,还有一种‘Standalone Backend’使用的方式,虽然通过这种方式也可以启动服务器,但是一般只在数据库的初始化(Postgresql


的cluster的初始化,相当于其他数据库的instance的初始化)、紧急维护的时候使用,所以简单来说可以认为Postgresql是使用C/S的形


式进行访问的。
Postgresql把客户端称为前端(Frontend),把服务器端成为后端(Backend),后端有复数个进程构成,这个在后面会进行说明。
前端和后端通信的协议在Postgresql的官方文档中的《前端和后端的通信协议》一章中有详细的说明。简单来说,大体的工作模式是:


前端向后端发送查询sql文,然后后端通过复数个报文把结果返回给前端。
由于需要进行连接的初始化、错误等各种各样处理,Postgresql的协议的处理也是相当复杂,如果要自己从头实现这些协议的处理的话


,还是相当麻烦的,所以Postgresql本身提供了C语言写的libpq这样一个协议处理库,利用这个库可以比较轻松地和后端进行通信。


Postgresql的话除了C以外,还支持Perl和PHP等其他语言,这些语言在内部也调用了libpq.
也有不使用libpq而直接与Postgresql通信的库。比较具有代表性的是Java,Postgresql的JDBC驱动是不依赖于libpq直接与Postgresql


通信的.
另外后端的话,比较核心的是进行数据库处理的数据库引擎(Database Engine)。 数据库引擎可以对用户所编写的函数进行解析和处理


用户如果能够利用好这个功能的话,可以柔软地扩展Postgresql功能。 比较经常使用的是存储过程(Postgresql中称为用户自定义


函数),Postgresql支持用户定义函数的语言如下:
语言 对应的自定义函数
C C函数
sql sql 函数
类似Oracle的PL/sql的语言 PL/pgsql
Perl PL/Perl
Python PL/Python


Postgresql的话,用户可以自定义语言处理引擎。各种服务器脚本语言的解析引擎,以第三方的形式存在,主要的处理语言有Ruby、


Java以及PHP等。
Postgresql的结构


这里的话,再详细看看Postgresql的结构。 后端由几个进程构成。


Potgres(常驻进程)
管理后端的常驻进程,也称为’postmaster’。其默认监听UNIX Domain Socket和TCP/IP(Windows等,一部分的平台只监听tcp/ip)的


5432端口,等待来自前端的的连接处理。监听的端口号可以在Postgresql的设置文件postgresql.conf里面可以改。
一旦有前端连接过来,postgres会通过fork(2)生成子进程。没有Fork(2)的windows平台的话,则利用createProcess()生成新的进程。


这种情形的话,和fork(2)不同的是,父进程的数据不会被继承过来,所以需要利用共享内存把父进程的数据继承过来。
Postgres(子进程)
子进程根据pg_hba.conf定义的安全策略来判断是否允许进行连接,根据策略,会拒绝某些特定的IP及网络,或者也可以只允许某些特定


用户或者对某些数据库进行连接。
Postgres会接受前端过来的查询,然后对数据库进行检索,最好把结果返回,有时也会对数据库进行更新。更新的数据同时还会记录在


事务日志里面(Postgresql称为WAL日志),这个主要是当停电的时候,服务器当机,重新启动的时候进行恢复处理的时候使用的。另外


,把日志归档保存起来,可在需要进行恢复的时候使用。在Postgresql 9.0以后,通过把WAL日志传送其他的postgresql,可以实时得进


数据库复制,这就是所谓的‘数据库复制’功能
其他的进程
Postgres之外还有一些辅助的进程。这些进程都是由常驻postgres启动的进程。
Writer process
Writer process在适当的时间点把共享内存上的缓存写往磁盘。通过这个进程,可以防止在检查点的时候(checkpoint),大量的往磁盘写


而导致性能恶化,使得服务器可以保持比较稳定的性能。Background writer起来以后就一直常驻内存,但是并非一直在工作,它会在工


作一段时间后进行休眠,休眠的时间间隔通过postgresql.conf里面的参数bgwriter_delay设置,默认是200微秒。
这个进程的另外一个重要的功能是定期执行检查点(checkpoint)。
检查点的时候,会把共享内存上的缓存内容数据库文件写,使得内存和文件的状态一致。通过这样,可以在系统崩溃的时候可以缩短


从WAL恢复的时间,另外也可以防止WAL无限的增长。 可以通过postgresql.conf的checkpoint_segments、checkpoint_timeout指定执行


检查点的时间间隔。
WAL writer process
WAL writer process把共享内存上的WAL缓存在适当的时间点往磁盘写,通过这样,可以减轻后端进程在写自己的WAL缓存时的压力,提


性能。另外,非同步提交设为true的时候,可以保证在一定的时间间隔内,把WAL缓存上的内容写入WAL日志文件
Archive process
Archive process把WAL日志转移到归档日志里。如果保存了基础备份以及归档日志,即使实在磁盘完全损坏的时候,也可以回复数据库


到最新的状态。
stats collector process
统计信息的收集进程。收集好统计表的访问次数,磁盘的访问次数等信息。收集到的信息除了能被autovaccum利用,还可以给其他数据


管理员作为数据库管理的参考信息。
Logger process
把postgresql的活动状态写到日志信息文件(并非事务日志),在指定的时间间隔里面,对日志文件进行rotate.
Autovacuum启动进程
autovacuum launcher process是依赖于postmaster间接启动vacuum进程。而其自身是不直接启动自动vacuum进程的。通过这样可以提高


系统的可靠性。
自动vacuum进程
autovacuum worker process进程实际执行vacuum的任务。有时候会同时启动多个vacuum进程。
wal sender / wal receiver
wal sender 进程和wal receiver进程是实现postgresql复制(streaming replication)的进程。Wal sender进程通过网络传送WAL日志,


而其他Postgresql实例的wal receiver进程则接收相应的日志。Wal receiver进程的宿主Postgresql(也称为Standby)接受到WAL日志


后,在自身的数据库上还原,生成一个和发送端的Postgresql(也称为Master)完全一样的数据库
后端的处理流程


下面看看数据库引擎postgres子进程的处理概要。为了简单起见下面的说明中,把backend process简称为backend。Backend的main函数


是PostgresMain (tcop/postgres.c)。
接收前端发送过来的查询(sql文)
sql文是单纯的文字,电脑是认识不了的,所以要转换成比较容易处理的内部形式构文树parser tree,这个处理的称为构文解析。构文解


析的模块称为parser.这个阶段只能够使用文字字面上得来的信息,所以只要没语法错误之类的错误,即使是select不存在的表也不会报


错。这个阶段的构文树被称为raw parse tree. 构文处理的入口在raw_parser (parser/parser.c)。
构文树解析完以后,会转换为查询树(Query tree)。这个时候,会访问数据库,检查表是否存在,如果存在的话,则把表名转换为OID。


这个处理称为分析处理(Analyze),进行分析处理的模块是analyzer。 另外,Postgresql代码里面提到构文树parser tree的时候,更


多的时候是指查询树Query tree。分析处理的模块的入口在parse_analyze (parser/analyze.c)
Postgresql还通过查询语句的重写实现视图(view)和规则(rule),所以需要的时候,在这个阶段会对查询语句进行重写。这个处理称为


重写(rewrite),重写的入口在QueryRewrite (rewrite/rewriteHandler.c)。
通过解析查询树,可以实际生成计划树。生成查询树的处理称为‘执行计划处理’,最关键是要生成估计能在最短的时间内完成的计划


树(plan tree)。这个步骤称为’查询优化’(不叫query optimize,而是optimize),而完成这个处理的模块称为查询优化器(不叫query


optimizer,而是optimizer,或者称为planner)。执行计划处理的入口在standard_planner (optimizer/plan/planner.c)。
按照执行计划里面的步骤可以完成查询要达到的目的。运行执行计划树里面步骤的处理称为执行处理‘execute’,完成这个处理的模块


称为执行器‘Executor’,执行器的入口地址为,ExecutorRun (executor/execMain.c)
执行结果返回给前端。
返回到步骤一重复执行。
Postgresql的源码


现在基本上理解了Postgresql的大体的结构,我们再来看看Postgresql代码的结构。 Postgresql初期的时候,大概只有20万行左右的代


码,现在已经发展到100万行了。这个量来说,没有指导读起来是极为难理解的,这里把大概的代码结构说明一下,让大家对源码的结构


有个理解。
第一级目录结构


进入Postgresql的源码目录后,第一级的结构如下表所示。在这一级里,通过执行如下命令configure;make;make install可以立即进行


简单的安装,实际上从Postgresql源码安装是极为简单的。
文件目录 说明
COPYRIGHT 版权信息
GUNMakefile 第一级目录的 Makefile
GUNMakefile.in Makefile 的雏形
HISTORY 修改历史
INSTALL 安装方法简要说明
Makefile Makefile模版
README 简单说明
aclocal.m4 config 用的文件的一部分
config/ config 用的文件的目录
configure configure 文件
configure.in configure 文件的雏形
contrib/ contribution 程序
doc/ 文档目录
src/ 代码目录


Postgresql 的src下面有。
文件目录 说明
DEVELOPERS 面向开发人员的注视
Makefile Makefile
Makefile.global make 的设定值(从configure生成的)
Makefile.global.in Configure使用的Makefile.global的雏形
Makefile.port 平台相关的make的设定值,实际是一个到makefile/Makefile的连接. (从configure生成的)
Makefile.shlib 共享库用的Makefile
backend/ 后端的源码目录
bcc32.mak Win32 ポート用の Makefile (Borland C++ 用)
bin/ psql 等 UNIX命令的代码
include/ 文件
interfaces/ 前端相关的库的代码
makefiles/ 平台相关的make 的设置值
nls-global.mk 信息目录用的Makefile文件的规则
pl/ 存储过程语言的代码
port/ 平台移植相关的代码
template/ 平台相关的设置值
test/ 各种测试脚本
timezone/ 时区相关代码
tools/ 各自开发工具和文档
tutorial/ 教程
win32.mak Win32 ポート用の Makefile (Visual C++ 用)


这里比较核心的是backend,bin,interface这几个目录。Backend是对应于后端,bin和interface对应于前端。
bin里面有pgsql,initdb,pg_dump等各种工具的代码。interface里面有Postgresql的C语言的库libpq,另外可以在C里嵌入sql的ECPG命令


的相关代码
Backend目录的结构如下:
目录文件 说明
Makefile makefile
access/ 各种存储访问方法(在各个子目录下) common(共同函数)、gin (Generalized Inverted Index通用逆向索引)
gist (Generalized Search Tree通用索引)、 hash (哈希索引)、heap (heap的访问方法)、
index (通用索引函数)、 nbtree (Btree函数)、transam (事务处理)
bootstrap/ 数据库的初始化处理(initdb的时候)
catalog/ 系统目录
commands/ SELECT/INSERT/UPDATE/DELETE以为的sql文的处理
executor/ 执行器(访问的执行)
foreign/ FDW(Foreign Data Wrapper)处理
lib/ 共同函数
libpq/ 前端/后端通信处理
main/ postgres的主函数
nodes/ 构文树节点相关的处理函数
optimizer/ 优化器
parser/ sql构文解析器
port/ 平台相关的代码
postmaster/ postmaster的主函数 (常驻postgres)
replication/ streaming replication
regex/ 正则处理
rewrite/ 规则及视图相关的重写处理
snowball/ 全文检索相关(语干处理)
storage/ 共享内存、磁盘上的存储、缓存等全部一次/二次记录管理(以下的目录)buffer/(缓存管理)、 file/(文件)、
freespace/(Fee Space Map管理) ipc/(进程间通信)、large_object /(大对象的访问函数)、
lmgr/(锁管理)、page/(页面访问相关函数)、 smgr/(存储管理器)
tcop/ postgres (数据库引擎的进程)的主要部分
tsearch/ 全文检索
utils/ 各种模块(以下目录) adt/(嵌入的数据类型)、cache/(缓存管理)、 error/(错误处理)、fmgr/(函数管理)、
hash/(hash函数)、 init/(数据库初始化、postgres的初期处理)、 mb/(多字节文字处理)、
misc/(其他)、mmgr/(内存的管理函数)、 resowner/(查询处理中的数据(buffer pin及表锁)的管理)、
sort/(排序处理)、time/(事务的 MVCC 管理)


backend等的代码的头文件包含在include里面。其组织虽然与backend的目录结构类似,但是并非完全相同,基本上来说下一级的子目录


不再设下一级目录。例如backend的目录下面有utils这个目录,而util下面还有adt这个子目录,但是include里面省略了这个目录,变


成了扁平的结构。
access/
bootstrap/
c.h
catalog/
commands/
dynloader.h
executor/
fmgr.h
foreign/
funcapi.h
getaddrinfo.h
getopt_long.h
lib/
libpq/
mb/
miscadmin.h
nodes/
optimizer/
parser/
pg_config.h
pg_config.h.in
pg_config.h.win32
pg_config_manual.h
pg_config_os.h
pg_trace.h
pgstat.h
pgtime.h
port/
port.h
portability/
postgres.h
postgres_ext.h
postgres_fe.h
postmaster/
regex/
rewrite/
rusagestub.h
snowball/
stamp-h
storage/
tcop/
tsearch/
utils/
windowapi.h
代码的阅读方法


用调试器追踪代码


Postgresql那样的庞大系统,用眼睛来追踪源码并不容易。这里推荐用gdb这样的实际调试器来追踪代码的执行流程。可能有些人畏惧调


试器,但是如果只是简单追踪代码的执行流的话,还是很简单的。
但是多少还是要做一些准备的,Postgresql在编译的时候一定要把调试开关打开。通常在编译的时候configure的时候加上--enable-


debug的选项,然后可能的话可以编辑src/Makefile.global这个文件
CFLAGS = -O2 -Wall -Wmissing-prototypes -Wpointer-arith \
-Wdeclaration-after-statement -Wendif-labels -Wformat-security \
-fno-strict-aliasing -fwrapv
上面的行的"-O2"选项删除,然后加上"-g"
CFLAGS = -g -Wall -Wmissing-prototypes -Wpointer-arith \
-Wdeclaration-after-statement -Wendif-labels -Wformat-security \
-fno-strict-aliasing -fwrapv
"-O2"是编译器的优化选项,如果打开了,代码的执行顺序会改变,使得追踪起代码来比较困难,所以要去除。当然这样的话,编译后的


可执行文件会比较大,而且会比较慢,生产环境不太合适。大家需要理解这个操作仅仅是在学习的时候而设置的。
实际使用gdb试试


下面实际使用gdb来看看比较简单点的select文。
select 1;
select文执行后,至executor的其中一个函数ExecSelect停止,然后我们调查一下实际调用了那些函数
首先以Postgresql的超级用户登录。我的环境是使用t-ishii这个用户安装Postgresql的,通常一般使用postgres这个用户,大家在阅读


的时候替换一下即可。
然后,用psql数据库进行连接,连接的状态可以通过ps命令调查。
$ ps x


3714 ? Ss 0:00 postgres: t-ishii test [local] idle
可以看到上面的进程。这个就是后端的进程。这个是后端的进程,还有其他大量用户的Postgresql的连接也显示出来,比较难看清楚,


所以还是准备好测试的环境来进行测试比较好。
启动gdb后,附加到ps里显示的进程号码。
$ gdb postgres 3714
GNU gdb (GDB) 7.2
Copyright (C) 2010 Free Software Foundation,Inc.
License GPLv3+: GNU GPL version 3 or later
<http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY,to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-vine-linux".
For bug reporting instructions,please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /usr/local/pgsql/bin/postgres...done.
Attaching to program: /usr/local/pgsql/bin/postgres,process 3714
Reading symbols from /lib64/libdl.so.2...done.
Loaded symbols for /lib64/libdl.so.2
Reading symbols from /lib64/libm.so.6...done.
Loaded symbols for /lib64/libm.so.6
Reading symbols from /lib64/libc.so.6...done.
Loaded symbols for /lib64/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2...done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
Reading symbols from /lib64/libnss_files.so.2...done.
Loaded symbols for /lib64/libnss_files.so.2
0x00007fad266f82e2 in __libc_recv (fd=<value optimized out>,buf=0xbe9900,
n=8192,flags=<value optimized out>)
at ../sysdeps/unix/sysv/linux/x86_64/recv.c:30
30 ../sysdeps/unix/sysv/linux/x86_64/recv.c:
in ../sysdeps/unix/sysv/linux/x86_64/recv.c
(gdb)
(gdb) 是gdb的命令行。在这个状态下,可以接受gdb的命令,如果输入b命令的话,在ExecResult可以设置断点。
(gdb) b ExecResult
Breakpoint 1,ExecResult (node=0xd13eb0) at nodeResult.c:75
(gdb)
psql启动以后从终端执行select 1,输入以后,后端就会执行该命令。这个时候,postgres进程已经暂停,所以psql会动不了。要继续执


行的话,可在gdb里执行"c"命令。执行了以后,就会在ExecResult 处停止。
Continuing.


Breakpoint 1,ExecResult (node=0xd13eb0) at nodeResult.c:75
75 econtext = node->ps.ps_ExprContext;
(gdb)
到ExecSelect为止的函数调用路径可以用bt的命令显示出来。
(gdb) bt
#0 ExecResult (node=0xd13eb0) at nodeResult.c:75
#1 0x00000000005b92a4 in ExecProcNode (node=0xd13eb0) at execProcnode.c:367
#2 0x00000000005b71bb in ExecutePlan (estate=0xd13da0,planstate=0xd13eb0,
operation=CMD_SELECT,sendTuples=1 '\001',numberTuples=0,
direction=ForwardScanDirection,dest=0xcf9938) at execMain.c:1439
#3 0x00000000005b5835 in standard_ExecutorRun (queryDesc=0xc62820,count=0) at execMain.c:313
#4 0x00000000005b5729 in ExecutorRun (queryDesc=0xc62820,count=0) at execMain.c:261
#5 0x00000000006d2f79 in PortalRunSelect (portal=0xc60810,
forward=1 '\001',count=0,dest=0xcf9938) at pquery.c:943
#6 0x00000000006d2c4e in PortalRun (portal=0xc60810,
count=9223372036854775807,isTopLevel=1 '\001',dest=0xcf9938,
altdest=0xcf9938,completionTag=0x7fffa4b0eeb0 "") at pquery.c:787
#7 0x00000000006cd135 in exec_simple_query
(query_string=0xcf8420 "select 1;") at postgres.c:1018
#8 0x00000000006d1144 in PostgresMain (argc=2,argv=0xc42da0,
username=0xc42c40 "t-ishii") at postgres.c:3926
#9 0x0000000000683ced in BackendRun (port=0xc65600) at postmaster.c:3600
#10 0x00000000006833dc in BackendStartup (port=0xc65600) at postmaster.c:3285
#11 0x0000000000680759 in ServerLoop () at postmaster.c:1454
#12 0x000000000067ff4d in PostmasterMain (argc=3,argv=0xc40e00)
at postmaster.c:1115
#13 0x00000000005f7a39 in main (argc=3,argv=0xc40e00) at main.c:199
(gdb)
说明一下看的方法,发起调用函数在下面,被调用函数在上面。也就是ExecProcNode调用了ExecResult,ExecutePlan调用


ExecProcNode,ExecutorRun调用了ExecProcNode,这样的形式来写。特别是中间的第7行。
#7 0x00000000006cd135 in exec_simple_query
(query_string=0xcf8420 "select 1;") at postgres.c:1018
这样可以清楚看到在处理SELECT文。:)仔细看gdb的输出,可以发现这些细节。
gdb是源码调试器,所以可以看到和源代码的对应关系。例如list命令可以看到现在执行的行附近的代码
(gdb) list
70 TupleTableSlot *resultSlot;
71 PlanState *outerPlan;
72 ExprContext *econtext;
73 ExprDoneCond isDone;
74
75 econtext = node->ps.ps_ExprContext;
76
77 /*
78 * check constant qualifications like (2 > 1),if not already done
79 */
利用up命令可以往上面的函数移动。下面用list命令,可以确认实际调用ExecSelect 的地方。
(gdb) up
#1 0x00000000005b92a4 in ExecProcNode (node=0xd13eb0) at execProcnode.c:367
367 result = ExecResult((ResultState *) node);
(gdb) list
362 {
363 /*
364 * control nodes
365 */
366 case T_ResultState:
367 result = ExecResult((ResultState *) node);
368 break;
369
370 case T_ModifyTableState:
371 result = ExecModifyTable((ModifyTableState *) node);
利用down可以往下面的函数移动。利用up和down的组合,可以调查函数调用关系。
退出gdb的话可以用quit。
(gdb) quit
Inferior 1 [process 3714] will be detached.
Quit anyway? (y or n) y
Detaching from program: /usr/local/pgsql/bin/postgres,process 3714
到了这里gdb就结束了,但是后端进程并不会终止。
使用tag来跳转到相应的函数定义文件


我们已经使用了gdb来调查postgresql的运行,另外用gdb的list来追踪源码的话还是相当辛苦的,一般来说用emacs等编辑器一起调查和


浏览代码,可以在边调试边查看代码
当然,在gdb模式下也可以使用。这个时候,例如如果想看看'exec_simple_query'的定义的话,使用emacs的tags命令可以立刻跳转到函


数定义的地方。要使用tags的话,需要生产tags文件,Postgresql的话,带有生产tags文件的脚本。
$ cd /usr/local/src/postgresql-9.1.1/src
$ tools/make_etags (使用emacs的场合)
$ tools/make_tags (使用vi的场合)
这样就可以拉。 然后在emacs中,在exec_simple_query 处执行'ESC-.'(按了ESC键后输入逗号.),即可打开光标所在文字所在的


exec_simple_query函数的定义文件
总结


要完全理解Postgresql的话,通过调查源代码还是比较有效果的。要理解代码的话,可以按照目的自己追加必要的功能,改变一些功能


的行为,大家可以最大限度的的享受开源带来的好处。这次为了让大家能够理解Postgresql的源代码,说明了Postgresql 9.1的全体结


构,还有说明了代码树。然后还使用了调试器来追踪Postgresql的动作。
========

Postgresql源码简单分析(1)

Postgresql源码简单分析(by linux_prog@loveopensource.com)

Postgresql是一个非常强大的开源数据库,既然使开源,当然,我们可以去修改他的代码做任何事情。
最近,忙着设计一个分布式数据库系统,所以,理所当然,就想到了在postgresql的基础上直接改。因此,
分析其源代码就必不可少了。
简单讲一下分析内容
源码目录:
$ cd postgresql-8.2.4/src/backend/
$ ls
access catalog executor libpq Makefile nodes parser port postmaster rewrite tcop
bootstrap commands lib main nls.mk optimizer po postgres regex storage utils

其中:main/main.c是程序启动主文件
文件没有作什么重要的事情,主要是作成为daemon等等一些我们并不关心的事情。

tcop/postgres.c是backend执行入口文件
请看第3414行:
case ‘Q’: /* simple query */
{
const char *query_string;


/* Set statement_timestamp() */
SetCurrentStatementStartTimestamp();


query_string = pq_getmsgstring(&input_message); //拿到通过libpq传过来的sql语句

pq_getmsgend(&input_message);


exec_simple_query(query_string); //执行这个sql,并把结果通过libpq返回


send_ready_for_query = true;
}
break;

再看看postgres.c的第745行:
static void
exec_simple_query(const char *query_string)
{
CommandDest dest = whereToSendOutput;
MemoryContext oldcontext;
List *parsetree_list;
ListCell *parsetree_item;
bool save_log_statement_stats = log_statement_stats;
bool was_logged = false;
char msec_str[32];


/*
* Report query to varIoUs monitoring facilities.
*/
debug_query_string = query_string;


pgstat_report_activity(query_string);


/*
* We use save_log_statement_stats so ShowUsage doesn’t report incorrect
* results because ResetUsage wasn’t called.
*/
if (save_log_statement_stats)
ResetUsage();


/*
* Start up a transaction command. All queries generated by the
* query_string will be in this same command block,*unless* we find a
* BEGIN/COMMIT/ABORT statement; we have to force a new xact command after
* one of those,else bad things will happen in xact.c. (Note that this
* will normally change current memory context.)
*/
start_xact_command();


/*
* Zap any pre-existing unnamed statement. (While not strictly necessary,
* it seems best to define simple-Query mode as if it used the unnamed
* statement and portal; this ensures we recover any storage used by prior
* unnamed operations.)
*/
unnamed_stmt_pstmt = NULL;
if (unnamed_stmt_context)
{
DropDependentPortals(unnamed_stmt_context);
MemoryContextDelete(unnamed_stmt_context);
}
unnamed_stmt_context = NULL;


/*
* Switch to appropriate context for constructing parsetrees.
*/
oldcontext = MemoryContextSwitchTo(MessageContext);


QueryContext = CurrentMemoryContext;


/*
* Do basic parsing of the query or queries (this should be safe even if
* we are in aborted transaction state!)
*/

// 解析这个sql语句到一个语法树结构中
parsetree_list = pg_parse_query(query_string);


我想做的事情如下:
在postgresql的基础上作一个分布式数据库,但sql parse和backend/frontend的通信都不想自己写,
也就是说要使用postgresql的libpq。
因此做如下实验:
任何sql语句进来后,我会在exec_simple_query里面捷获,如果是一个select语句,
我会返回一行记录:列名—name 列值– lijianghua

继续分析文件: src/access/common/printtup.c


//以下函数使通过libpq发送返回的列的column 描述信息的
void
SendRowDescriptionMessage(TupleDesc typeinfo,List *targetlist,int16 *formats)
{
Form_pg_attribute *attrs = typeinfo->attrs;
int natts = typeinfo->natts;
int proto = PG_PROTOCOL_MAJOR(FrontendProtocol);
int i;
StringInfoData buf;
ListCell *tlist_item = list_head(targetlist);


pq_beginmessage(&buf,‘T’); /* tuple descriptor message type */
pq_sendint(&buf,natts,2); /* # of attrs in tuples */


for (i = 0; i < natts; ++i)
{
Oid atttypid = attrs->atttypid;
int32 atttypmod = attrs->atttypmod;


pq_sendstring(&buf,NameStr(attrs->attname));
/* column ID info appears in protocol 3.0 and up */
if (proto >= 3)
{
/* Do we have a non-resjunk tlist item? */
while (tlist_item &&
((TargetEntry *) lfirst(tlist_item))->resjunk)
tlist_item = lnext(tlist_item);
if (tlist_item)
{
TargetEntry *tle = (TargetEntry *) lfirst(tlist_item);


pq_sendint(&buf,tle->resorigtbl,4);
pq_sendint(&buf,tle->resorigcol,2);
tlist_item = lnext(tlist_item);
}
else
{
/* No info available,so send zeroes */
pq_sendint(&buf,2);
}
}
/* If column is a domain,send the base type and typmod instead */
atttypid = getBaseTypeAndTypmod(atttypid,&atttypmod);
pq_sendint(&buf,(int) atttypid,sizeof(atttypid));
pq_sendint(&buf,attrs->attlen,sizeof(attrs->attlen));
/* typmod appears in protocol 2.0 and up */
if (proto >= 2)
pq_sendint(&buf,atttypmod,sizeof(atttypmod));
/* format info appears in protocol 3.0 and up */
if (proto >= 3)
{
if (formats)
pq_sendint(&buf,formats,2);
else
pq_sendint(&buf,2);
}
}
pq_endmessage(&buf);
}


//下面这个函数是select返回的数据的值,每一行数据都会调用一下这个函数
static void
printtup(TupleTableSlot *slot,DestReceiver *self)
{
TupleDesc typeinfo = slot->tts_tupleDescriptor;
DR_printtup *myState = (DR_printtup *) self;
StringInfoData buf;
int natts = typeinfo->natts;
int i;


/* Set or update my derived attribute info,if needed */
if (myState->attrinfo != typeinfo || myState->nattrs != natts)
printtup_prepare_info(myState,typeinfo,natts);


/* Make sure the tuple is fully deconstructed */
slot_getallattrs(slot);


/*
* Prepare a DataRow message
*/
pq_beginmessage(&buf,‘D’);


pq_sendint(&buf,2);


/*
* send the attributes of this tuple
*/
for (i = 0; i < natts; ++i)
{
PrinttupAttrInfo *thisState = myState->myinfo + i;
Datum origattr = slot->tts_values,
attr;


if (slot->tts_isnull)
{
pq_sendint(&buf,-1,4);
continue;
}


/*
* If we have a toasted datum,forcibly detoast it here to avoid
* memory leakage inside the type’s output routine.
*/
if (thisState->typisvarlena)
attr = PointerGetDatum(PG_DETOAST_DATUM(origattr));
else
attr = origattr;


if (thisState->format == 0)
{
/* Text output */
char *outputstr;


outputstr = OutputFunctionCall(&thisState->finfo,attr);
pq_sendcountedtext(&buf,outputstr,strlen(outputstr),false);
pfree(outputstr);
}
else
{
/* Binary output */
bytea *outputbytes;


outputbytes = SendFunctionCall(&thisState->finfo,attr);
pq_sendint(&buf,VARSIZE(outputbytes) - VARHDRSZ,4);
pq_sendbytes(&buf,VARDATA(outputbytes),
VARSIZE(outputbytes) - VARHDRSZ);
pfree(outputbytes);
}


/* Clean up detoasted copy,if any */
if (attr != origattr)
pfree(DatumGetPointer(attr));
}


pq_endmessage(&buf);
}


根据以上分析,我来修改exec_simple_query:
在833行加入如下内容
//此范例只处理select语句
if(parsetree->type == T_SelectStmt)
{
StringInfoData buf;
pq_beginmessage(&buf,‘T’); /* tuple descriptor message type */
pq_sendint(&buf,1,2); /* number of columns in tuples */
pq_sendstring(&buf,“name”); // column名称
pq_sendint(&buf,4);
pq_sendint(&buf,2);
pq_sendint(&buf,4);
pq_sendint(&buf,2,2);
pq_endmessage(&buf);

pq_beginmessage(&buf,‘D’);
pq_sendint(&buf,2);
pq_sendcountedtext(&buf,“lijianghua”,10,false);
pq_endmessage(&buf);

//此行必须加上,告诉libpq返回结果结束(C代表completed)
pq_puttextmessage(’C',“select return 1 rows”);

return;
}

修改结束,按照正常流程编译Postgresql,并启动。
测试结果:
[mypg@webtrends mypg]$ psql
Welcome to psql 8.2.4,the Postgresql interactive terminal.


Type: \copyright for distribution terms
\h for help with sql commands
\? for help with psql commands
\g or terminate with semicolon to execute query
\q to quit


mypg=# \d
List of relations
name
————
lijianghua
(1 row)


mypg=# select * from test2;
name
————
lijianghua
(1 row)


mypg=# select * from test3;
name
————
lijianghua
(1 row)


mypg=# select * from test5;
name
————
lijianghua
(1 row)


可以看到任何select语句都只返回我们预定义的结果,说明我们当初的想法是可行的(\d其实也是一个select语句)。
========

Postgresql存储引擎源码分析一

Postgresql的存储系统作为Postgresql的最低层,向下通过操作系统系统接口访问物理数据,向上为存取系统提供由缓冲区页面页面


上的接口函数


存储系统的总体架构如下图所示


注释:Lock Manager是锁管理器,IPC是进程间通信,他们实现了存取层对存储层的互


斥访问,操作。


存储系统各子系统功能如下:


Page Manager:对缓冲区页面的结构进行定义并提供页面的相关操作。


Buffer Manager:对共享缓冲区和本地缓冲区进行管理。


Storage Manager:屏蔽不同物理设备接口函数的差异,向Buffer Manager提供统一的接口。


File Manager:一般的操作系统只允许一个进程打开256个文件,而Postgresql服务器在工作时需要打开的文件会很多,因此,其使用


File Manager来封装操作系统文件读写的函数


下面对Page Manager的一段代码进行分析:Page Manager模块的功能上面已经讲到过,这里便不再赘述,这个模块主要由三个文件组成


:源码根目录下的backend\storage\page路径下的bufpage.c,itemptr.c,以及根目录下include\storage路径的头文件bufpage.h组成。


页面Page的结构大致如下:页面由页首部,页面存储记录的ID,存储的记录以及特殊空间所组成。其中,页面首部定义在bufpage.h文件


中。如下所示:


typedef struct PageHeaderData
{
XLogRecPtr pd_lsn; /* XLogRecPtr是定义在/include/access/xlogdefs.h中的一个结构体,定义了存取层所用的日志文件 ,期待


其他模块同学的完善。*/
uint16 pd_tli; /* 善未搞明白。。。*/
uint16 pd_flags; /* 页首部标志*/
LocationIndex pd_lower; /* 页面空闲区域起始偏移量,LocationIndex 是无符号16位整型数据,下同*/
LocationIndex pd_upper; /* 页面空闲区域结束偏移量 */
LocationIndex pd_special; /* 页面特殊区域起始偏移量 */
uint16 pd_pagesize_version;/* 页面大小*/
TransactionId pd_prune_xid; /* 不重要的XID,如果为空则为0 */
ItemIdData pd_linp[1]; /* ItemidData是定义在/include/storage/itemid.h中的结构体,主要定义了元组项的底层特征:元组在页


面上的偏移量,元组项指针的状态,元组的比特位长度,这里是定义了一个元组项的一个指针,指向页面不同的元组项(也就是记录)


*/
} PageHeaderData;


typedef PageHeaderData *PageHeader;


下面来分析/backend/storage/page/bufpage.c中的PageInit函数页面初始化)


void PageInit(Page page,Size pageSize,Size specialSize)


{


PageHeader p = (PageHeader) page;


specialSize = MAXALIGN(specialSize);//MAXALIGN是常量表达式


Assert(pageSize == BLCKSZ);//如果页面大小和磁盘块大小相等的话,函数终止,页面初始化失败
Assert(pageSize > specialSize + SizeOfPageHeaderData);//SizeofPageHeaderData是定义在bufpage.h中的宏,即offsetof


(PageHeaderData,pd_linp),功能是获得页面首部中pd_linp数组的偏移量,如果页面大小大于特殊空间大小与偏移量之和的话,函数


终止。


MemSet(p,pageSize);//讲页首部初始化,清零。


p->pd_lower = SizeOfPageHeaderData;//初始化页面空闲区域起始偏移量
p->pd_upper = pageSize - specialSize;//初始化页面空闲区域结束偏移量
p->pd_special = pageSize - specialSize;//初始化特殊区域起始偏移量
PageSetPageSizeAndVersion(page,pageSize,PG_PAGE_LAYOUT_VERSION);//设置页面大小以及页面布局的版本号,这是定义在


bufpage.h下的一个宏原型为:#define PageSetPageSizeAndVersion(page,size,version) \
( \
AssertMacro(((size) & 0xFF00) == (size)),\
AssertMacro(((version) & 0x00FF) == (version)),\
((PageHeader) (page))->pd_pagesize_version = (size) | (version) \
)里面基本上是一些位操作:将页面大小的后16位置0,版本号的前16为置0.


}


========

Postgresql源码分析之page

前面几篇博客分析了shared buffer,从shared buffer到磁盘文件的映射,到shared buffer的分配和替换,再到如何测量shared buffer的性能情况,配置是否合理,基本把shared buffer大概介绍了下,这篇博客主要分析page。 page的源码落在/src/backend/storage/page,page对于Postgresql是个什么概念?page,block,file,这些概念怎么理解? 文件数据库的持久化存储,当然我们已经知道数据库的relation以文件的形式存在在磁盘上,无论是xxx文件还是xxx_fsm,还是 xxx_vm,这是文件的概念。当relation的xxx的文件特别大,超过1G的时候,同一个relation还会分文件存储,出现xxx.1,xxx.2这种文 件。Whatever,文件在,Postgresql数据库的信息就在。 所谓block,指的是每次加载进内存的基本单位,如果Postgresql需要某个relation的信息,不会是直接relation对应的磁盘文件全 部读入内存,而是分block载入内存。Postgresql有一定的规则知道自己需要的信息或者记录或者说tuple 落在磁盘文件的那个8K block 上,然后将8K block加载入local buffer,或者shared buffer,总之加载入内存。简单的说,block是磁盘上文件和内存之间加载/驱逐 的基本单位。 page是个什么概念呢?page大小也是8K,就是上面提到的block,只不过,page仔细的端详了8KB的内容,分析了信息是如何组织,如 何存放到8KB的block空间之内。注意每条记录内部的结构不是page关心的事情,他的视角没有这个细,我们关心的是这条记录作为一个 整体如何存放到8K的page中去;当然8K的page可能存放多条记录,如何摆放到8K的page中去;当前page剩余空间还有多少;我有一条需要空 间为size的记录,page是否有足够的空间容纳之;记录可能会插入,也可能会删除,page里面会不会因为删除动作,页面内部有很多的洞 ,或者页面碎片化,如何清理碎片,这些都是page要解决的问题。 简而言之,page,就是管理8K大小的一亩三分地,他要把多条记录(Tuple)有条不紊地组织在这8K的空间之内。 一条记录会插入到8KB的page之中,信息如何组织?自然大多数记录占用的空间不会超过8KB,以我们前边提到的friends为例: 这个friends的设计不太好,不过我们的重点不在于此,我们关心的是这长度为8192(1个Block或者说1个page)的文件,到底存放 的是啥内容? 我们看到文件虽然有8K,但是实际上只有最前面的2行32字节,和最后面的64字节中包含信息,因为这个文件对应的就是我们的 friends这张表,而这张表里面有Lee,Bean ,158XXXXX,Nan Jing等信息,当然了这是一条记录,或者一个Tuple,Tuple内部的组成或 者layout我们不关心,但是这个16385文件作为一定记录了这些信息。我们用vbindiff查看之: 我们看到了,我们的信息Bean,Nan Jing之类的,不管是如何组织的,的确存储在表friend对应文件16385之中。这条记录如何放入 8K的空间之内,头部的一些字符有是干啥的,记录的信息为何放到了现在的这个位置,这就是page要管的事情,我们下面详查之。 上图就是page的结构图,8K的空间包括一个头部Page Header,若干个Item,每个Item指向一条记录(Tuple),有些Page在初始化 的时候,就page的末尾,预留出空间作为Special用,作什么用,我暂时不知,不过没关系,不影响我们理解Page。当然了,有些Page不 需要Special空间,就没有预留。 好我们可以分析源码了。 INIT-page的初始化 首当其冲的是PageInit函数。我们申请了一个新的干净的8K的page,把记录插入page之前,需要将page初始化,基本就是初始化一 下Page Header。: void PageInit(Page page,Size specialSize) { PageHeader p = (PageHeader) page; specialSize = MAXALIGN(specialSize); Assert(pageSize == BLCKSZ); Assert(pageSize > specialSize + SizeOfPageHeaderData); /* Make sure all fields of page are zero,as well as unused space */ MemSet(p,pageSize); /* p->pd_flags = 0; done by above MemSet */ p->pd_lower = SizeOfPageHeaderData; p->pd_upper = pageSize - specialSize; p->pd_special = pageSize - specialSize; PageSetPageSizeAndVersion(page,PG_PAGE_LAYOUT_VERSION); /* p->pd_prune_xid = InvalidTransactionId; done by above MemSet */ } 对于pageSize,默认情况下就是8K即BLCKSZ,而specialSize,某些情况下为0,某些情况下不为0,这都没关系。 Init做的事情是 1 给special预留空间 specialSize = MAXALIGN(specialSize); //4 字节对齐 p->pd_special = pageSize - specialSize; page header的成员变量pd_special相当于画了一条线,从pd_special这个位置到page的结尾,都是special的地盘,普通插入Tuple ,都不许进入这个私有地盘。而且这个pd_special一旦初始化之后,这个值就不会动了。 2 设置pd_lower和pg_upper 当初始化的时候,pd_lower设置为SizeOfPageHeaderData,pd_upper设置为和pd_special一样。但是注意,这个lower和upper不是固 定的,随着Tuple的不断插入,lower变大,而upper不断变小。当我们每插入一条Tuple,需要在当前的lower位置再分配一个Item,记录 Tuple的长度,Tuple的起始位置offset,还有flag信息。这个Page Header中的pd_lower就是记录分配下一个Item的起始位置。所以如果 不断插入,lower不断增加,每增加一条Tuple,就要分配一个Item(4个字节)。同样道理,Tuple的存放位置,根据upper提供的信息, 可以找到将Tuple分配到何处比较合。分配之后,pd_upper就会减少,减少Tuple的长度(对齐也考虑进去)。 3 设置 page的size 和version #define PageSetPageSizeAndVersion(page,version) ( AssertMacro(((size) & 0xFF00) == (size)),AssertMacro(((version) & 0x00FF) == (version)),((PageHeader) (page))->pd_pagesize_version = (size) | (version) ) 这个不多说,基本就是将版本号和page的长度记录在16bit的结构里面。 下面我们比较刚初始化和插入一条记录之后的情形: 一个记录对应两个部分,就头部附近Item空间和真正记录信息的Tuple。Item记录的是Tuple在Page的offset,size等信息。 AddItem-page增加一个记录 Page是用来存放Tuple的,增加一个Tuple删除一个Tuple都是Page份内的事情,我们首先看下Page如何增加一个Tuple: function PageAddItem是完成这件事情。因为这个接口是很通用的接口,要满足上层的各种需求,所以稍显复杂,不过整体还好。 OffsetNumber PageAddItem(Page page,Item item,Size size,OffsetNumber offsetNumber,bool overwrite,bool is_heap) item是我的当前记录的指针,size记录记录的长度,(item,item+size)这部分地址是Tuple的信息。 Page表示从这个page中查找 空间保存当前的Tuple。这我们很好理解,因为这是基本的要求:在当前页随便找个空间保存我的item。咱的要求比较简单,可是有些客 户要求可就不简单了,比如客户要求,就要将我的记录拜放到page的第三个item,这就是比较坑爹的客户了。就像去饭馆吃饭,我到了 饭馆,喊了一嗓子,小二,给哥随便找个8人桌,小二很happy,因为我的要求低。也有客官直接喊了一嗓子,小二,我要去三楼最好的 那个雅间,如果有客人,让他给我腾地方,我们有8个人。得,小二就傻了眼,但是还得办不是。PageAddItem也是一样,offsetNumber 这个如参表示,大爷我就要将记录存放在这个位置。overwrite则这个参数就更拽了,如果有记录放在我要的位置,让原来那条记录给大 爷滚蛋,。如果overwrite =0 表示,大爷要的位置如果有人,原来位置的记录换个地方,给大爷我腾地方。OK,这几个参数是干啥的, 我基本交代清楚了 因为Page Header的长度是固定,而紧跟其后的Item的长度也是固定的,而每增加一个Item,pd_lower就增加一个Item的长度,这样, 根据pd_lower就可以算出当前的页面已经有几个Tuple了。 #define PageGetMaxOffsetNumber(page) (((PageHeader) (page))->pd_lower <= SizeOfPageHeaderData ? 0 : ((((PageHeader) (page))->pd_lower - SizeOfPageHeaderData) / sizeof(ItemIdData))) limit = OffsetNumberNext(PageGetMaxOffsetNumber(page)); 这个limit记录的是当前记录数+1 ,用这个来判段新来的AddItem请求有没有指定既有的位置 if (OffsetNumberIsValid(offsetNumber)) //大爷型请求,值定了记录的存储位置 { if (overwrite) //原有的记录删除,属于要求改写 { if (offsetNumber < limit) { itemId = PageGetItemId(phdr,offsetNumber); if (ItemIdIsUsed(itemId) || ItemIdHasStorage(itemId)) { elog(WARNING,"will not overwrite a used ItemId"); return InvalidOffsetNumber; } } } else //新增加的客户要求这个位置,需要将原来位于这个位置的记录迁移到其他位置。 { if (offsetNumber < limit) needshuffle = true; /* need to move existing linp's */ } } else //普通客户 { } 上面分析了文艺青年式的AddItem,下面我们分析下普通青年的AddItem,普通青年要求低,随便找个地儿存放当年记录: if (OffsetNumberIsValid(offsetNumber)) { ... } else { /* offsetNumber was not passed in,so find a free slot */ /* if no free slot,we'll put it at limit (1st open slot) */ if (PageHasFreeLinePointers(phdr)) { /* * Look for "recyclable" (unused) ItemId. We check for no storage * as well,just to be paranoid --- unused items should never have * storage. */ for (offsetNumber = 1; offsetNumber < limit; offsetNumber++) { itemId = PageGetItemId(phdr,offsetNumber); if (!ItemIdIsUsed(itemId) && !ItemIdHasStorage(itemId)) break; } if (offsetNumber >= limit) { /* the hint is wrong,so reset it */ PageClearHasFreeLinePointers(phdr); } } else { /* don't bother searching if hint says there's no free slot */ offsetNumber = limit; } } 比较容易想到的是offsetNumber = limit = 当前记录数 + 1,这个太顺理成章了,那个PageHasFreeLinePointers是搞什么飞机?我 们看下: #define PageHasFreeLinePointers(page) (((PageHeader) (page))->pd_flags & PD_HAS_FREE_LINES) 这个标志是啥意思?看名字的意思是 表征是否有free line。我们会把一些Item状态置为LP_UNUSED,这时候,Item和它原来的 Tuple就没有映射关系。这样原来对应Tuple,就成了垃圾。后面会有会PageRepairFragmentation清理这些空间,但是仍然不会删除这个 LP_UNUSED状态的Item,只是打上一个标志,表示存在无主的Item,可以被复用。 if (offsetNumber == limit || needshuffle) lower = phdr->pd_lower + sizeof(ItemIdData); //新增一个Item else lower = phdr->pd_lower; alignedSize = MAXALIGN(size); upper = (int) phdr->pd_upper - (int) alignedSize; if (lower > upper) return InvalidOffsetNumber; /* * OK to insert the item. First,shuffle the existing pointers if needed. */ itemId = PageGetItemId(phdr,offsetNumber); if (needshuffle) memmove(itemId + 1,itemId,(limit - offsetNumber) * sizeof(ItemIdData)); /* set the item pointer */ ItemIdSetNormal(itemId,upper,size); /* copy the item's data onto the page */ memcpy((char *) page + upper,item,size); /* adjust page header */ phdr->pd_lower = (LocationIndex) lower; phdr->pd_upper = (LocationIndex) upper; return offsetNumber; 因为新增个Tuple,需要alignedSize存储这记录的Tuple部分,所以pd_upper - alignedSize作为新的pd_upper. ItemIdSetNormal把Tuple的size,offset信息记录在Item中: #define ItemIdSetNormal(itemId,off,len) ( (itemId)->lp_flags = LP_NORMAL,(itemId)->lp_off = (off),//记录offset, page + off = Tuple的起始位置 (itemId)->lp_len = (len) //记录Tuple的size 。 (page + off ,page + off + len)记录的是Tuple的信息 ) PageIndexTupleDelete-page删除一条记录 我们下面讲述删除一条记录: void PageIndexTupleDelete(Page page,OffsetNumber offnum) offnum指示第几个记录,offnum是从1开始计数的,查找对应item 是offnum-1. 我们找到Item,就可以找到Tuple对应的offset和size: tup = PageGetItemId(page,offnum); Assert(ItemIdHasStorage(tup)); size = ItemIdGetLength(tup); offset = ItemIdGetOffset(tup); 删除第二个记录之后,我们得到的Page布局如下: 我们可以看到,至少发生两次memmove 1 删除记录的Item后面的item都要往迁移,防止出现一个空洞 nbytes = phdr->pd_lower - ((char *) &phdr->pd_linp[offidx + 1] - (char *) phdr); if (nbytes > 0) memmove((char *) &(phdr->pd_linp[offidx]),(char *) &(phdr->pd_linp[offidx + 1]),nbytes); 2 删除记录的Tuple后面的Tuple,也要移动,否则,会出现Tuple-2对应的空洞。 addr = (char *) page + phdr->pd_upper; if (offset > phdr->pd_upper) memmove(addr + size,addr,(int) (offset - phdr->pd_upper)); 除了移动内存,item对应的指针也要发生相应的改变:比如洋红色的两个item需要修改offset if (!PageIsEmpty(page)) { int i; nline--; /* there's one less than when we started */ for (i = 1; i <= nline; i++) { ItemId ii = PageGetItemId(phdr,i); Assert(ItemIdHasStorage(ii)); if (ItemIdGetOffset(ii) <= offset) //在前面Tuple2 前面的Tuple,发生了移位,所以对应Item的lp_off要修改。 ii->lp_off += size; } } Page还剩多少剩余空间这是很重要的,这决定我们能否插入一条记录到当前Page。 原理就非常简单了,pd_upper - pd_lower,就 是剩余空间,但是,还需要存放Item,所以还需要减Item占据的空间,剩下的才能存放Tuple的空间: Size PageGetFreeSpace(Page page) { int space; /* * Use signed arithmetic here so that we behave sensibly if pd_lower > * pd_upper. */ space = (int) ((PageHeader) page)->pd_upper - (int) ((PageHeader) page)->pd_lower; if (space < (int) sizeof(ItemIdData)) return 0; space -= sizeof(ItemIdData); return (Size) space; } 文章写的已经很长了,PageIndexMultiDelete 和 PageRepairFragmentation核心逻辑是类似的,我就不写这两个。原来也不难,把 这些碎片化的Tuple排个序,重新连接成一个连续的空间。 ========

猜你在找的Postgre SQL相关文章