MySQL科学计数法展示解惑

打印 上一主题 下一主题

主题 679|帖子 679|积分 2037


  • GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源。
  • GreatSQL是MySQL的国产分支版本,使用上与MySQL一致。
  • 作者: 好好先生
一、问题引入

今天遇到一个很奇怪的问题,在MySQL客户端输入,用不同科学计数法表示的数值,展示效果却截然不同:
  1. mysql> select 1e+14,1e+15;
  2. +-----------------+-------+
  3. | 1e+14           | 1e+15 |
  4. +-----------------+-------+
  5. | 100000000000000 |  1e15 |
  6. +-----------------+-------+
复制代码
为什么都是用科学计数法,一个是用完全展开的形式表示,另外一个却变成用科学计数法来表示?
二、代码跟踪

我们知道,在MySQL中解析这类科学计数法的标识token,是通过BISON来进行词法和语法解析的,并最终转成Item类型,Item构造初始化的堆栈如下所示:
  1. #0  Item_float::init (this=0x7fffe844e8c0, str_arg=0xe7f312c00e0f8 <error: Cannot access memory at address 0xe7f312c00e0f8>, length=21845) at /home/greatdb/sql/item.cc:7216
  2. #1  0x0000555559260095 in Item_float::Item_float (this=0x7fff2c00dad8, pos=..., str_arg=0x7fff2c00dac8 "1.234e-14", length=9) at /home/greatdb/sql/item.h:5237
  3. #2  0x00005555592503af in MYSQLparse (YYTHD=0x7fff2c001040, parse_tree=0x7fffe84506b0) at /home/greatdb/sql/sql_yacc.yy:16616
  4. #3  0x0000555558ea2410 in THD::sql_parser (this=0x7fff2c001040) at /home/greatdb/sql/sql_class.cc:3149
  5. #4  0x0000555558fe8126 in parse_sql (thd=0x7fff2c001040, parser_state=0x7fffe8450990, creation_ctx=0x0) at /home/greatdb/sql/sql_parse.cc:7391
  6. #5  0x0000555558fe1f16 in dispatch_sql_command (thd=0x7fff2c001040, parser_state=0x7fffe8450990, update_userstat=false) at /home/greatdb/sql/sql_parse.cc:5293
  7. #6  0x0000555558fd7969 in dispatch_command (thd=0x7fff2c001040, com_data=0x7fffe8451b70, command=COM_QUERY) at /home/greatdb/sql/sql_parse.cc:1994
  8. #7  0x0000555558fd5cd9 in do_command (thd=0x7fff2c001040) at /home/greatdb/sql/sql_parse.cc:1442
  9. #8  0x00005555591fffc6 in handle_connection (arg=0x555560992ff0) at /home/greatdb/sql/conn_handler/connection_handler_per_thread.cc:307
  10. #9  0x000055555ae2314b in pfs_spawn_thread (arg=0x5555608507a0) at /home/greatdb/storage/perfschema/pfs.cc:2899
  11. #10 0x00007ffff77c3609 in start_thread (arg=<optimized out>) at pthread_create.c:477
  12. #11 0x00007ffff76e8133 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
复制代码
我们仔细看下面这个函数的实现:
  1. void Item_float::init(const char *str_arg, uint length) {
  2.   int error;
  3.   const char *end_not_used;
  4.   //经过观察,我们发现:不管是1e+14还是1e+15,这里把字符串转成浮点数,都是完全展开形式。
  5.   //即1e+14展开为100000000000000
  6.   //1e+15展开为1000000000000000
  7.   //说明至少到目前为止,没有触发展示差异的原因。
  8.   value = my_strntod(&my_charset_bin, str_arg, length, &end_not_used, &error);
  9.   if (error) {
  10.     char tmp[NAME_LEN + 1];
  11.     snprintf(tmp, sizeof(tmp), "%.*s", length, str_arg);
  12.     my_error(ER_ILLEGAL_VALUE_FOR_TYPE, MYF(0), "double", tmp);
  13.   }
  14.   presentation.copy(str_arg, length);
  15.   item_name.copy(str_arg, length);
  16.   set_data_type(MYSQL_TYPE_DOUBLE);
  17.   decimals = (uint8)nr_of_decimals(str_arg, str_arg + length);
  18.   max_length = length;
  19.   fixed = true;
  20. }
