ToB企服应用市场:ToB评测及商务社交产业平台

标题: DECIMAL 数据处理原理浅析 [打印本页]

作者: 慢吞云雾缓吐愁    时间: 2022-9-16 17:21
标题: 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”.)
以上材料提到的最大精度和小数位是本文分析关注的重点:
接下来将先分析 MySQL 服务输入处理 DECIMAL 类型的常数。
现在,先抛出几个问题:
MySQL 如何解析常数

来看第1个问题,MySQL 的词法分析在处理 SELECT 查询常数的语句时,会根据数字串的长度选择合适的类型来存储数值,决策逻辑代码位于 int_token(const char *str, uint length)@sql_lex.cc,具体的代码片段如下:
  1. static inline uint int_token(const char *str, uint length) {
  2.   ...
  3.   if (neg) {
  4.       cmp = signed_long_str + 1;
  5.       smaller = NUM;      // If <= signed_long_str
  6.       bigger = LONG_NUM;  // If >= signed_long_str
  7.     } else if (length < signed_longlong_len)
  8.       return LONG_NUM;
  9.     else if (length > signed_longlong_len)
  10.       return DECIMAL_NUM;
  11.     else {
  12.       cmp = signed_longlong_str + 1;
  13.       smaller = LONG_NUM;  // If <= signed_longlong_str
  14.       bigger = DECIMAL_NUM;
  15.     }
  16.   } else {
  17.     if (length == long_len) {
  18.       cmp = long_str;
  19.       smaller = NUM;
  20.       bigger = LONG_NUM;
  21.     } else if (length < longlong_len)
  22.       return LONG_NUM;
  23.     else if (length > longlong_len) {
  24.       if (length > unsigned_longlong_len) return DECIMAL_NUM;
  25.       cmp = unsigned_longlong_str;
  26.       smaller = ULONGLONG_NUM;
  27.       bigger = DECIMAL_NUM;
  28.     } else {
  29.       cmp = longlong_str;
  30.       smaller = LONG_NUM;
  31.       bigger = ULONGLONG_NUM;
  32.     }
  33.   }
  34.   while (*cmp && *cmp++ == *str++)
  35.     ;
  36.   return ((uchar)str[-1] <= (uchar)cmp[-1]) ? smaller : bigger;
  37. }
复制代码
接着上面的思路往下看常数的语法解析:
  1. static const char *long_str = "2147483647";
  2. static const uint long_len = 10;
  3. static const char *signed_long_str = "-2147483648";
  4. static const char *longlong_str = "9223372036854775807";
  5. static const uint longlong_len = 19;
  6. static const char *signed_longlong_str = "-9223372036854775808";
  7. static const uint signed_longlong_len = 19;
  8. static const char *unsigned_longlong_str = "18446744073709551615";
  9. static const uint unsigned_longlong_len = 20;
复制代码
语法解析器在获取到 toekn = DECIMAL_NUM 后,会创建一个 Item_decimal 对象来存储输入的数值。
在分析代码之前先来看几个常数定义:
  1. root@mysqldb 14:09:  [(none)]> SELECT 111111111111111111111111111111111111111111111111111111111111111111111111111111111;
  2. +-----------------------------------------------------------------------------------+
  3. | 111111111111111111111111111111111111111111111111111111111111111111111111111111111 |
  4. +-----------------------------------------------------------------------------------+
  5. | 111111111111111111111111111111111111111111111111111111111111111111111111111111111 |
  6. +-----------------------------------------------------------------------------------+
  7. 1 row in set (2.28 sec)
  8. root@mysqldb 14:09:  [(none)]> SELECT 1111111111111111111111111111111111111111111111111111111111111111111111111111111111;
  9. +------------------------------------------------------------------------------------+
  10. | 1111111111111111111111111111111111111111111111111111111111111111111111111111111111 |
  11. +------------------------------------------------------------------------------------+
  12. |                  99999999999999999999999999999999999999999999999999999999999999999 |
  13. +------------------------------------------------------------------------------------+
  14. 1 row in set, 1 warning (2.01 sec)
复制代码
  1. NUM_literal:
  2.           int64_literal
  3.         | DECIMAL_NUM
  4.           {
  5.             $$= NEW_PTN Item_decimal(@$, $1.str, $1.length, YYCSCL);
  6.           }
  7.         | FLOAT_NUM
  8.           {
  9.             $$= NEW_PTN Item_float(@$, $1.str, $1.length);
  10.           }
  11.         ;
