注:本文分析內容基于 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 中查詢一下兩條陳述句分別回傳結果是多少?為什么?
SELECT 111111111111111111111111111111111111111111111111111111111111111111111111111111111; SELECT 1111111111111111111111111111111111111111111111111111111111111111111111111111111111;
MySQL 如何決議常數
來看第1個問題,MySQL 的詞法分析在處理 SELECT 查詢常數的陳述句時,會根據數字串的長度選擇合適的型別來存盤數值,決策邏輯代碼位于 int_token(const char *str, uint length)@sql_lex.cc,具體的代碼片段如下:
static inline uint int_token(const char *str, uint length) {
...
if (neg) {
cmp = signed_long_str + 1;
smaller = NUM; // If <= signed_long_str
bigger = LONG_NUM; // If >= signed_long_str
} else if (length < signed_longlong_len)
return LONG_NUM;
else if (length > signed_longlong_len)
return DECIMAL_NUM;
else {
cmp = signed_longlong_str + 1;
smaller = LONG_NUM; // If <= signed_longlong_str
bigger = DECIMAL_NUM;
}
} else {
if (length == long_len) {
cmp = long_str;
smaller = NUM;
bigger = LONG_NUM;
} else if (length < longlong_len)
return LONG_NUM;
else if (length > longlong_len) {
if (length > unsigned_longlong_len) return DECIMAL_NUM;
cmp = unsigned_longlong_str;
smaller = ULONGLONG_NUM;
bigger = DECIMAL_NUM;
} else {
cmp = longlong_str;
smaller = LONG_NUM;
bigger = ULONGLONG_NUM;
}
}
while (*cmp && *cmp++ == *str++)
;
return ((uchar)str[-1] <= (uchar)cmp[-1]) ? smaller : bigger;
}
上面代碼中,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 檔案中,具體如下:
static const char *long_str = "2147483647";
static const uint long_len = 10;
static const char *signed_long_str = "-2147483648";
static const char *longlong_str = "9223372036854775807";
static const uint longlong_len = 19;
static const char *signed_longlong_str = "-9223372036854775808";
static const uint signed_longlong_len = 19;
static const char *unsigned_longlong_str = "18446744073709551615";
static const uint unsigned_longlong_len = 20;
因此,這里我們可以得出結論:MySQL 中當使用 SELECT 查詢常數時,根據數值串的長度和數值大小來決定使用什么型別來接收常數,當數值串長度大于 20,或數值串長度等于 20 且數值小于-9223372036854775808或大于18446744073709551615時,MySQL 服務選擇使用 DECIMAL 型別來接收處理常數,
這里,再拋出一個問題:
3. 上面分析提到的 DECIMAL 是否與官方檔案中提到的 DECIMAL 型別或者換一種方式說:是否與建表陳述句 CREATE TABLE t(d DECIMAL(65, 30)); 中欄位 d 的 DECIMAL(65, 30)型別(可以不考慮精度和小數位)相同?
MySQL 決議 DECIMAL 常數時怎么處理溢位
分析第2個問題,先看一下陳述句的執行結果:
root@mysqldb 14:09: [(none)]> SELECT 111111111111111111111111111111111111111111111111111111111111111111111111111111111;
+-----------------------------------------------------------------------------------+
| 111111111111111111111111111111111111111111111111111111111111111111111111111111111 |
+-----------------------------------------------------------------------------------+
| 111111111111111111111111111111111111111111111111111111111111111111111111111111111 |
+-----------------------------------------------------------------------------------+
1 row in set (2.28 sec)
root@mysqldb 14:09: [(none)]> SELECT 1111111111111111111111111111111111111111111111111111111111111111111111111111111111;
+------------------------------------------------------------------------------------+
| 1111111111111111111111111111111111111111111111111111111111111111111111111111111111 |
+------------------------------------------------------------------------------------+
| 99999999999999999999999999999999999999999999999999999999999999999 |
+------------------------------------------------------------------------------------+
1 row in set, 1 warning (2.01 sec)
接著上面的思路往下看常數的語法決議:
NUM_literal:
int64_literal
| DECIMAL_NUM
{
$$= NEW_PTN Item_decimal(@$, $1.str, $1.length, YYCSCL);
}
| FLOAT_NUM
{
$$= NEW_PTN Item_float(@$, $1.str, $1.length);
}
;
語法決議器在獲取到 toekn = DECIMAL_NUM 后,會創建一個 Item_decimal 物件來存盤輸入的數值,
在分析代碼之前先來看幾個常數定義:
/** maximum length of buffer in our big digits (uint32). */
static constexpr int DECIMAL_BUFF_LENGTH{9};
/** the number of digits that my_decimal can possibly contain */
static constexpr int DECIMAL_MAX_POSSIBLE_PRECISION{DECIMAL_BUFF_LENGTH * 9};
/**
maximum guaranteed precision of number in decimal digits (number of our
digits * number of decimal digits in one our big digit - number of decimal
digits in one our big digit decreased by 1 (because we always put decimal
point on the border of our big digits))
*/
static constexpr int DECIMAL_MAX_PRECISION{DECIMAL_MAX_POSSIBLE_PRECISION -
8 * 2};
static constexpr int DECIMAL_MAX_SCALE{30};
DECIMAL_BUFF_LENGTH:表示整個DECIMAL型別資料的緩沖區大小DECIMAL_MAX_POSSIBLE_PRECISION:每個緩沖區單元可以存盤9位數字,所以最大可以處理的精度這里為81DECIMAL_MAX_PRECISION:用來限制官方檔案介紹中decimal(M,D)中的M的最大值,亦或是當超大常數溢位后回傳的整數部分最大長度DECIMAL_MAX_SCALE:用來限制官方檔案介紹中decimal(M,D)中的D的最大值
Item_decimal::Item_decimal(const POS &pos, const char *str_arg, uint length,
const CHARSET_INFO *charset)
: super(pos) {
str2my_decimal(E_DEC_FATAL_ERROR, str_arg, length, charset, &decimal_value);
item_name.set(str_arg);
set_data_type(MYSQL_TYPE_NEWDECIMAL);
decimals = (uint8)decimal_value.frac;
fixed = true;
max_length = my_decimal_precision_to_length_no_truncation(
decimal_value.intg + decimals, decimals, unsigned_flag);
}
在Item_decimal建構式中呼叫str2my_decimal函式對輸入數值進行處理,將其轉換為my_decimal型別的資料,
int str2my_decimal(uint mask, const char *from, size_t length,
const CHARSET_INFO *charset, my_decimal *decimal_value) {
const char *end, *from_end;
int err;
char buff[STRING_BUFFER_USUAL_SIZE];
String tmp(buff, sizeof(buff), &my_charset_bin);
if (charset->mbminlen > 1) {
uint dummy_errors;
tmp.copy(from, length, charset, &my_charset_latin1, &dummy_errors);
from = tmp.ptr();
length = tmp.length();
charset = &my_charset_bin;
}
from_end = end = from + length;
err = string2decimal(from, (decimal_t *)decimal_value, &end);
if (end != from_end && !err) {
/* Give warning if there is something other than end space */
for (; end < from_end; end++) {
if (!my_isspace(&my_charset_latin1, *end)) {
err = E_DEC_TRUNCATED;
break;
}
}
check_result_and_overflow(mask, err, decimal_value);
return err;
}
str2my_decimal 函式先將數值字串轉為合適的字符集后,呼叫 string2decimal 函式將數值字串轉為 decimal_t 型別的資料,my_decimal 型別和 decimal_t 型別的關系如下:
@startuml
class decimal_t
{
+ int intg, frac, len;
+ bool sign;
+ decimal_digit_t *buf;
}
class my_decimal
{
- decimal_digit_t buffer[DECIMAL_BUFF_LENGTH];
}
decimal_t <|-- my_decimal
@enduml
decimal_digit_t是int32_t的別名intg表示整數部分的字符個數frac表示小數部分的字符個數sign表示是否負數buf指向bufferbuffer是資料存放陣列,陣列長度為9,也就意味著一個decimal最多可以存放9個int32_t大小的資料,但由于設計限制每個陣列元素限制存盤9個字符,因此buffer最多可以存盤81個字符
由于 buffer 長度的限制,在 string2decimal 函式決議時會有溢位的可能,因此,決議后還需要呼叫check_result_and_overflow函式處理溢位的情況,
string2decimal 的代碼實作:
int string2decimal(const char *from, decimal_t *to, const char **end) {
const char *s = from, *s1, *endp, *end_of_string = *end;
int i, intg, frac, error, intg1, frac1;
dec1 x, *buf;
sanity(to);
error = E_DEC_BAD_NUM; /* In case of bad number */
while (s < end_of_string && my_isspace(&my_charset_latin1, *s)) s++;
if (s == end_of_string) goto fatal_error;
if ((to->sign = (*s == '-')))
s++;
else if (*s == '+')
s++;
s1 = s;
while (s < end_of_string && my_isdigit(&my_charset_latin1, *s)) s++;
intg = (int)(s - s1);
if (s < end_of_string && *s == '.') {
endp = s + 1;
while (endp < end_of_string && my_isdigit(&my_charset_latin1, *endp))
endp++;
frac = (int)(endp - s - 1);
} else {
frac = 0;
endp = s;
}
*end = endp;
if (frac + intg == 0) goto fatal_error;
error = 0;
intg1 = ROUND_UP(intg);
frac1 = ROUND_UP(frac);
FIX_INTG_FRAC_ERROR(to->len, intg1, frac1, error);
if (unlikely(error)) {
frac = frac1 * DIG_PER_DEC1;
if (error == E_DEC_OVERFLOW) intg = intg1 * DIG_PER_DEC1;
}
/* Error is guranteed to be set here */
to->intg = intg;
to->frac = frac;
buf = to->buf + intg1;
s1 = s;
for (x = 0, i = 0; intg; intg--) {
x += (*--s - '0') * powers10[i];
if (unlikely(++i == DIG_PER_DEC1)) {
*--buf = x;
x = 0;
i = 0;
}
}
if (i) *--buf = x;
buf = to->buf + intg1;
for (x = 0, i = 0; frac; frac--) {
x = (*++s1 - '0') + x * 10;
if (unlikely(++i == DIG_PER_DEC1)) {
*buf++ = x;
x = 0;
i = 0;
}
}
if (i) *buf = x * powers10[DIG_PER_DEC1 - i];
/* Handle exponent */
if (endp + 1 < end_of_string && (*endp == 'e' || *endp == 'E')) {
int str_error;
longlong exponent = my_strtoll10(endp + 1, &end_of_string, &str_error);
if (end_of_string != endp + 1) /* If at least one digit */
{
*end = end_of_string;
if (str_error > 0) {
error = E_DEC_BAD_NUM;
goto fatal_error;
}
if (exponent > INT_MAX / 2 || (str_error == 0 && exponent < 0)) {
error = E_DEC_OVERFLOW;
goto fatal_error;
}
if (exponent < INT_MIN / 2 && error != E_DEC_OVERFLOW) {
error = E_DEC_TRUNCATED;
goto fatal_error;
}
if (error != E_DEC_OVERFLOW) error = decimal_shift(to, (int)exponent);
}
}
/* Avoid returning negative zero, cfr. decimal_cmp() */
if (to->sign && decimal_is_zero(to)) to->sign = false;
return error;
fatal_error:
decimal_make_zero(to);
return error;
}
決議程序大致如下:
- 分別計算整數部分和小數部分各有多少個字符
- 分別計算整數部分和小數部分各需要多少個
buffer元素來存盤- 如果整數部分需要的
buffer元素個數超過9,則表示溢位 - 如果整數部分和小數部分需要的
buffer元素個數超過9,則表示需要將小數部分進行截斷
由于先決議整數部分,再決議小數部分,因此,如果整數部分如果完全占用所有buffer元素,此時,小數部分會被截斷,
- 如果整數部分需要的
- 將整數部分和小數部分按每
9個字符轉為一個整數記錄到buffer的元素中(buffer中的模型示例如下)
例如常數:111111111222222222333333333.444444444
intg = 27, frac = 9, len = 9, sign = false
byte 0 1 2 3 4 5 6 6 7 8
buffer: | 111111111 | 222222222 | 333333333 | 444444444 | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN |
低地址 -----------------------------------------------------------------------------------------------> 高地址
check_result_and_overflow 代碼實作:
void max_decimal(int precision, int frac, decimal_t *to) {
int intpart;
dec1 *buf = to->buf;
assert(precision && precision >= frac);
to->sign = false;
// 發生溢位時將 buffer 中的資料更新為 9 99 999 ...
if ((intpart = to->intg = (precision - frac))) {
int firstdigits = intpart % DIG_PER_DEC1;
if (firstdigits) *buf++ = powers10[firstdigits] - 1; /* get 9 99 999 ... */
for (intpart /= DIG_PER_DEC1; intpart; intpart--) *buf++ = DIG_MAX;
}
if ((to->frac = frac)) {
int lastdigits = frac % DIG_PER_DEC1;
for (frac /= DIG_PER_DEC1; frac; frac--) *buf++ = DIG_MAX;
if (lastdigits) *buf = frac_max[lastdigits - 1];
}
}
inline void max_my_decimal(my_decimal *to, int precision, int frac) {
assert((precision <= DECIMAL_MAX_PRECISION) && (frac <= DECIMAL_MAX_SCALE));
max_decimal(precision, frac, to);
}
inline void max_internal_decimal(my_decimal *to) {
max_my_decimal(to, DECIMAL_MAX_PRECISION, 0);
}
inline int check_result_and_overflow(uint mask, int result, my_decimal *val) {
// 檢查前面的處理是否發生溢位
if (val->check_result(mask, result) & E_DEC_OVERFLOW) {
bool sign = val->sign();
val->sanity_check();
max_internal_decimal(val);
val->sign(sign);
}
/*
Avoid returning negative zero, cfr. decimal_cmp()
For result == E_DEC_DIV_ZERO *val has not been assigned.
*/
if (result != E_DEC_DIV_ZERO && val->sign() && decimal_is_zero(val))
val->sign(false);
return result;
}
如果 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型別接收處理,
Enjoy GreatSQL ??
文章推薦:
有趣的SQL DIGEST
ulimits不生效導致資料庫啟動失敗和相關設定說明
MGR及GreatSQL資源匯總
GreatSQL MGR FAQ
在Linux下原始碼編譯安裝GreatSQL/MySQL
關于 GreatSQL
GreatSQL是由萬里資料庫維護的MySQL分支,專注于提升MGR可靠性及性能,支持InnoDB并行查詢特性,是適用于金融級應用的MySQL分支版本,
Gitee:
https://gitee.com/GreatSQL/GreatSQL
GitHub:
https://github.com/GreatSQL/GreatSQL
Bilibili:
https://space.bilibili.com/1363850082/favlist
技術交流群:
微信:掃碼添加
GreatSQL社區助手微信好友,發送驗證資訊加群,

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/502528.html
標籤:MySQL
上一篇:2022-08-19 PreparedStatement
下一篇:DECIMAL 資料處理原理淺析
