笔者曾做过数据库 Data Type 相关的设计和从 0 到 1 的源码实现,对 Numeric(与 Decimal 等价,都是标准 SQL 的一部分), Datetime, Timestamp, varchar … 等数据类型的设计、源码实现及在内存中计算原理有比较深的理解。

本篇基于 PostgreSQL 源码,解析 PostgreSQL 中 Numeric 类型的内存计算结构和磁盘存储结构。

c 源码 :https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/numeric.c

头文件:https://github.com/postgres/postgres/blob/master/src/include/utils/numeric.h

精度的要求

在编程的过程中,大家可能对内置的 4 字节 float 和 8 字节 doulbe 类型比较熟悉,进行加减乘除运算。虽然浮点数是通过科学计数法来存储,但在二进制和十进制互相转换机制中,对一部分二进制数,其精度是有缺失的。

对于类似金融场景,动辄存储巨大的数值,以及对数据精度的高要求,哪怕再小的精度损失都是不可接受的。市面上各式各样的数据库基本都包含 Numeric 类型,通过字符串来精确存储每一位数,做到浮点数都做不到的精确计算。

Numeric 语法简介

NUMERIC(precision, scale)

  • precision:numeric 中全部数字个数的总和

  • scale:小数点后面的数字个数

例如:12.345,那么 precision 是 5、scale 是 3。

注意事项:

  1. 所有的整数都可以看成 scale 为 0 的 numeric;

  2. precision 必须为正数,scale 可以为 0 或者正数;

  3. numeric(precision) 语法,默认的 scale 是 0;

  4. 语法中不带任何参数,则任意 precision 和 scale 的值都可以被存储,只要不超过 precision 的最大值;

  5. 只要 numeric 中声明了 scale,则输入的值都要强制的去匹配这个 scale(即进行 round 操作,round 为四舍五入);

  6. 如果输入的 scale 数值溢出,则报错。

不指定精度的情况时各数值类型的取值范围【常见】:

Numeric 特殊值

除了正常的数值之外,numeric 还支持特殊的值:NaN( meaning “not-a-number”)。当要将其当做常量用于 SQL 中时,需要打上引号,例如:

 UPDATE table SET x = 'NaN'

SQL 中 Numeric 数据流向

我们知道,一条 SQL 在数据库中的执行流程大致为:

 CREATE TABLE test (name VARCHAR(100) NOT NULL,price NUMERIC(5,2));INSERT INTO test (name, price)VALUES ('Phone',500.215), ('Tablet',500.214);

以上述示例两条 SQL 为例,先建一张 test 表,并插入数据。这里我们关注写入的 Numeric 数字在内存中是如何表示,定义的 NUMERIC(5,2) 对应的数据结构在内存中如何表示。写入的数据在落入磁盘之后,其存储结构又是什么样的。

这里,数据在内存中的存储结构和落盘时的存储结构是不一样的,最终落盘时需要去掉内存中所占用的无效字节的。比如,varchar(100),假如在内存中分配 100 个字节,而实际只写入 “abc” 三个字节,那么它所分配的内存是 100 个字节,而落盘时没有用到的 97 个字节是要去掉的,最后 3 个字节写入磁盘时,还要做数据压缩。大家可以设想一下,如果内存中的存储结构不做任何处理直接写入到磁盘,如果数据量非常大,那会多浪费磁盘空间!

Numeric 磁盘存储结构解析

结构体 NumericData 是最终落到磁盘上的结构,如下,可以看到 NumericData 包含了 NumericLong 和 NumericShort 的 union 字段:

 struct NumericLong{uint16          n_sign_dscale;  /* Sign + display scale */int16           n_weight;               /* Weight of 1st digit  */NumericDigit n_data[FLEXIBLE_ARRAY_MEMBER]; /* Digits */};struct NumericShort{uint16          n_header;               /* Sign + display scale + weight */NumericDigit n_data[FLEXIBLE_ARRAY_MEMBER]; /* Digits */};union NumericChoice{uint16          n_header;               /* Header word */struct NumericLong n_long;      /* Long form (4-byte header) */struct NumericShort n_short;    /* Short form (2-byte header) */};struct NumericData{int32           vl_len_;                /* varlena header (do not touch directly!) */union NumericChoice choice; /* choice of format */};

结构体 NumericLong

 struct NumericLong{uint16          n_sign_dscale;  /* Sign + display scale */int16           n_weight;               /* Weight of 1st digit  */NumericDigit n_data[FLEXIBLE_ARRAY_MEMBER]; /* Digits */};

**uint16 n_sign_dscale **:第一个字节中高两位 bit 用于保存正负号。
若为 0x0000:则符号位正

若为 0x4000:则符号位负

若为 0xC000:则为 NaN

