写点什么

TiDB 存储 Decimal 类型数据逻辑

作者: TiDBer_zzCOdTu4 原文来源:https://tidb.net/blog/e944f21f


最近在 TiDB 201 系列课程:https://learn.pingcap.cn/learner/course/1050001 里学了 TiDB 的数据类型。里面提到的各个类型的特点引起了我想深入了解其原理的兴趣。


Decimal 类型通常用于需要高精度的计算,比如财务计算。那么为什么它保持精度不丢失呢?让我们通过 DECIMAL(5,2) 存储与读取 123.45 来详细解析 Decimal 数据类型的存储逻辑吧。

核心思想:以整数形式存储

Decimal 类型的所有存储实现都基于一个核心原则:以一个定长的整数来存储数值,同时额外存储小数点位置信息。


这样做的好处是:


  • 精确表示:没有浮点数的舍入误差。

  • 计算精确:所有的算术运算(加、减、乘)都可以使用整数运算来完成,最后再调整小数点位置。

基本概念与规则

  • DECIMAL(M,D) 表示:总位数 M,小数位数 D;整数位数 = M - D。

  • 物理压缩规则

  • 每 9 个十进制数字打包为 4 字节(用一个 32 位整数保存那 9 位的数值)。

  • 不足 9 位的尾组按下表占用字节数(这是“按位压缩”的规则):

  • 0→0 字节

  • 1–2 →1 字节

  • 3–4 →2 字节

  • 5–6 →3 字节

  • 7–9 →4 字节

  • 符号处理

  • 正数:编码的第一个字节的最高位(MSB)被置为 1(即与 0x80 有关的标识)。

  • 负数:先对编码后的字节取按位反,然后最高位为 0(或相应规则),以区别负数。

TiDB 中 DECIMAL(5,2) 存储与读取 123.45 的逻辑

1) 把 123.45 拆解为整数组与小数组

  • 原数值:123.45

  • 整数部分(integer part) = 123(3 位)

  • 小数部分(fraction part) = 45(2 位)

  • 因为每 9 位一组,这里每边都只占 “剩余” 组:整数组需 2 字节(3 位 -> 对应 2 字节),小数组需 1 字节(2 位 -> 对应 1 字节)。(表格对照:3–4 位 → 2 字节;1–2 位 → 1 字节。)

2) 存储编码过程

  1. 解析数值把 "123.45" 解析为内部十进制结构:记录符号、总位数、整数位数、以及每一位的数字序列(或按组存储的数值)。

  2. 按组打包

  3. 把整数部分从高位到低位按组(每组最多 9 位)分组;这里只有一组(123),需 2 字节存储。

  4. 把小数部分从高位到低位按组分组;这里只有一组(45),需 1 字节存储。

  5. 每组的数值以二进制整数形式存放(紧凑存放,不是 ASCII)。例如整数 123 -> 对应数值 123,会以 2 字节或合适的字节数保存其数值。

  6. 加上符号位(正 / 负区分)

  7. 对于正数:把编码序列的第一个字节的最高位置 1(即把该字节与 0x80 或在实现中置位的方式)。

  8. 对于负数:按 MySQL 规则做按位取反(ones’ complement)以方便排序。

  9. 输出字节序列最终产物是一个紧凑的字节数组:整数组字节(高位组先)紧接着小数组字节(高位组先),并在第一个字节包含符号标志位。


[ firstByte_with_signBit ] [ remaining int-byte(s) ] [ frac-byte(s) ]


以 DECIMAL(5,2) 为例


  • 整数组(3 位)按 2 字节存储表示 123;小数组(2 位)按 1 字节表示 45

  • 加上正号标志(最高位置 1),得到的字节序列(示意)为: [ 0x80 ][ 0x7B ][ 0x2D ]


| 区段 | 含义 | 举例说明(以 DECIMAL(5,2)=123.45) | | —————————- | ———————————————————————– | ———————————————————- | | firstByte_with_signBit | 整数部分的最高字节 + 符号标志。 即在整数部分编码的第一个字节上,把最高位 (bit7) 置 1 表示“正数”;若为负,则取反并清零最高位。 | 整数部分 “123” 编码为 2 字节(00 7B), 正数 → 在第一个字节加上 0x80 → 80 7B | | remaining int-byte(s) | 整数部分剩余的字节(不含符号位部分)。 | 第二个字节为 7B(对应十进制 123 的余值) | | frac-byte(s) | 小数部分字节,按同样的压缩规则存储(1–2 位 → 1 字节)。 | 小数部分 “45” 编码为 2D |

3) 读取(解码)过程——TiDB 如何把存储的字节还原为可用的 Decimal

  1. 从存储读取原始字节数组

  2. 检查并处理符号位


  • 查看第一个字节的最高位:若为 1 → 原数为正;若为 0 → 可能是按位取反后的负数编码(需要补偿以还原原始负数数值)。

  • 对于负数,需要做反向操作(按位取反并重建十进制组)以得到原始数值。


  1. 按组拆分并恢复十进制位


  • 依据列定义(例如 DECIMAL(5,2) 的 M/D)或在编码中内嵌的长度信息,知道每一侧(整数组 / 小数组)应当按多少字节读取,依次读取每组字节并转换为对应的十进制数片段(把每组的二进制数值转换回对应的 1..9 位十进制字符序列)。

  • 将整数组段和小数组段拼起来,得出完整的十进制数字字符序列(例如 "12345"),再根据小数位数插入小数点得到 123.45


  1. 组装为内部数值对象与 SQL 层输出

结论

  • TiDB 的 DECIMAL 存储使用 * 压缩的十进制定点编码 *(每 9 位 -> 4 字节,剩余按表占字节)。

  • 编码时整数组在前、分组内用二进制数值保存、最高字节最高位用于标志正负(正数置 1,负数按位取反处理)。

  • 读取时反向:检查符号位 → 按组解析数值 → 重建十进制字符序列 → 变成内部 Decimal 对象,再由 SQL 层格式化为字符串或其他输出。


发布于: 37 分钟前阅读数: 4
用户头像

TiDB 社区官网:https://tidb.net/ 2021-12-15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
TiDB存储Decimal类型数据逻辑_学习&认证&课程_TiDB 社区干货传送门_InfoQ写作社区