0
0
0
0
博客/.../

TiDB存储Decimal类型数据逻辑

 TiDBer_zzCOdTu4  发表于  2025-11-07

最近在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. 按组打包

    • 把整数部分从高位到低位按组(每组最多 9 位)分组;这里只有一组(123),需 2 字节存储。
    • 把小数部分从高位到低位按组分组;这里只有一组(45),需 1 字节存储。
    • 每组的数值以二进制整数形式存放(紧凑存放,不是 ASCII)。例如整数 123 -> 对应数值 123,会以 2 字节或合适的字节数保存其数值。
  3. 加上符号位(正/负区分)

    • 对于正数:把编码序列的第一个字节的最高位置 1(即把该字节与 0x80 或在实现中置位的方式)。
    • 对于负数:按 MySQL 规则做按位取反(ones’ complement)以方便排序。
  4. 输出字节序列最终产物是一个紧凑的字节数组:整数组字节(高位组先)紧接着小数组字节(高位组先),并在第一个字节包含符号标志位。

    [ 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 层格式化为字符串或其他输出。

0
0
0
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论