PostgreSQL:Numeric类型介绍——PostgreSQL源码分析课程作业
摘要
通过PostgreSQL课程⼀学期的学习,我掌握了通过调试PostgreSQL来辅助阅读源码的技能。我选取了PostgreSQL中的numeric 数据类型进⾏深⼊研究,接下来主要会介绍数据结构的定义、⾼精度的实现⽅法、以及numeric⼀些重要函数,主要是numeric_in、round_var等函数。通过本⽂的分析,读者不但可以了解到numeric的基本实现⽅式和特性,更可以从这些看似简单的定义和实现中感受到设计的精巧,并从中得到C语⾔系统开发的⼀些技能和经验。从numeric这⼀数据类型中,我们也可以了解到PostgreSQL中数据类型实现的⼀些共性。
关键词:numeric,数据结构,位运算,精度
⽂章⽬录
⼀、引⾔
在C语⾔中,可以表⽰数值的数据类型有char、short、int、long、float、double等,⽽这些表⽰的精度有限,即使是double也最多只有15位⼗进制数字精度。在PostgreSQL中,numeric是精度最⾼的⼀种数值类型,其精度可以达到⼩数点前 131072 位,⼩数点后16383 位。所以,numeric⼏乎可以看作是⼀个
任意精度的数字,在科学计算中起着重要作⽤。
⼀个numeric类型的标度(scale)是⼩数部分的位数,精度(precision)是全部数据位的数⽬,也就是⼩数点两边的位数总和。要声明⼀个字段的类型为numeric,可以⽤以下三种语法:NUMERIC(precision, scale)意为同时指定标度和精度,精度必须为正数,标度可以为零或者正数。另外,NUMERIC(precision)选择了标度为0,不带任何精度与标度的声明。NUMERIC则创建⼀个可以存储⼀个直到实现精度上限的任意精度和标度的数值。如果⼀个要存储的数值的标度⽐字段声明的标度⾼,那么系统将尝试圆整(四舍五⼊)该数值到指定的⼩数位。然后,如果⼩数点左边的数据位数超过了声明的精度减去声明的标度,那么将抛出⼀个错误。
除了普通的数字值之外,numeric类型允许⽤特殊值NaN表⽰"不是⼀个数字"。任何在NaN上⾯的操作都⽣成另外⼀个NaN。
⼆、数据结构韩湘子
Numeric的数据结构在磁盘上和在内存中是不同的。在磁盘上存储效率较⾼,⽽在内存中读取效率较⾼。每次从磁盘加载到内存需要先进⾏结构的转换,存储时也要进⾏转换。因此,我们将分别对两种数据结构进⾏说明。
1.内存中的实现
在内存中的数据结构如图1所⽰:学习资料网
垂丝海棠花
图 1 Numeric在内存中的数据结构——NumericVar
麻将咋玩⾸先要知道⽐较重要的⼀个宏定义NBASE,定义值为10000,表⽰单个digit的范围为0-9999。由于⽤两个字节(int16)可以覆盖这个范围,所以digits的类型NumericDigit使⽤了int16。
然后来看数据结构中其他部分,ndigits表⽰数字占⽤的digit个数,ndigits的值为⼀个数字去掉前导0和末尾0剩下的有效digit位数。weight为数字最⾼位表⽰的权重,简单理解为⼩数点的偏移,以NBASE为基本单位,这个值可以为正,可以为负,也可以为0。例如weight为1,表⽰最⾼位的digit要乘以100
00,如果weight为-1,表⽰最⾼位的digit要乘以10-4。sign表⽰正负符号,为0则表⽰数字为正,为1则表⽰数字为负。dscale表⽰⼩数位数,也即引⾔中提到的标度,通过ndigts和dscale可以确定数字中有效位数。buf该数组不直接访问,⼀般先开⼀个到两个元素的空间作为缓冲,下⽂将具体分析其作⽤。digits作为⼀个柔型数组,在结构体的末尾,是存储digit的数组空间,其空间是动态开辟的,使⽤起来类似于⼀个指针。开学典礼讲话
2.磁盘上的实现
磁盘上的实现分为两种,⼀种是short型,⼀种是long类型,short类型与long类型的结构相似,但占⽤空间长度要短。在精度要求不是特别⾼的情况下,⽤short类型可以节约存储空间。
图 2 磁盘上数据结构
FLEXIBLE_ARRAY_MEMBER是宏定义,值为空,所以此处的n_data也为柔性数组。两种不同的数据结构头部每个位的表⽰含义如下。
图 3 两种数据结构头部含义
NumericShort中的n_header存储符号位sign、⼩数位数display scale、权重weight,以及最⾼位的两位有特殊含义,含义如表1所⽰。NumericLong和NumericShort的区别在于,多花了两个字节的空间存储。联系两个结构体的⽅式是有⼀个联合体存在,联合体可以根据n_header的最⾼两位来判断numeric的类型,⽤不同的⽅式去读取后⾯实际存储的数据。
表 1 n_header最⾼两位的含义
最⾼两位表⽰含义
00类型为long,且符号为正
01类型为long,且符号为负
10类型为short,符号看第3位
11类型为NaN
龙虎宗除了上⽂提到的数据结构,还有⼀个数据结构,包含了⼀个头部和联合体,如图4所⽰。vl_len_是头部,值为(4+2+ndigits2)<<2,4是
vl_len_⾃⾝的字节数,2位n_header的字节数,ndigits2为n_data的字节数,左偏两位是由于varlena的特殊结构,需要空出两位去存储其他信息。Numeric被定义为NumericData*,即⼀个指针指向存储的位置。
图 4 NumericData的实现⽅式
3.数据举例
我们来看⼀个数字,通过其在两种数据结构下的实际存储来直观感受numeric的实现⽅式。对于数字12345.06789,其有效数字有10位,⼩数有5位,在内存中⽤NumericVar实现的⽅式为:
ndigits: 4
weight: 1
sign: 0
dscale: 5
digits: 0001 2345 0678 9000
由于整数部分有5位,所以最⾼位的digit为1,权重weight为1,表⽰1的权重为104,第⼆个digit存储数字2345,这两位⼀起表⽰整数部分。⼩数部分有5位,所以dscale为5,⽤两位digit表⽰,分别为0678和9000。⼀共使⽤了4位digits,所以ndigits为4,sign为0表⽰这个数字为正数。
这个数字在磁盘上实现的格式应该是:
n_header: 33409(1000 0010 1000 0001)2
n_data: 0001 2345 0678 9000
n_header最⾼⼆位的10表⽰这是⼀个NumericShort类型,第三位sign为0,接下来为000101,表⽰dscale为5,0000001表⽰weight为1,之所以没有存储ndights是因为这个数字可以根据结构体的⼤⼩(因为含有柔型数组)减去头部⼤⼩⽽计算得到,在实际的代码中也是这样做的。n_data即为NumericVar中的digits直接复制⽽来,完全相同。在这⾥,vl_len_的值为56,也即(4+2+4*2)<<2。
4.精度分析
根据numeric的数据结构,我们可以由此分析⼀下⽂档中所写的⾼精度实现原理。⼩数点前 131072 位、⼩数点后 16383 位是被dscale 和weight限制的,超出精度范围会报错。
包罗万象
对于long,dscale⽤14位表⽰,所以⼩数点后的位数位16383位(11 1111 1111 1111),也即14位2进制的最⼤值。weight⽤⼀个short表⽰,最⼤值为32767,因此最多可以有(32767+1)*4=131072位。
这种表⽰⽅法是⼀种程序员和机器理解、效率和精度之间的平衡。⽤⼗进制⽅式表⽰,效率并不⾼,
本来⼀个signed short类型的变量可以表⽰0~32767,但只⽤来表⽰0~9999。不过相对于⼆进制表⽰程序员和代码阅读者便于理解,使⽤⽅便。⽤⼗进制另⼀个好处是可以精确表⽰,不会出现精度损失的问题。
但是,这个表⽰效率⾼于之前⽤字符串表⽰⼤整数或者⼤浮点数的⽅法,因为之前的⽅法8位只能表⽰0~9,也即32位才能表⽰0~9999。注意这⾥weight可以为负数,如果把weight当作⽆符号整数,表⽰范围会更⼤。但这样的话weight没有负数的表⽰范围,如果数字很⼩需要很多0占⽤digits的空间,可能效率会很低。
三、numeric_in函数诫外甥书
1.总体介绍
这个函数的主要功能是将⼀个字符串类型的⼩数转换为numeric类型。在每⼀次使⽤numeric类型时,数据库要把SQL代码中涉及的⼩数字符串通过这个函数转换为可以计算和使⽤的numeric类型。传⼊的字符串可能如“1.345000”、“-0.00023”、“1E-6”等,相应地可以均转换为numeric类型并返回。下⾯我们要⼤致介绍⼀下numeric_in中转换的⼤致过程。
⾸先通过PG_GETARG_CSTRING获取第⼀个参数,读取⽅式为cstring。PG_GETARG_CSTRING是
⼀个特殊的函数,可以从传⼊的参数PG_FUNCTION_ARGS以⼀定的格式获得具体的参数值。同理,下⾯PG_GETARG_INT32就是⽤int32的格式获取第三个参数。⾸先要去除字符串⼀开始的空格,如图5所⽰。
图 5 numeric_in代码1
接下来,如图6所⽰,该函数要判断这个字符串是否表⽰NaN。如果前三个字符为“NaN”,就可以构造⼀个表⽰NaN的numeric对象了。596⾏之后是判断NaN之后是否有其他⾮空字符,如果有,则说明字符串格式有误,⽆法转换成numeric。
图 6 numeric_in代码2
如果前三个字符不是“NaN”,则当作⼀般的⼩数去转换。转换过程主要经过以下三步:1. 开辟NumericVar的内存空间并把字符串转为NumericVar类型;2. 检查精度以及四舍五⼊;3. 把NumericVar类型转换为Numeric类型并返回。这个过程如图7所⽰。
图 7 numeric_in代码3
接下来我们具体去看⼀下这个过程中涉及到的⼏个函数。涉及到的主要函数有t_var_from_str、apply_typmod、make_result三个函数。
2.t_var_from_str函数
t_var_from_str函数的功能从函数名可以看出,把⼀个字符串转换为NumericVar类型。返回值为⼀个指针,指向字符串中满⾜⼩数格式的末尾的位置再加1,这个返回值主要⽤于检查末尾的空间和垃圾。在函数的⼀开始,⾸先检查数字的符号以及是否直接以⼩数点开头(如果整数部分为0时,可以忽略0以⼩数点开头),如图8代码所⽰。
图 8 t_var_from_str代码1——判断符号