剩余的 14 个 bit 用来保存 display scale(终端界面可显示的范围)

int16 n_weight :保存权值。这里要解释下权值在这里的含义。在这里 numeric 是用一组 int16 数组表示的,每一个元素用 int16 表示 4 位数字,也就是最大保存 9999。那么基数 base 值就是 10000。这里的权值的 base 值就是 10000(10 进制的权值 base 值是 10,二进制是 2)。

NumericDigit n_data[FLEXIBLE_ARRAY_MEMBER] :动态数组(也有叫柔性数组的,在这里统一称动态数组吧),是 C99 之后添加的一个特性。这个特性是在这个结构体中,动态数组并不占用任何空间,其长度由 NumericData 中的 vl_len_ 决定。

这里看到有 long 和 short 两个结构体,对于早期的 PostgreSQL 版本,使用的是 long 的存储方式,后面进行了优化,改进成 short 的存储方式,改进之后的版本为了保持向前兼容,能依然读取之前版本存储的数据,保留了 long 类型的存储方式。

结构体 NumericShort

 struct NumericShort{uint16          n_header;               /* Sign + display scale + weight */NumericDigit n_data[FLEXIBLE_ARRAY_MEMBER]; /* Digits */};

uint16 n_header :保存符号、dynamic scale和权值的信息。

若为 0xC000 则意味着该 Numeric 为 NaN

剩余的 14 个 bit 中,1 个用来保存符号,6 个保存 dynamic scale,7 个用来保存权值 weight。

**NumericDigit n_data[FLEXIBLE_ARRAY_MEMBER] **:参考上文柔性数组描述。

联合体 NumericChoice

 union NumericChoice{uint16          n_header;               /* Header word */struct NumericLong n_long;      /* Long form (4-byte header) */struct NumericShort n_short;    /* Short form (2-byte header) */};

uint16 n_header :这个占两个字节的变量包含有很多信息。如果 n_header 第一个字节最高两个 bit 位的值为:

0x8000:则采用 NumericShort 存储格式

0xC000:则为 NaN

除此之外,则采用 NumericLong 存储格式。

结构体 NumericData

 struct NumericData{int32           vl_len_;                /* varlena header (do not touch directly!) */union NumericChoice choice; /* choice of format */};

int32 vl_len_ :用来保存动态数组的长度,这个数组是 NumericLong 或者 NumericShort 结构体中定义的动态数组。

Numeric 内存计算结构解析

 typedef struct NumericVar{int                     ndigits;                /* # of digits in digits[] - can be 0! */int                     weight;                 /* weight of first digit */int                     sign;                   /* NUMERIC_POS, NUMERIC_NEG, or NUMERIC_NAN */int                     dscale;                 /* display scale */NumericDigit *buf;                      /* start of palloc'd space for digits[] */NumericDigit *digits;           /* base-NBASE digits */} NumericVar;

NumericVar 是用来做算术运算的格式,在 digit-array 部分同存储格式一样,但是在 header 部分更复杂。下面分别作分析:

  • buf:指向实际为 NumericVar 申请的内存 start 位置

  • digits:指向实际使用时的第一个数字的位置(这里的元素是 int16,非 0)

    • buf 跟 digts 之间一般预留一到两个元素(int16)作为可能的 carry(进位)用,当然,考虑到实际 numericleading 部分可能有好多 0,意味着 bufdigits 之间可以相隔好多个元素
  • dscale:display scale 的缩写,表示 numeric 小数点后有多少个十进制数

    • 就目前的版本,总是 >= 0,dscale 的值可能比实际存储的小数位数要大,这意味多出来的部分是 0(trailing zeros),同时也意味着在写入磁盘时,是不会把无意义的 0 写进去的(节约磁盘空间)
  • rscale:这里提一个在函数计算时用到的变量,result scale 的缩写,保存目标计算结果的精度,总是 >= 0

    • rscale 并不保存在 NumericVar 中,实际值是根据输入的 dscales 确定的
  • sign:标记正负号或者 NAN

  • weight:权值,权值是进制的(位数 -1)幂

    • 比如 9999 9999 9999.9999,占用三个 int16,权值是 2(原理跟 10 进制权值一样的算法,只是 int16 的基数值是 10000)
  • ndigits:在 digits[ ] 数组中的 int16 的个数

关注“青云技术社区”公众号,后台回复关键字“云原生实战”,即可加入课程交流群。

作者

高日耀 资深数据库内核研发、MySQL 系列产品内核开发

本文由博客一文多发平台 OpenWrite 发布!

