
DECIMAL 数据处理原理浅析
注:本文分析内容基于 MySQL 8.0 版本
文章开始前先复习一下官方文档关于 DECIMAL
类型的一些介绍:
The declaration syntax for a DECIMAL column is DECIMAL(M,D). The ranges of values for the arguments are as follows:
M is the maximum number of digits (the precision). It has a range of 1 to 65.
D is the number of digits to the right of the decimal point (the scale). It has a range of 0 to 30 and must be no larger than M.
If D is omitted, the default is 0. If M is omitted, the default is 10.
The maximum value of 65 for M means that calculations on DECIMAL values are accurate up to 65 digits. This limit of 65 digits of precision also applies to exact-value numeric literals, so the maximum range of such literals differs from before. (There is also a limit on how long the text of DECIMAL literals can be; see Section 12.25.3, “Expression Handling”.)
以上材料提到的最大精度和小数位是本文分析关注的重点:
- 最大精度是
65
位 - 小数位最多
30
位
接下来将先分析 MySQL 服务输入处理 DECIMAL
类型的常数。
现在,先抛出几个问题:
- MySQL 中当使用
SELECT
查询常数时,例如:SELECT 123456789.123;
是如何处理的? - MySQL 中查询以下两条语句分别返回结果是多少?为什么?
MySQL 如何解析常数
来看第1个问题,MySQL 的词法分析在处理 SELECT
查询常数的语句时,会根据数字串的长度选择合适的类型来存储数值,决策逻辑代码位于 int_token(const char *str, uint length)@sql_lex.cc
,具体的代码片段如下:
上面代码中,long_len
值为 10
,longlong_len
值为 19
,unsigned_longlong_len
值为20
。
neg
表示是否是负数,直接看正数的处理分支,负数同理:
- 当输入的数值串长度等于
10
时 MySQL 可能使用LONG_NUM
或LONG_NUM
表示 - 当输入的数值串长度小于
19
时 MySQL 使用LONG_NUM
表示 - 当输入的数值串长度等于
20
时 MySQL 可能使用LONG_NUM
或DECIMAL_NUM
表示 - 当输入的数值串长度大于
20
时 MySQL 使用DECIMAL_NUM
表示 - 其他长度时,MySQL 可能使用
LONG_NUM
或ULONGLONG_NUM
表示
对于可能有两种表示方式的数据,MySQL 是通过将数字串与 cmp
指向的数值字符串进行比较,如果小于等于 cmp
表示的数值则使用 smaller
表示,否则使用 bigger
表示。cmp
指向的数值字符串定义在 sql_lex.cc
文件中,具体如下:
因此,这里我们可以得出结论:MySQL 中当使用 SELECT
查询常数时,根据数值串的长度和数值大小来决定使用什么类型来接收常数。当数值串长度大于 20
,或数值串长度等于 20
且数值小于-9223372036854775808
或大于18446744073709551615
时,MySQL 服务选择使用 DECIMAL
类型来接收处理常数。
这里,再抛出一个问题:
- 上面分析提到的
DECIMAL
是否与官方文档中提到的DECIMAL
类型或者换一种方式说:是否与建表语句CREATE TABLE t(d DECIMAL(65, 30));
中字段d
的DECIMAL(65, 30)
类型(可以不考虑精度和小数位)相同?
MySQL 解析 DECIMAL 常数时怎么处理溢出
分析第2个问题,先看一下语句的执行结果:
接着上面的思路往下看常数的语法解析:
语法解析器在获取到 toekn = DECIMAL_NUM
后,会创建一个 Item_decimal
对象来存储输入的数值。
在分析代码之前先来看几个常数定义:
-
DECIMAL_BUFF_LENGTH
:表示整个DECIMAL
类型数据的缓冲区大小 -
DECIMAL_MAX_POSSIBLE_PRECISION
:每个缓冲区单元可以存储9
位数字,所以最大可以处理的精度这里为81
-
DECIMAL_MAX_PRECISION
:用来限制官方文档介绍中decimal(M,D)
中的M
的最大值,亦或是当超大常数溢出后返回的整数部分最大长度 -
DECIMAL_MAX_SCALE
:用来限制官方文档介绍中decimal(M,D)
中的D
的最大值
在Item_decimal
构造函数中调用str2my_decimal
函数对输入数值进行处理,将其转换为my_decimal
类型的数据。
str2my_decimal
函数先将数值字符串转为合适的字符集后,调用 string2decimal
函数将数值字符串转为 decimal_t
类型的数据。my_decimal
类型和 decimal_t
类型的关系如下:
-
decimal_digit_t
是int32_t
的别名 -
intg
表示整数部分的字符个数 -
frac
表示小数部分的字符个数 -
sign
表示是否负数 -
buf
指向buffer
-
buffer
是数据存放数组,数组长度为9
,也就意味着一个decimal
最多可以存放9
个int32_t
大小的数据,但由于设计限制每个数组元素限制存储9
个字符,因此buffer
最多可以存储81
个字符
由于 buffer
长度的限制,在 string2decimal
函数解析时会有溢出的可能,因此,解析后还需要调用check_result_and_overflow
函数处理溢出的情况。
string2decimal
的代码实现:
解析过程大致如下:
- 分别计算整数部分和小数部分各有多少个字符
- 分别计算整数部分和小数部分各需要多少个
buffer
元素来存储
- 如果整数部分需要的
buffer
元素个数超过9
,则表示溢出 - 如果整数部分和小数部分需要的
buffer
元素个数超过9
,则表示需要将小数部分进行截断 由于先解析整数部分,再解析小数部分,因此,如果整数部分如果完全占用所有buffer
元素,此时,小数部分会被截断。
- 将整数部分和小数部分按每
9
个字符转为一个整数记录到buffer
的元素中(buffer
中的模型示例如下)
check_result_and_overflow
代码实现:
如果 check_result_and_overflow
调用之前的处理发生了溢出行为,则意味着 decimal
不能存储完整的数据,MySQL 决定这种情况下仅返回decimal
默认的最大精度数值,由上面的代码片段可以看出最大精度数值是 65
个 9
。
超大常量数据生成的 DECIMAL 数据与 DECIMAL 字段类型的区别
通过上面对超大常量数据生成的 DECIMAL
数据处理的分析,可以得出问题3的答案:两者不同,区别如下:
-
DECIMAL
字段类型有显式的精度和小数位的限制,也就是DECIMAL
字段插入数据时能插入的正数部分的长度为M-D
,而超大常量数据生成的DECIMAL
数据则会隐含的优先处理考虑整数部分,整数部分处理完才继续处理小数部分,如果缓冲区不够则将小数位截断,如果缓冲区不够整数部分存放则转为65
个9
。 - 在 MySQL 的服务源码中
DECIMAL
字段类型使用Field_new_decimal
类型接收处理,而超大常量数据生成的DECIMAL
数据由Item_decimal
类型接收处理。
本文转载自公众号:GreatSQL社区