复制代码
接着我们继续查看Item处理结果的堆栈信息:
  1. #0  my_gcvt (x=100000000000000, type=MY_GCVT_ARG_DOUBLE, width=342, to=0x7fffe844ec60 "\033", error=0x0) at /home/greatdb/strings/dtoa.cc:428
  2. #1  0x00005555594ba4d5 in floating_point_to_text (value=100000000000000, decimals=31, gcvt_arg_type=MY_GCVT_ARG_DOUBLE, buffer=0x7fffe844ec60 "\033") at /home/greatdb/sql/protocol_classic.cc:3539
  3. #2  0x00005555594ba53d in store_floating_point (value=100000000000000, decimals=31, zerofill=0, gcvt_arg_type=MY_GCVT_ARG_DOUBLE, packet=0x7fff2c003148) at /home/greatdb/sql/protocol_classic.cc:3558
  4. #3  0x00005555594ba703 in Protocol_text::store_double (this=0x7fff2c005600, from=100000000000000, decimals=31, zerofill=0) at /home/greatdb/sql/protocol_classic.cc:3579
  5. #4  0x0000555558aa2260 in Item::send (this=0x7fff2c021f90, protocol=0x7fff2c005600, buffer=0x7fffe844ef00) at /home/greatdb/sql/item.cc:7625
  6. #5  0x0000555558ea1886 in THD::send_result_set_row (this=0x7fff2c001040, row_items=const mem_root_deque<Item*> & = {...}) at /home/greatdb/sql/sql_class.cc:2975
  7. #6  0x0000555558ddbf7d in Query_result_send::send_data (this=0x7fff2c022c88, thd=0x7fff2c001040, items=const mem_root_deque<Item*> & = {...}) at /home/greatdb/sql/query_result.cc:109
  8. #7  0x000055555912c6e3 in Query_expression::ExecuteIteratorQuery (this=0x7fff2c021408, thd=0x7fff2c001040) at /home/greatdb/sql/sql_union.cc:1282
  9. #8  0x000055555912ca74 in Query_expression::execute (this=0x7fff2c021408, thd=0x7fff2c001040) at /home/greatdb/sql/sql_union.cc:1342
  10. #9  0x0000555559069670 in Sql_cmd_dml::execute_inner (this=0x7fff2c022c50, thd=0x7fff2c001040) at /home/greatdb/sql/sql_select.cc:827
  11. #10 0x00005555590689ec in Sql_cmd_dml::execute (this=0x7fff2c022c50, thd=0x7fff2c001040) at /home/greatdb/sql/sql_select.cc:580
  12. #11 0x0000555558fe0324 in mysql_execute_command (thd=0x7fff2c001040, first_level=true) at /home/greatdb/sql/sql_parse.cc:4788
  13. #12 0x0000555558fe24bb in dispatch_sql_command (thd=0x7fff2c001040, parser_state=0x7fffe8450990, update_userstat=false) at /home/greatdb/sql/sql_parse.cc:5393
  14. #13 0x0000555558fd7969 in dispatch_command (thd=0x7fff2c001040, com_data=0x7fffe8451b70, command=COM_QUERY) at /home/greatdb/sql/sql_parse.cc:1994
  15. #14 0x0000555558fd5cd9 in do_command (thd=0x7fff2c001040) at /home/greatdb/sql/sql_parse.cc:1442
  16. #15 0x00005555591fffc6 in handle_connection (arg=0x555560992ff0) at /home/greatdb/sql/conn_handler/connection_handler_per_thread.cc:307
  17. #16 0x000055555ae2314b in pfs_spawn_thread (arg=0x5555608507a0) at /home/greatdb/storage/perfschema/pfs.cc:2899
  18. #17 0x00007ffff77c3609 in start_thread (arg=<optimized out>) at pthread_create.c:477
  19. #18 0x00007ffff76e8133 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
