
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 re@R_403_3657@ 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排个序,重新连接成一个连续的空间。 ======== 总结
以上是内存溢出为你收集整理的PostgreSQL源码分析全部内容,希望文章能够帮你解决PostgreSQL源码分析所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)