DECIMAL 數據處理原理淺析

来源:https://www.cnblogs.com/greatsql/archive/2022/08/23/16615339.html
-Advertisement-
Play Games

註:本文分析內容基於 MySQL 8.0 版本 文章開始前先複習一下官方文檔關於 DECIMAL 類型的一些介紹: The declaration syntax for a DECIMAL column is DECIMAL(M,D). The ranges of values for the ar ...


註:本文分析內容基於 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”.)

以上材料提到的最大精度和小數位是本文分析關註的重點:

  1. 最大精度是 65
  2. 小數位最多 30

接下來將先分析 MySQL 服務輸入處理 DECIMAL 類型的常數。

現在,先拋出幾個問題:

  1. MySQL 中當使用 SELECT 查詢常數時,例如:SELECT 123456789.123; 是如何處理的?
  2. 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 值為 10longlong_len 值為 19unsigned_longlong_len值為20

neg表示是否是負數,直接看正數的處理分支,負數同理:

  • 當輸入的數值串長度等於 10 時 MySQL 可能使用 LONG_NUMLONG_NUM 表示
  • 當輸入的數值串長度小於 19 時 MySQL 使用 LONG_NUM 表示
  • 當輸入的數值串長度等於 20 時 MySQL 可能使用 LONG_NUMDECIMAL_NUM 表示
  • 當輸入的數值串長度大於 20 時 MySQL 使用 DECIMAL_NUM 表示
  • 其他長度時,MySQL 可能使用 LONG_NUMULONGLONG_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)); 中欄位 dDECIMAL(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 位數字,所以最大可以處理的精度這裡為 81
  • DECIMAL_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_tint32_t 的別名
  • intg 表示整數部分的字元個數
  • frac 表示小數部分的字元個數
  • sign 表示是否負數
  • buf 指向 buffer
  • buffer 是數據存放數組,數組長度為9,也就意味著一個 decimal 最多可以存放 9int32_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;
}

解析過程大致如下:

  1. 分別計算整數部分和小數部分各有多少個字元
  2. 分別計算整數部分和小數部分各需要多少個 buffer 元素來存儲
    1. 如果整數部分需要的 buffer 元素個數超過 9,則表示溢出
    2. 如果整數部分和小數部分需要的 buffer 元素個數超過 9,則表示需要將小數部分進行截斷
      由於先解析整數部分,再解析小數部分,因此,如果整數部分如果完全占用所有 buffer 元素,此時,小數部分會被截斷。
  3. 將整數部分和小數部分按每 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 預設的最大精度數值,由上面的代碼片段可以看出最大精度數值是 659

超大常量數據生成的 DECIMAL 數據與 DECIMAL 欄位類型的區別

通過上面對超大常量數據生成的 DECIMAL 數據處理的分析,可以得出問題3的答案:兩者不同,區別如下:

  1. DECIMAL 欄位類型有顯式的精度和小數位的限制,也就是 DECIMAL 欄位插入數據時能插入的正數部分的長度為 M-D,而超大常量數據生成的 DECIMAL 數據則會隱含的優先處理考慮整數部分,整數部分處理完才繼續處理小數部分,如果緩衝區不夠則將小數位截斷,如果緩衝區不夠整數部分存放則轉為 659
  2. 在 MySQL 的服務源碼中 DECIMAL 欄位類型使用 Field_new_decimal 類型接收處理,而超大常量數據生成的 DECIMAL 數據由 Item_decimal 類型接收處理。

Enjoy GreatSQL

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一:背景 這篇我們來聊一下 PerfView 在協助 WinDbg 分析 Dump 過程中的兩個超實用技巧,可能會幫助我們快速定位最後的問題,主要有如下兩塊: 洞察記憶體泄漏中的靜態大集合變數名。 驗證當前程式的 GC 模式。 這裡就把經驗分享一下,希望讓大家少走彎路。 二:如何洞察 1. 查看靜態變 ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是IAR下調試信息輸出機制之半主機(Semihosting)。 在嵌入式世界里,輸出列印信息是一種非常常用的輔助調試手段,藉助列印信息,我們可以比較容易地定位和分析程式問題。在嵌入式應用設計里實現列印信息輸出的方式有很多,本系列將以 IA ...
  • 原文鏈接:https://www.zhoubotong.site/post/76.html 最近發現一個文檔類網站,編寫教程很合適,特地查了一下叫Read the Docs ,可以使用 Sphinx 生成文檔,GitHub 托管文檔,然後導入到 ReadtheDocs進行展示,這裡順便記錄一下搭建過 ...
  • 目錄 gun組織 項目 Linux版本 指令下達和執行 如何關機 linux Base gun組織 項目 copyleft:代表無版權。 copyright:代表有版權 opensource:開放源代碼、軟體誰都可以使用、誰都可以傳播、都可二次開發 free:免費 GPL:通用許可證協議,如果軟體打 ...
  • shell前言 什麼是shell shell-‘殼’ 命令解釋器,一種應用程式 shell語言特點 SHELL語言是指UNIX操作系統的命令語言,同時又是該命令語言的解釋程式的簡稱。 Shell本身是一個用C語言編寫的程式,它是用戶使用Unix/Linux的橋梁,用戶的大部分工作都是通過Shell完 ...
  • 操作系統的”進程”很早就出現了,許多教科書上定義這個概念總是晦澀難懂。電腦技術發展太快了,簡單的概念經過無數次演化,也會變得複雜。我們追溯一下操作系統的發展歷史,就能理解進程解決了什麼問題、為什麼這樣設計。進程是獨立功能的程式的一次動態執行過程,也是系統資源分配的獨立實體。每個進程都擁有獨立的地址... ...
  • 作者:Stephen Thorn 翻譯:劉玲玲 原文:https://www.percona.com/blog/2020/10/08/the-criticality-of-a-kubernetes-operator-for-databases/ 一些剛接觸 Kubernetes 的公司嘗試使用傳統環 ...
  • 資料庫如何在 Kubernetes 上運行?如果可以,哪些類型的資料庫和數據最適合使用 K8s?讓我們一起來看看。 Kubernetes 是用於自動部署、擴展和管理容器化應用程式的一個開源的容器編排解決方案。儘管 Kubernetes 最初是為無狀態應用程式設計的,但隨著有狀態工作負載的日益流行,K ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...