为金融场景而生的数据类型:Numeric相关推荐

  1. 源码 | 为金融场景而生的数据类型:Numeric

    高日耀 资深数据库内核研发 毕业于华中科技大学,喜欢研究主流数据库架构和源码,并长期从事分布式数据库内核研发.曾参与分布式 MPP 数据库 CirroData 内核开发(东方国信),现主要负责 MyS ...

  2. 千亿级金融场景下,基于Pulsar的云原生消息队列有怎样的表现?

    导语 | 云原生场景,多语言.多种协议兼容,任意多的消息 Topic.任意多的消费者,性能的按需快速扩展成为消息队列基本的要求.本文是对腾讯TEG技术委员会专家工程师刘德志老师在云+社区沙龙 onli ...

  3. 【数据竞赛】消费金融场景下的用户购买预测冠军方案分享

    大赛介绍 2000多年前,阿基米德说:"给我一个支点,我可以撬动整个地球".伴随近年来新技术的快速涌现和迅猛发展,大数据或将成为传统金融行业向金融科技转型的"阿基米德支点 ...

  4. smartupload 路径不存在_洞悉复杂金融场景,覆盖完备测试路径

    随着应用系统数量不断增加.系统规模不断扩大以及微服务架构持续推进,系统间模块间的关联关系越来越复杂,全面的测试设计不仅要考虑所测试系统,还要考虑关联交易以及上下游关联系统.通过在工作中不断摸索尝试,本 ...

  5. FinTech浪潮已到,五大金融场景将迎变革

    FinTech是Finance+Technology的缩写,可译为"金融科技",与"互联网金融"一词相比,它更能突出科技的重要性.FinTech所带来的,不仅仅 ...

  6. 揭秘 RocketMQ 新特性以及在金融场景下的实践

    2019 年末, RocketMQ 正式发布了 4.6.0 版本,增加了" Request-Reply "的同步调用的新特性." Request-Reply " ...

  7. 华为mysql金融版_华为云数据库MySQL金融版公测,打造满足金融场景数据安全性的高端产品...

    日前,华为云数据库推出MySQL 金融版,基于Paxos协议,采用一主两备三节点架构,解决数据库分布式环境下数据一致性的问题,实现了自动脑裂保护机制,保证数据库高可用和高可靠,满足金融场景下的数据库高 ...

  8. rocketmq新扩容的broker没有tps_揭秘 RocketMQ 新特性以及在金融场景下的实践

    2019 年末, RocketMQ 正式发布了 4.6.0 版本,增加了" Request-Reply "的同步调用的新特性." Request-Reply " ...

  9. 核心金融场景分布式事务

    分布式事务是分布式系统架构设计中的一个技术难点,特别是在这几年越来越火的微服务架构中,服务拆分所带来的跨服务数据一致性问题亟待解决,本文将围绕分布式事务产生背景和蚂蚁金服的分布式事务解决方案(SOFA ...

最新文章

  1. 98页PPT,看懂阿里、小米、京东、美团的组织架构和战略变迁!
  2. 支付宝能扫码闪电开发票了!人均省时3分钟
  3. python【力扣LeetCode算法题库】136-只出现一次的数字
  4. jmeter 逻辑控制器
  5. LaTex文章中插入Visio及Matlab矢量图
  6. Git关于pull,commit,push的总结
  7. php麻将机器人ai算法,高性能麻将AI算法
  8. java中对map使用entrySet循环
  9. Java之Collections.emptyList()、emptySet()、emptyMap()的作用和好处以及要注意的地方。
  10. 人工智能TensorFlow工作笔记004---还记得标准差嘛_标准差的由来
  11. [转] css3变形属性transform
  12. PAT-乙级-1035 插入与归并
  13. matlab平稳性检验,平稳性检验方法的有效性研究
  14. openstack详解(二十四)——Neutron服务注册
  15. 数列极限:重要极限 π 与 e
  16. 塑造棋牌游戏文化内涵
  17. 一元三次方程求解(洛谷)c语言
  18. 【光学】--色度学与Lab模型
  19. nginx php permanent,Nginx中的rewrite指令详解(break,last,redirect,permanent)
  20. 年后跳槽:哪些迹象告诉我们,公司可能不行了?

热门文章

  1. 自定义View-仿小米秒钟
  2. 阿米巴管理 在软件企业中的问题
  3. 计算机窗口中找不到桌面,电脑桌面上的部分图标不见了怎么办啊
  4. 宏基服务器 安装系统安装系统,韩博士win7系统重装,宏基f5-573g一键安装系统win7图文...
  5. VScode配置Leetcode环境
  6. 格林威治时间转换成北京时间,Mon May 10 2021 15:34:42 GMT+0800 (中国标准时间) TO 2021/5/10
  7. linux服务器管理系统/虚拟主机管理系统wdcp v2.2发布
  8. 【兴趣书签】十部国产黑暗动画经典
  9. 图片处理:完成图片的颜色反转
  10. BME680及bsec在STM32上的应用