复制代码
我们发现不管是1e+14还是1e+15,最终都会进入到my_gcvt这个函数里面,此函数大概的功能是把一个浮点数类型转成对应的字符串类型输出。现在我们来看该函数的关键逻辑实现:
  1. //1、x表示要转换的浮点数
  2. //2、width表示展示宽度。具体来说:MySQL其实对数据库返回的每一个列的最大宽度是有要求的。如果浮点数是完全展开的情况,那就有可能超过这个展示的最大宽度,这里固定取值342
  3. //3、to表示存放转换后浮点数x对应的字符数组指针
  4. size_t my_gcvt(double x, my_gcvt_arg_type type, int width, char *to,
  5.                bool *error) {
  6.   int decpt, sign, len, exp_len;
  7.   char *res, *src, *end, *dst = to, *dend = dst + width;
  8.   char buf[DTOA_BUFF_SIZE];
  9.   bool have_space, force_e_format;
  10.   assert(width > 0 && to != nullptr);
  11. ...
  12. //这里主要是为了计算浮点数x的存储的小数点位置decpt。
  13. //比如100000000000000,得到的decpt为15
  14. //1000000000000000,得到的decpt为16
  15.   res =
  16.       dtoa(x, 4, type == MY_GCVT_ARG_DOUBLE ? width : std::min(width, FLT_DIG),
  17.            &decpt, &sign, &end, buf, sizeof(buf));
  18. ...
  19. //have_space为真表示,浮点数完全展开的形式对应的长度,符合MySQL列展示最大宽度的要求。
  20. //下面这个判断的逻辑主要是说:
  21. //浮点数的展示宽度足够或者展示宽度不足的情况下,继续判断小数点位置decpt是否介于[-MAX_DECPT_FOR_F_FORMAT + 1,MAX_DECPT_FOR_F_FORMAT]之间。
  22. //跟踪一下代码的宏定义,很容易发现MAX_DECPT_FOR_F_FORMAT定义的长度就是15。
  23. //那这样就是说decpt介于[-14,15]之间的时候,也是符合要求的。
  24.   if ((have_space ||
  25.        /*
  26.          Not enough space, let's see if the 'f' format provides the most number
  27.          of significant digits.
  28.        */
  29.        ((decpt <= width &&
  30.          (decpt >= -1 || (decpt == -2 && (len > 1 || !force_e_format)))) &&
  31.         !force_e_format)) &&
  32.       /*
  33.         Use the 'e' format in some cases even if we have enough space for the
  34.         'f' one. See comment for MAX_DECPT_FOR_F_FORMAT.
  35.       */
  36.       (!have_space || (decpt >= -MAX_DECPT_FOR_F_FORMAT + 1 &&
  37.                        (decpt <= MAX_DECPT_FOR_F_FORMAT || len > decpt)))) {
  38.   //如果符合这个条件的要求,那浮点数x就按照'f' format,即非科学计数法,完全展开形式处理。
  39.   //1e+14的decpt取值为15,介于[-14,15]区间,故按照完全展开形式处理。
  40.   ...
  41. }else{
  42.   //否则浮点数x按照'e' format,即科学计数法表示。
  43.   //1e+15的decpt取值为16,超出[-14,15]区间,故按照科学计数法形式处理。
  44.   ...
  45. }
  46. }
复制代码
三、总结

经过代码的调用分析,发现最终的结论和输入数据的现象相符。当我们在使用MySQL过程中,遇到问题的时候,不要慌乱。可以尝试从源码分析的角度作为切入点,从根源上理解这种现象触发的原因,更能进一步加深我们对数据库运行机制的了解和掌握。

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

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

科技颠覆者

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表