复制代码
在Item_decimal构造函数中调用str2my_decimal函数对输入数值进行处理,将其转换为my_decimal类型的数据。
  1. /** maximum length of buffer in our big digits (uint32). */
  2. static constexpr int DECIMAL_BUFF_LENGTH{9};
  3. /** the number of digits that my_decimal can possibly contain */
  4. static constexpr int DECIMAL_MAX_POSSIBLE_PRECISION{DECIMAL_BUFF_LENGTH * 9};
  5. /**
  6.   maximum guaranteed precision of number in decimal digits (number of our
  7.   digits * number of decimal digits in one our big digit - number of decimal
  8.   digits in one our big digit decreased by 1 (because we always put decimal
  9.   point on the border of our big digits))
  10. */
  11. static constexpr int DECIMAL_MAX_PRECISION{DECIMAL_MAX_POSSIBLE_PRECISION -
  12.                                            8 * 2};
  13. static constexpr int DECIMAL_MAX_SCALE{30};
复制代码
str2my_decimal 函数先将数值字符串转为合适的字符集后,调用 string2decimal 函数将数值字符串转为 decimal_t 类型的数据。my_decimal 类型和 decimal_t 类型的关系如下:
  1. Item_decimal::Item_decimal(const POS &pos, const char *str_arg, uint length,
  2.                            const CHARSET_INFO *charset)
  3.     : super(pos) {
  4.   str2my_decimal(E_DEC_FATAL_ERROR, str_arg, length, charset, &decimal_value);
  5.   item_name.set(str_arg);
  6.   set_data_type(MYSQL_TYPE_NEWDECIMAL);
  7.   decimals = (uint8)decimal_value.frac;
  8.   fixed = true;
  9.   max_length = my_decimal_precision_to_length_no_truncation(
  10.       decimal_value.intg + decimals, decimals, unsigned_flag);
  11. }
复制代码
解析过程大致如下:
  1. int str2my_decimal(uint mask, const char *from, size_t length,
  2.                    const CHARSET_INFO *charset, my_decimal *decimal_value) {
  3.   const char *end, *from_end;
  4.   int err;
  5.   char buff[STRING_BUFFER_USUAL_SIZE];
  6.   String tmp(buff, sizeof(buff), &my_charset_bin);
  7.   if (charset->mbminlen > 1) {
  8.     uint dummy_errors;
  9.     tmp.copy(from, length, charset, &my_charset_latin1, &dummy_errors);
  10.     from = tmp.ptr();
  11.     length = tmp.length();
  12.     charset = &my_charset_bin;
  13.   }
  14.   from_end = end = from + length;
  15.   err = string2decimal(from, (decimal_t *)decimal_value, &end);
  16.   if (end != from_end && !err) {
  17.     /* Give warning if there is something other than end space */
  18.     for (; end < from_end; end++) {
  19.       if (!my_isspace(&my_charset_latin1, *end)) {
  20.         err = E_DEC_TRUNCATED;
  21.         break;
  22.       }
  23.   }
  24.   check_result_and_overflow(mask, err, decimal_value);
  25.   return err;
  26. }
复制代码
check_result_and_overflow 代码实现:
  1. @startuml
  2. class decimal_t
  3. {
  4.   + int intg, frac, len;
  5.   + bool sign;
  6.   + decimal_digit_t *buf;
  7. }
  8. class my_decimal
  9. {
  10.   - decimal_digit_t buffer[DECIMAL_BUFF_LENGTH];
  11. }
  12. decimal_t <|-- my_decimal
  13. @enduml
复制代码
如果 check_result_and_overflow 调用之前的处理发生了溢出行为,则意味着 decimal 不能存储完整的数据,MySQL 决定这种情况下仅返回decimal 默认的最大精度数值,由上面的代码片段可以看出最大精度数值是 65 个 9。
超大常量数据生成的 DECIMAL 数据与 DECIMAL 字段类型的区别

通过上面对超大常量数据生成的 DECIMAL 数据处理的分析,可以得出问题3的答案:两者不同,区别如下:

Enjoy GreatSQL
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4