GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 作者: 好好先生 一、問題引入 今天遇到一個很奇怪的問題,在MySQL客戶端輸入,用不同科學計數法表示的數值,展示效果卻截然不同: mysql> sel ...
- GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。
- GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。
- 作者: 好好先生
一、問題引入
今天遇到一個很奇怪的問題,在MySQL客戶端輸入,用不同科學計數法表示的數值,展示效果卻截然不同:
mysql> select 1e+14,1e+15;
+-----------------+-------+
| 1e+14 | 1e+15 |
+-----------------+-------+
| 100000000000000 | 1e15 |
+-----------------+-------+
為什麼都是用科學計數法,一個是用完全展開的形式表示,另外一個卻變成用科學計數法來表示?
二、代碼跟蹤
我們知道,在MySQL中解析這類科學計數法的標識token,是通過BISON來進行詞法和語法解析的,並最終轉成Item類型,Item構造初始化的堆棧如下所示:
#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
#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
#2 0x00005555592503af in MYSQLparse (YYTHD=0x7fff2c001040, parse_tree=0x7fffe84506b0) at /home/greatdb/sql/sql_yacc.yy:16616
#3 0x0000555558ea2410 in THD::sql_parser (this=0x7fff2c001040) at /home/greatdb/sql/sql_class.cc:3149
#4 0x0000555558fe8126 in parse_sql (thd=0x7fff2c001040, parser_state=0x7fffe8450990, creation_ctx=0x0) at /home/greatdb/sql/sql_parse.cc:7391
#5 0x0000555558fe1f16 in dispatch_sql_command (thd=0x7fff2c001040, parser_state=0x7fffe8450990, update_userstat=false) at /home/greatdb/sql/sql_parse.cc:5293
#6 0x0000555558fd7969 in dispatch_command (thd=0x7fff2c001040, com_data=0x7fffe8451b70, command=COM_QUERY) at /home/greatdb/sql/sql_parse.cc:1994
#7 0x0000555558fd5cd9 in do_command (thd=0x7fff2c001040) at /home/greatdb/sql/sql_parse.cc:1442
#8 0x00005555591fffc6 in handle_connection (arg=0x555560992ff0) at /home/greatdb/sql/conn_handler/connection_handler_per_thread.cc:307
#9 0x000055555ae2314b in pfs_spawn_thread (arg=0x5555608507a0) at /home/greatdb/storage/perfschema/pfs.cc:2899
#10 0x00007ffff77c3609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#11 0x00007ffff76e8133 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
我們仔細看下麵這個函數的實現:
void Item_float::init(const char *str_arg, uint length) {
int error;
const char *end_not_used;
//經過觀察,我們發現:不管是1e+14還是1e+15,這裡把字元串轉成浮點數,都是完全展開形式。
//即1e+14展開為100000000000000
//1e+15展開為1000000000000000
//說明至少到目前為止,沒有觸發展示差異的原因。
value = my_strntod(&my_charset_bin, str_arg, length, &end_not_used, &error);
if (error) {
char tmp[NAME_LEN + 1];
snprintf(tmp, sizeof(tmp), "%.*s", length, str_arg);
my_error(ER_ILLEGAL_VALUE_FOR_TYPE, MYF(0), "double", tmp);
}
presentation.copy(str_arg, length);
item_name.copy(str_arg, length);
set_data_type(MYSQL_TYPE_DOUBLE);
decimals = (uint8)nr_of_decimals(str_arg, str_arg + length);
max_length = length;
fixed = true;
}
接著我們繼續查看Item處理結果的堆棧信息:
#0 my_gcvt (x=100000000000000, type=MY_GCVT_ARG_DOUBLE, width=342, to=0x7fffe844ec60 "\033", error=0x0) at /home/greatdb/strings/dtoa.cc:428
#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
#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
#3 0x00005555594ba703 in Protocol_text::store_double (this=0x7fff2c005600, from=100000000000000, decimals=31, zerofill=0) at /home/greatdb/sql/protocol_classic.cc:3579
#4 0x0000555558aa2260 in Item::send (this=0x7fff2c021f90, protocol=0x7fff2c005600, buffer=0x7fffe844ef00) at /home/greatdb/sql/item.cc:7625
#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
#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
#7 0x000055555912c6e3 in Query_expression::ExecuteIteratorQuery (this=0x7fff2c021408, thd=0x7fff2c001040) at /home/greatdb/sql/sql_union.cc:1282
#8 0x000055555912ca74 in Query_expression::execute (this=0x7fff2c021408, thd=0x7fff2c001040) at /home/greatdb/sql/sql_union.cc:1342
#9 0x0000555559069670 in Sql_cmd_dml::execute_inner (this=0x7fff2c022c50, thd=0x7fff2c001040) at /home/greatdb/sql/sql_select.cc:827
#10 0x00005555590689ec in Sql_cmd_dml::execute (this=0x7fff2c022c50, thd=0x7fff2c001040) at /home/greatdb/sql/sql_select.cc:580
#11 0x0000555558fe0324 in mysql_execute_command (thd=0x7fff2c001040, first_level=true) at /home/greatdb/sql/sql_parse.cc:4788
#12 0x0000555558fe24bb in dispatch_sql_command (thd=0x7fff2c001040, parser_state=0x7fffe8450990, update_userstat=false) at /home/greatdb/sql/sql_parse.cc:5393
#13 0x0000555558fd7969 in dispatch_command (thd=0x7fff2c001040, com_data=0x7fffe8451b70, command=COM_QUERY) at /home/greatdb/sql/sql_parse.cc:1994
#14 0x0000555558fd5cd9 in do_command (thd=0x7fff2c001040) at /home/greatdb/sql/sql_parse.cc:1442
#15 0x00005555591fffc6 in handle_connection (arg=0x555560992ff0) at /home/greatdb/sql/conn_handler/connection_handler_per_thread.cc:307
#16 0x000055555ae2314b in pfs_spawn_thread (arg=0x5555608507a0) at /home/greatdb/storage/perfschema/pfs.cc:2899
#17 0x00007ffff77c3609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#18 0x00007ffff76e8133 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
我們發現不管是1e+14還是1e+15,最終都會進入到my_gcvt這個函數裡面,此函數大概的功能是把一個浮點數類型轉成對應的字元串類型輸出。現在我們來看該函數的關鍵邏輯實現:
//1、x表示要轉換的浮點數
//2、width表示展示寬度。具體來說:MySQL其實對資料庫返回的每一個列的最大寬度是有要求的。如果浮點數是完全展開的情況,那就有可能超過這個展示的最大寬度,這裡固定取值342
//3、to表示存放轉換後浮點數x對應的字元數組指針
size_t my_gcvt(double x, my_gcvt_arg_type type, int width, char *to,
bool *error) {
int decpt, sign, len, exp_len;
char *res, *src, *end, *dst = to, *dend = dst + width;
char buf[DTOA_BUFF_SIZE];
bool have_space, force_e_format;
assert(width > 0 && to != nullptr);
...
//這裡主要是為了計算浮點數x的存儲的小數點位置decpt。
//比如100000000000000,得到的decpt為15
//1000000000000000,得到的decpt為16
res =
dtoa(x, 4, type == MY_GCVT_ARG_DOUBLE ? width : std::min(width, FLT_DIG),
&decpt, &sign, &end, buf, sizeof(buf));
...
//have_space為真表示,浮點數完全展開的形式對應的長度,符合MySQL列展示最大寬度的要求。
//下麵這個判斷的邏輯主要是說:
//浮點數的展示寬度足夠或者展示寬度不足的情況下,繼續判斷小數點位置decpt是否介於[-MAX_DECPT_FOR_F_FORMAT + 1,MAX_DECPT_FOR_F_FORMAT]之間。
//跟蹤一下代碼的巨集定義,很容易發現MAX_DECPT_FOR_F_FORMAT定義的長度就是15。
//那這樣就是說decpt介於[-14,15]之間的時候,也是符合要求的。
if ((have_space ||
/*
Not enough space, let's see if the 'f' format provides the most number
of significant digits.
*/
((decpt <= width &&
(decpt >= -1 || (decpt == -2 && (len > 1 || !force_e_format)))) &&
!force_e_format)) &&
/*
Use the 'e' format in some cases even if we have enough space for the
'f' one. See comment for MAX_DECPT_FOR_F_FORMAT.
*/
(!have_space || (decpt >= -MAX_DECPT_FOR_F_FORMAT + 1 &&
(decpt <= MAX_DECPT_FOR_F_FORMAT || len > decpt)))) {
//如果符合這個條件的要求,那浮點數x就按照'f' format,即非科學計數法,完全展開形式處理。
//1e+14的decpt取值為15,介於[-14,15]區間,故按照完全展開形式處理。
...
}else{
//否則浮點數x按照'e' format,即科學計數法表示。
//1e+15的decpt取值為16,超出[-14,15]區間,故按照科學計數法形式處理。
...
}
}
三、總結
經過代碼的調用分析,發現最終的結論和輸入數據的現象相符。當我們在使用MySQL過程中,遇到問題的時候,不要慌亂。可以嘗試從源碼分析的角度作為切入點,從根源上理解這種現象觸發的原因,更能進一步加深我們對資料庫運行機制的瞭解和掌握。
Enjoy GreatSQL