欧美vvv,亚洲第一成人在线,亚洲成人欧美日韩在线观看,日本猛少妇猛色XXXXX猛叫

新聞資訊

    要:查詢引擎會在所有可行的查詢訪問路徑中選擇執行代價最低的一條。

    本文分享自華為云社區《一文讀懂簡單查詢代價估算【這次高斯不是數學家】-云社區-華為云》,作者: leapdb。

    查詢代價估算——如何選擇一條最優的執行路徑

    SQL生命周期:詞法分析(Lex) -> 語法分析(YACC) -> 分析重寫 -> 查詢優化(邏輯優化和物理優化) -> 查詢計劃生成 -> 查詢執行。

    1. 詞法分析:描述詞法分析器的*.l文件經Lex工具編譯生成lex.yy.c, 再由C編譯器生成可執行的詞法分析器。基本功能就是將一堆字符串根據設定的保留關鍵字和非保留關鍵字,轉化成相應的標識符(Tokens)。
    2. 語法分析:語法規則描述文件*.y經YACC工具編譯生成gram.c,再由C編譯器生成可執行的語法分析器。基本功能就是將一堆標識符(Tokens)根據設定的語法規則,轉化成原始語法樹。
    3. 分析重寫:查詢分析將原始語法樹轉換為查詢語法樹(各種transform);查詢重寫根據pg_rewrite中的規則改寫表和視圖,最終得到查詢語法樹。
    4. 查詢優化:經過邏輯優化和物理優化(生成最優路徑)。
    5. 查詢計劃生成:將最優的查詢路徑轉化為查詢計劃。
    6. 查詢執行:通過執行器去執行查詢計劃生成查詢的結果集。

    在物理優化階段,同樣的一條SQL語句可以產生很多種查詢路徑,例如:多表JOIN操作,不同的JOIN順序產生不同的執行路徑,也導致中間結果元組規模的不同。查詢引擎會在所有可行的查詢訪問路徑中選擇執行代價最低的一條。

    通常我們依據 COST = COST(CPU) + COST(IO) 這一公式來選擇最優執行計劃。這里最主要的問題是如何確定滿足某個條件的元組數量,基本方法就是依據統計信息和一定的統計模型。某條件的查詢代價 = tuple_num * per_tuple_cost。

    統計信息主要是pg_class中的relpages和reltuples,以及pg_statistics中的distinct, nullfrac, mcv, histgram等。

    為何需要這些統計信息,有了這些夠不夠?

    統計信息的收集來自analyze命令通過random方式隨機采集的部分樣本數據。我們將其轉化為一個數學問題就是:給定一個常量數組可以分析出來哪些數據特征?找規律哈。

    用最樸素的數學知識能想到的是:

    1. 是不是空數組
    2. 是不是常量數組
    3. 是不是unique無重復
    4. 是不是有序
    5. 是不是單調的,等差,等比
    6. 出現次數最多的數字有哪些
    7. 數據的分布規律(打點方式描繪數據增長趨勢)

    還有嗎?這是一個值得不斷思考的問題。

    如何依據統計信息估算查詢代價

    統計信息是不準確的,不可靠的,兩個原因:

    1. 統計信息只來自部分采樣數據,不能精準描述全局特征。
    2. 統計信息是歷史數據,實際數據隨時可能在變化。

    如何設計一個數學模型,利用不可靠的統計信息,盡可能準確的估算查詢代價,是一個很有意思的事情。接下來我們通過一個個的具體例子來介紹。

    主要內容來自以下鏈接,我們主要對其進行詳細的解讀。

    https://www.postgresql.org/docs/14/planner-stats-details.html

    https://www.postgresql.org/docs/14/using-explain.html

    最簡單的表的行數估算(使用relpages和reltuples)

    SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1'; ---采樣估算有358個頁面,10000條元組
     relpages | reltuples
    ----------+-----------
          358 |     10000
    
    EXPLAIN SELECT * FROM tenk1; ---查詢估算該表有10000條元組
                             QUERY PLAN
    -------------------------------------------------------------
     Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)

    【思考】查詢計劃中估算的rows=10000就直接是pg_class->reltuples嗎?

    統計信息是歷史數據,表的元組數在隨時變化。例如:analyze數據采樣時有10個頁面,存在50條元組;實際執行時有20個頁面,可能存在多少條元組?用你最樸素的情感想一想是不是,很可能是100條元組對不對?

    表大小的估算方法在函數 estimate_rel_size -> table_relation_estimate_size -> heapam_estimate_rel_size 中。

    1. 先通過表物理文件的大小計算實際頁面數actual_pages = file_size / BLOCKSIZE
    2. 計算頁面的元組密度
    3. a. 如果相關頁大于0,密度 = reltuples / (雙)重頁
    4. b. 如果relpages為空,density = (BLCKSZ - SizeOfPageHeaderData) / tuple_width, 頁面大小 / 元組寬度。
    5. 估算表元組個數 = 頁面元組密度 * 實際頁面數 actual_pages =

    所以,估算rows的10000 = (10000 / 358) * 358,歷史頁面密度 * 新的頁面數,以推算當前元組數。

    最簡單的范圍比較(使用直方圖)

    SELECT histogram_bounds FROM pg_stats WHERE tablename='tenk1' AND attname='unique1';
                       histogram_bounds
    ------------------------------------------------------
     {0,993,1997,3050,4040,5036,5957,7057,8029,9016,9995}
    
     EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000; ---估算小于1000的有1007條
                                       QUERY PLAN
    --------------------------------------------------------------------------------
     Bitmap Heap Scan on tenk1  (cost=24.06..394.64 rows=1007 width=244)
       Recheck Cond: (unique1 < 1000)
       ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..23.80 rows=1007 width=0)
             Index Cond: (unique1 < 1000)

    查詢引擎在查詢語法樹的WHERE子句中識別出比較條件,再到pg_operator中根據操作符和數據類型找到oprrest為scalarltsel,這是通用的標量數據類型的小于操作符的代價估算函數。最終是在 scalarineqsel -> ineq_histogram_selectivity 中進行直方圖代價估算。

    在PG中采用的是等高直方圖,也叫等頻直方圖。將樣本范圍劃分成N等份的若干個子區間,取所有子區間的邊界值,構成直方圖。

    使用:使用時認為子區間(也叫桶)內的值是線性單調分布的,也認為直方圖的覆蓋范圍就是整個數據列的范圍。因此,只需計算出在直方圖中的占比,既是總體中的占比。

    【思考】以上的兩點假設靠譜嗎?有沒有更合理的辦法?

    選擇率 = (前面桶的總數 + 目標值在當前桶中的范圍 / 當前桶的區間范圍) / 桶的總數
    selectivity = (1 + (1000 - bucket[2].min) / (bucket[2].max - bucket[2].min)) / num_buckets
                = (1 + (1000 - 993) / (1997 - 993)) / 10
                = 0.100697
    估算元組數 = 基表元組數 * 條件選擇率
    rows = rel_cardinality * selectivity
         = 10000 * 0.100697
         = 1007  (rounding off)

    最簡單的等值比較(使用MCV)

    SELECT null_frac, n_distinct, most_common_vals, most_common_freqs FROM pg_stats WHERE tablename='tenk1' AND attname='stringu1';
    null_frac         | 0
    n_distinct        | 676
    most_common_vals  | {EJAAAA,BBAAAA,CRAAAA,FCAAAA,FEAAAA,GSAAAA,?JOAAAA,MCAAAA,NAAAAA,WGAAAA}
    most_common_freqs | {0.00333333,0.003,0.003,0.003,0.003,0.003,?0.003,0.003,0.003,0.003}
    
    EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'CRAAAA'; ---
                            QUERY PLAN
    ----------------------------------------------------------
     Seq Scan on tenk1  (cost=0.00..483.00 rows=30 width=244)
       Filter: (stringu1 = 'CRAAAA'::name)

    查詢引擎在查詢語法樹的WHERE子句中識別出比較條件,再到pg_operator中根據操作符和數據類型找到oprrest為eqsel,這是通用的等值比較操作符的代價估算函數。最終是在 eqsel_internal -> var_eq_const 中進行MCV代價估算。

    MCV是在樣本中選取重復次數最多的前100個組成,并計算每個值在樣本中的占比。使用時,就簡單的認為這個占比就是在全局中的占比。

    【思考】將樣本中占比就這樣簡單的認為是在全局中的占比,這樣合理嗎?有沒有更好的辦法?

    CRAAAA位于MCV中的第3項,占比是0.003
    selectivity = mcf[3]
                = 0.003
    rows = 10000 * 0.003
         = 30

    接下來,我們在看一個不在MCV中的等值比較。

    EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'xxx'; ---查找不存在于MCV中的值
                            QUERY PLAN
    ----------------------------------------------------------
     Seq Scan on tenk1  (cost=0.00..483.00 rows=15 width=244)
       Filter: (stringu1 = 'xxx'::name)

    通常MCV用于等值比較,直方圖用于范圍比較。“不存在于MCV中的值”,認為它們共享“不存在于MCV中的概率”,即:選擇率 = (1 - MCV概率總和) / (不在MCV中的值的distinct個數)。

    【思考】這樣判斷不在MCV中的方法合理嗎?有沒有更好的方法?

    selectivity = (1 - sum(mvf)) / (num_distinct - num_mcv)
                = (1 - (0.00333333 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003 +
                        0.003 + 0.003 + 0.003 + 0.003)) / (676 - 10)
                = 0.0014559
    rows = 10000 * 0.0014559
         = 15  (rounding off)

    復雜一點的范圍比較(同時使用MCV和直方圖)

    前面 unique < 1000 的例子,在 scalarineqsel 函數中只利用到了直方圖,是因為unique列沒有重復值,也就不存在MCV。下面我們用一個不是unique的普通列再看一下范圍比較。

    這個范圍包括兩部分,重復次數比較多的值(在MCV中) 和 重復次數比較少的值(覆蓋在直方圖里),又由于計算直方圖時去掉了MCV的值,因此MCV和直方圖互相獨立可以聯合使用。

    SELECT null_frac, n_distinct, most_common_vals, most_common_freqs FROM pg_stats WHERE tablename='tenk1' AND attname='stringu1';
    null_frac         | 0
    n_distinct        | 676
    most_common_vals  | {EJAAAA,BBAAAA,CRAAAA,FCAAAA,FEAAAA,GSAAAA,?JOAAAA,MCAAAA,NAAAAA,WGAAAA}
    most_common_freqs | {0.00333333,0.003,0.003,0.003,0.003,0.003,?0.003,0.003,0.003,0.003}
    
    SELECT histogram_bounds FROM pg_stats WHERE tablename='tenk1' AND attname='stringu1';
                                    histogram_bounds
    -------------------------------------------------------------------?-------------
     {AAAAAA,CQAAAA,FRAAAA,IBAAAA,KRAAAA,NFAAAA,PSAAAA,SGAAAA,VAAAAA,?XLAAAA,ZZAAAA}
    
    EXPLAIN SELECT * FROM tenk1 WHERE stringu1 < 'IAAAAA'; ---查找一個不存在于MCV中的值
                             QUERY PLAN
    ------------------------------------------------------------
     Seq Scan on tenk1  (cost=0.00..483.00 rows=3077 width=244)
       Filter: (stringu1 < 'IAAAAA'::name)

    小于IAAAAA的值在MCV中有前6個,因此把它們的frac累加起來,就是小于IAAAAA且重復次數較多的人的概率

    selectivity = sum(relevant mvfs)
                = 0.00333333 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003
                = 0.01833333

    還有一部分小于IAAAAA但重復次數較少的人的概率 可以通過直方圖進行范圍計算。前面使用unique1列進行等值比較時,因為unique約束列不存在MCV,只有直方圖。因此,只計算在直方圖中桶的覆蓋占比就是選擇率了。這里還要考慮落在 直方圖中值的整體占比 histogram_fraction = 1 - sum(mcv_frac),直方圖桶的覆蓋占比 * 整個直方圖整體占比就是在直方圖中的選擇率了。

    selectivity = mcv_selectivity + histogram_selectivity * histogram_fraction
                = 0.01833333 + ((2 + ('IAAAAA'-'FRAAAA')/('IBAAAA'-'FRAAAA')) / 10) * (1 - sum(mvfs))
                = 0.01833333 + 0.298387 * 0.96966667
                = 0.307669
    
    rows        = 10000 * 0.307669
                = 3077  (rounding off)

    【思考】在這個特殊的例子中,從MCV中計算出來的選擇率為0.01833333,遠小于從直方圖中計算出來的選擇率0.28933593,是因為該列中數值分布的太平緩了(統計信息顯示MCV中的這些值出現的頻率比其他值要高,這可能是因為采樣導致的錯誤)。在大多數存在明顯重復值多的場景下,從MCV中計算出的選擇率會比較明顯,因為重復值的出現概率是比較準確的。

    多條件聯合查詢的例子

    EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000 AND stringu1 = 'xxx';
                                       QUERY PLAN
    -------------------------------------------------------------------?-------------
     Bitmap Heap Scan on tenk1  (cost=23.80..396.91 rows=1 width=244)
       Recheck Cond: (unique1 < 1000)
       Filter: (stringu1 = 'xxx'::name)
       ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..23.80 rows=1007 width=0)
             Index Cond: (unique1 < 1000)

    多條件的選擇率計算其實也很簡單,就按條件本身的邏輯運算來。

    兩個條件是與的關系,屬于互相獨立事件,就相乘就可以了。

    selectivity = selectivity(unique1 < 1000) * selectivity(stringu1 = 'xxx')
                = 0.100697 * 0.0014559
                = 0.0001466
    
    rows        = 10000 * 0.0001466
                = 1  (rounding off)

    一個使用JOIN的例子

    EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2
    WHERE t1.unique1 < 50 AND t1.unique2 = t2.unique2;
                                          QUERY PLAN
    -------------------------------------------------------------------?-------------------
     Nested Loop  (cost=4.64..456.23 rows=50 width=488)
       ->  Bitmap Heap Scan on tenk1 t1  (cost=4.64..142.17 rows=50 width=244)
             Recheck Cond: (unique1 < 50)
             ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.63 rows=50 width=0)
                   Index Cond: (unique1 < 50)
       ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.00..6.27 rows=1 width=244)
             Index Cond: (unique2 = t1.unique2)

    unique1 < 50這個約束在nested-loop join之前被執行,還是使用直方圖計算,類似前面的簡單范圍查找的例子,只不過這次50落在第一個桶中。

    selectivity = (0 + (50 - bucket[1].min)/(bucket[1].max - bucket[1].min))/num_buckets
                = (0 + (50 - 0)/(993 - 0))/10
                = 0.005035
    
    rows        = 10000 * 0.005035
                = 50  (rounding off)

    JOIN約束t1.unique2 = t2.unique2的選擇率估算,還是先到pg_operator中查找“等于”操作符的oprjoin,是eqjoinsel。JOIN的話,兩邊的統計信息都要參考

    SELECT tablename, null_frac,n_distinct, most_common_vals FROM pg_stats
    WHERE tablename IN ('tenk1', 'tenk2') AND attname='unique2';
    
    tablename  | null_frac | n_distinct | most_common_vals
    -----------+-----------+------------+------------------
     tenk1     |         0 |         -1 |
     tenk2     |         0 |         -1 |

    對于unique列沒有MCV,所以這里我們只能依賴distinct和nullfrac來計算選擇率。

    【*】選擇率 = 兩邊的非空概率相乘,再除以最大JOIN條數。
    selectivity = (1 - null_frac1) * (1 - null_frac2) * min(1/num_distinct1, 1/num_distinct2)
                = (1 - 0) * (1 - 0) / max(10000, 10000)
                = 0.0001
    【*】JOIN的函數估算就是兩邊輸入數量的笛卡爾積,再乘以選擇率
    rows = (outer_cardinality * inner_cardinality) * selectivity
         = (50 * 10000) * 0.0001
         = 50

    如果存在MCV,則分成兩部分計算選擇率:在MCV中的部分和不在MCV中的部分。

    更詳細的信息

    表大小的估算:src/后端/optimizer/util/plancat.c

    一般邏輯子句的選擇率估算:src/backend/optimizer/path/clausesel.c

    操作符函數的選擇率估算:src/backend/utils/adt/selfuncs.c.

    點擊下方,第一時間了解華為云新鮮技術~

    華為云博客_大數據博客_AI博客_云計算博客_開發者中心-華為云

    undo log的組織形式

    此部分是關于Undo log的組織形式的一個介紹;主要分為兩部分來對undo log的組織形式進行介紹:文件結構和內存結構。在介紹這兩部分時,先從局部出發,最后再給出各個部分的聯系。

    1. 文件結構

    首先,在MySQL5.6之前所有的undo log全部存儲在系統表空間中(ibdata1);但是從5.6開始也可以使用獨立表空間來存儲undo log。

    當前版本InnoDB默認有兩個undo tablespace,也可以使用CREATE UNDO TABLESPACE語句動態添加,最大128個;每個undo tablespace至多可以有TRX_SYS_N_RSEGS(128)個回滾段。

    1.1 Rollback Segment(回滾段)

    InnoDB在undo tablespace中使用回滾段來組織undo log。同時為了保證事務的并發操作,在寫undo log時不產生沖突,InnoDB使用 回滾段 來維護undo log的并發寫入和持久化;而每個回滾段 又有多個undo log slot。通常通過Rollback Segment Header來管理回滾段,Rollback Segment Header通常在回滾段的第一個頁,具體結構如下:

    • Max Size:參數名為 TRX_RSEG_MAX_SIZE,回滾段可以有用的最大page數。
    • History Size:參數名為TRX_RSEG_HISTORY_SIZE,history list包含的page數。
    • History List Base Node:參數名為TRX_RSEG_HISTORY,history list的Base Node。
    • Rollback Segment FSEG Entry:參數名為TRX_RSEG_FSEG_HEADER,file segment的存放位置。
    • Undo Slots Dictionary:參數名為TRX_RSEG_UNDO_SLOTS,存放活躍事務的undo header page no。

    Rollback Segment Header里面最重要的兩部分就是history listundo slot directory

    其中history list把所有已經提交但還沒有被purge事務的undo log串聯起來,purge線程可以通過此list對沒有事務使用的undo log進行purge。

    每個事務在需要記錄undo log時都會申請一個或兩個slot(insert/update分開),同時把事務的第一個undo page放入對應slot中;所以理論上InnoDB允許的最大事務并發數為128(undo tablespace) * 128(Rollback Segment) * 1024(TRX_RSEG_UNDO_SLOTS)。下面我們進一步介紹undo log在磁盤上如何記錄。

    1.2 UndoPage

    要想知道undo log如何記錄,我們先要搞清楚一個undo page具體內容,undo page一般分兩種情況:header page和normal page 。

    header page除了normal page所包含的信息,還包含一些undo segment信息,后面會對undo segment進行詳細介紹。

    我們下面先介紹一下undo header page的詳細分布。

    undo header page是事務需要寫undo log時申請的第一個undo page;一個undo header page他同一時刻只隸屬于同一個活躍事務,但是一個undo header page上面的內容可能包含多個已經提交的事務和一個活躍事務。

    undo normal page是當活躍事務產生的undo record超過undo header page容量后,單獨再為此事務分配的undo page(參考函數trx_undo_add_page);此page只隸屬于一個事務,只包含undo page header不包含undo segment header。

    1.3 Undo Page Header

    每一個undo page都要有header,其中記錄了當前undo page的一些狀態信息,具體內容如下:

    • Undo Page Type:參數名為TRX_UNDO_PAGE_TYPE,使用該page事務的類型,包含TRX_UNDO_INSERT,TRX_UNDO_UPDATE兩種。
    • Latest Log Record Offset:參數名為TRX_UNDO_PAGE_START,最新事務開始記錄undo log起始位置。
    • Free Space Offset:參數名為TRX_UNDO_PAGE_FREE,頁內空閑空間起始地址,在此之后可記錄undo log。
    • Undo Page List Node:參數名為TRX_UNDO_PAGE_NODE,undo page list節點,可以把同一個事務所用到的所有undo page雙向串聯起來。

    1.4 Undo Segment Header

    • State:參數名為TRX_UNDO_STATE,undo segment的狀態,TRX_UNDO_ACTIVE等
    • Last Log Offset:參數名為TRX_UNDO_LAST_LOG,當前page最后一個undo log header的位置。
    • Undo Segment FSEG Entry:參數名為TRX_UNDO_FSEG_HEADER,segment對應的inode的(space_id,page_no,offset等)
    • Undo Segment Page List Base Node:參數名為TRX_UNDO_PAGE_LIST,undo page list的Base Node,對于同一個事務下的undo header page和undo normal page構成雙向鏈表。

    上面只是介紹了一些undo log在文件上的基本結構,下面我們繼續介紹記錄undo log時的文件組織。

    1.5 Undo Log Header

    當事務開始記錄undo log時,先創建一個undo log header,當update/delete事務結束后,undo log header將會被加入到hisotry list中;insert事務的undo log會被立即釋放。

    • Transaction ID:參數名為TRX_UNDO_TRX_ID,事務id(事務開始的邏輯時間)
    • Transaction Number:參數名為TRX_UNDO_TRX_NO,事務no(事務結束的邏輯時間)
    • Delete Marks Flags:參數名為TRX_UNDO_DEL_MARKS,如果涉及到刪除記錄為TRUE
    • Log Start Offset:參數名為TRX_UNDO_LOG_START,事務中第一個undo record地址。
    • XID Flag:參數名為TRX_UNDO_XID_EXISTS,用于XID。
    • DDL Transaction Flag:參數名為TRX_UNDO_DICT_TRANS,是否是DDL事務
    • Table ID if DDL Transaction:參數名為TRX_UNDO_TABLE_ID,如果是DDL事務,記錄table id。
    • Next Undo Log Offset:參數名為TRX_UNDO_NEXT_LOG,當前頁的下一個undo log header位置
    • Prev Undo Log Offset:參數名為TRX_UNDO_PREV_LOG,當前頁的上一個undo log header位置
    • History List Node:參數名為TRX_UNDO_HISTORY_NODE,事務結束時放入history list的節點。

    有關XID的內容暫時不介紹。

    有了undo log header后,我們就可以記錄undo record了。

    1.6 Undo Record

    • Previous Record Offset:存儲上一條record的位置。
    • Next Record Offset:存儲下一條record的位置。
    • Type + Extern Flag + Compilation Info:存undo record的type等信息。

    可以從上面圖中看出,undo record除了存儲這里比較重要的幾個信息,包含前后undo record位置,類型,undo no,table id等;而undo recod中具體存儲的內容我們等到《undo log的分配與記錄》中去介紹。

    最后,我們通過下面這幅圖來了解undo log在文件組織上的一個總覽。

    2. 主要內存數據結構

    為了方便管理和記錄undo log,在內存中有如下關鍵結構體對象:

    • undo::Tablespace:undo tablespace內存結構體,維護undo tablespace相關信息,管理此tablespace中相關回滾段。
    • trx_rseg_t:回滾段的內存結構體,用于維護回滾段相關信息。
    • trx_undo_t:undo log內存結構體,用于維護undo log type等信息,便于對undo page進行維護和管理。
    • purge_pq_t:purge queue,對已經提交事務的undo log進行維護和回收。
    • trx_t:事務的內存結構體,對事務的信息進行管理和維護。

    我們重點對trx_rseg_t結構體的內容進行介紹。

    trx_rseg_t主要數據成員:

    • last_page_no:history list上此rseg最后一個沒有被purge的page no。
    • last offset:最后一個未被purge的undo log header 偏移。
    • last trx no:最后一個未被purge的事務no。
    • last_del_marks:最后一個未被purge的日志需要被清理。

    上面四個數據可以從trx_undo_t中獲取,參考trx_purge_add_update_undo_to_history函數。

    • trx_ref_count:被活躍事務引用的計數器;非0時,此回滾段所在的tablespace不可以被truncate。
    • update_undo_list:所有活躍的update事務的trx_undo_t對象存儲在此鏈表。
    • update_undo_cached:如果update事務提交時,此事務只使用了一個page并且此page剩余空間大于1/4放入此鏈表;新update事務新申請undo log時優先從此鏈表分配。
    • insert_undo_list:所有活躍的insert事務的trx_undo_t對象存儲在此鏈表。
    • insert_undo_cached:如果insert事務提交時,此事務只使用了一個page并且此page剩余空間大于1/4放入此鏈表;新insert事務新申請undo log時優先從此鏈表分配。

    下面我們通過一副關系圖來介紹內存中各個關鍵結構體之間的關系;其中實線代表擁有該對象,虛線代表引用該對象。

    undo log的分配與記錄

    我們通過之前的介紹了解到;undo log在磁盤和內存中是如何組織的;從中了解到,回滾段不論在磁盤和內存中,都是一個非常關鍵的結構體;InnoDB存儲引擎通過回滾段來對undo log進行組織和管理,所以首先我們需要弄清楚回滾段是如何分配與使用的,之后再闡述undo log具體是如何記錄的。

    1. 分配回滾段

    當開啟一個事務的時候,需要預先為事務分配一個回滾段。

    首先我們將事務分為兩大類:只讀事務與讀寫事務。分別從這兩大類事務來探討如何分配回滾段的。

    只讀事務:當事務涉及到對臨時表的讀寫時,我們需要為其分配一個回滾段對其產生的undo log record進行記錄,具體調用鏈路如下:

    trx_assign_rseg_temp() -> get_next_temp_rseg() -> (trx_sys->tmp_rsegs)

    trx_sys->tmp_rsegs 對應的臨時文件為ibtmp1,一般來說有128個回滾段。

    讀寫事務:當一個事務被判定為讀寫模式時,會為其分配trx_id以及回滾段,具體調用鏈路如下

    trx_assign_rseg_durable() -> get_next_redo_rseg()                                                  |                                                  ->get_next_redo_rseg_from_trx_sys() -> (trx_sys->rsegs)                                                  |                                                  ->get_next_redo_rseg_from_undo_spaces() -> (undo_space->rsegs())

    當InnoDB沒有配置獨立undo表空間時,從trx_sys->rsegs為讀寫事務分配回滾段;否則則從 undo_spaces->rsegs()為其分配回滾段;InnoDB從MySQL 8.0.3開始,獨立表空間個數默認值從0改為2。

    trx_sys->rsegs 對應的文件為ibdata1,默認有128個回滾段。

    undo_space->rsegs() 對應的文件為undo_001,undo_002...,最多可有128個undo文件,每個文件默認128個回滾段。

    具體從rsegs中分配時采用round-robin方式進行分配。

    2. 使用回滾段

    當發生數據變更時,我們需要使用undo log記錄下變更前的數據記錄;因此需要從回滾段分配中來分配一個undo slot來供事務記錄undo。

    記錄undo的入口函數為trx_undo_report_row_operation,其大致流程如下:

    1. 判斷操作的表是否為臨時表;如果是臨時表,為其分配臨時表回滾段,否則使用普通回滾段。
    2. 根據事務類型,通過trx_undo_assign_undo為其分配trx_undo_t對象;之后事務產生的undo記錄在此對象中。
    3. 根據事務類型,通過trx_undo_page_report_insert/modify,來記錄insert/update事務產生的undo。

    接著來看一下 trx_undo_assign_undo 函數流程:

    1. 首先嘗試通過trx_undo_reuse_cached() 來獲取可用的undo log對象。
    2. 對于INSERT類型的undo log,我們從rseg->insert_undo_cached鏈表上獲取undo log對象,并將其從鏈表上移除;之后通過trx_undo_insert_header_reuse()重新初始化undo page頭部信息。
    3. 對于UPDATE/DELETE類型undo log,從rseg->update_undo_cached鏈表上獲取undo log對象,并將其從鏈表上移除;然后通過trx_undo_header_create()創建新的undo log header。
    4. 然后使用trx_undo_header_add_space_for_xid() 作用于上述undo log對象,預留XID存儲空間。
    5. 最后使用trx_undo_mem_init_for_reuse()初始化undo log對象相關信息。
    6. 如果沒有緩存的undo log對象,我們就需要使用trx_undo_create()從回滾段上分配一個空閑的undo slot,并分配一個undo page,對其初始化。
    7. 將已經分配好的undo log對象放入相關的鏈表中(rseg->insert_undo_list或rseg->update_undo_list)。
    8. 最后,如果這個事務時DDL操作,需要將undo_hdr_page(事務記錄undo log的第一個page)中的TRX_UNDO_DICT_TRANS置為TRUE.

    undo header page結構參考之前的《undo log的組織形式》的內容

    undo log最小的并發單元為undo slot,所以undo log支持最大的并發事務為:undo tablespace 數 * 回滾段數 * undo slot數。

    3. undo log寫入

    當分配完undo slot,初始化完undo log對象后,我們就可以記錄真正的undo log record;undo log record也分為一下兩種,insert undo log record與update undo record。

    當數據庫需要修改某個數據記錄時,都會寫入一條update undo log record;當插入一條數據記錄時,會寫入一條insert undo log record。

    對于insert undo log寫入的入口函數為trx_undo_page_report_insert()

    • Prev record offset (2):本條record開始的位置。
    • Next record offset (2):下一條record開始的位置。
    • Type (1):標記undo log record的類型,此處一般為 TRX_UNDO_INSERT_REC.
    • Undo Number (1-11):trx->undo_no,事務的第幾條undo。
    • Table ID (1-11):聚集索引所對應的table id。
    • Unique Fields:唯一鍵值

    對于update undo log寫入的入口函數為trx_undo_page_report_modify()

    • Prev record offset (2):同上
    • Next record offset (2):同上
    • Type+Extern Flag+Comp Info (1)
    • Type為undo log rec的類型,此處一般有三種:
    • TRX_UNDO_DEL_MARK_REC: 標記刪除操作,未修改任何列值;可能由普通刪除操作產生,也有可能由修改聚集索引產生,因為修改聚集索引操作被分拆為刪除+插入操作。
    • TRX_UNDO_UPD_DEL_REC: 更新一個已經被刪除的記錄;如某個記錄被刪除后,在很快插入一個相同的記錄;之前的記錄若未被purge,就可能重用該記錄所在位置。
    • TRX_UNDO_UPD_EXIST_REC: 更新一個未被標記刪除的記錄,也就是普通更新。
    • Extern Flag:是否有外部存儲列,以提示purge線程去清理外部存儲。
    • Comp Info:更新相關信息,例如更新是否導致索引序發生變化。
    • Undo Number (1-11):同上
    • Table ID (1-11):同上
    • Info Bits (1):是否標記刪除REC_INFO_DELETED_FLAG.
    • Data Trx ID (1-11):修改舊記錄的事務ID。
    • Data Roll Ptr (1-11):舊記錄的回滾指針。
    • Unique Fields:唯一鍵值
    • Update Get N Fields (1-5): 更新的列數。
    • UPD Old Columns:發生更新時,舊記錄的內容。
    • Delete Fileds len (2): 刪除的列數。
    • DEL Old Columns:發生刪除時,舊記錄的內容;發生刪除時并不總是記錄舊記錄,只有ord_part=1也就是說此字段為n_uniq之一的字段,才會記錄舊字段。

    在寫入過程中,可能出現undo page空間不足的情況;當出現這種情況,我們需要通過trx_undo_erase_page_end()來清除剛剛寫入的區域,然后通過trx_undo_add_page()申請一個新的undo page加入到undo page list,同時將undo->last_page_no指向新的undo page,最后重試寫入。

    完成undo log record的寫入后,通過trx_undo_build_roll_ptr()構建新的回滾指針返回;通過回滾指針我們可以找到相關記錄的undo log record,從而構建出舊版本的數據;回滾指針將會記錄在聚集索引記錄中。

    undo log的應用

    我們通過之前的介紹已經了解到, undo log的組織方式與分配記錄;那么后面我們繼續介紹undo log主要的應用是什么。

    undo log的應用主要有兩方面:

    1. 事務回滾,崩潰恢復;此功能主要滿足了事務的原子性,簡單的說就是要么做完,要么不做。因為數據庫在任何時候都可能發生宕機;包括停電,軟硬件bug等。那數據庫就需要保證不管發生任何情況,在重啟數據庫時都能恢復到一個一致性的狀態;這個一致性的狀態是指此時所有事務要么處于提交,要么處于未開始的狀態,不應該有事務處于執行了一半的狀態;所以我們可以通過undo log在數據庫重啟時把正在提交的事務完成提交,活躍的事務回滾,這樣就保證了事務的原子性,以此來讓數據庫恢復到一個一致性的狀態。
    2. 多版本并發控制(MVCC),此功能主要滿足了事務的隔離性,簡單的說就是不同活躍事務的數據互相可能是不可見的。因為如果兩個活躍的事務彼此可見,那么一個事務將會看到另一個事務正在修改的數據,這樣會發生數據錯亂;所以我們可以借助undo log記錄的歷史版本數據,來恢復出對于一個事務可見的數據,來滿足其讀取數據的請求。

    我們接下來就詳細介紹上面兩個功能undo log是如何實現的。

    1. 崩潰恢復

    在InnoDB因為某些原因停止運行后;重啟InnoDB時,可能存在一個不一致的狀態,這個時候我們就需要把MySQL恢復到一個一致的狀態來保證數據庫的可用性。這個恢復過程主要分下面這么幾步:

    1. 把最新的undo log從redo log中恢復出來,因為undo log是受redo log保護的。
    2. 根據最新的undo log構建出InnoDB崩潰前的狀態。
    3. 回滾那些還沒有提交的事務。

    經過上面這三步后,InnoDB就可以恢復到一個一致的狀態,并且對外提供服務。

    下面我們詳細的來介紹這三部分的具體過程:

    1.1 undo log的恢復

    因為undo log受到redo log的保護,所以我們只需要根據最新的redo log就可以把undo log恢復到最新的狀態;具體的調用過程如下:

    recv_recovery_from_checkpoint_start()// 從最新的一個log checkpoint開始讀取redo log并應用。 | -> recv_recovery_begin() // 將redo log讀取到log buffer中,并將其parse到redo hash中 | -> recv_scan_log_recs() // 掃描 log buffer中的redo log,并將redo hash中的redo log應用 | -> recv_apply_hashed_log_recs() // 應用redo log到其對應的page上。 | ->recv_apply_log_rec()->recv_recover_page()->recv_parse_or_apply_log_rec_body() -> MLOG_UNDO_INSERT…

    經過上述的流程之后,undo log就可以恢復到InnoDB崩潰前的最新的狀態;雖然undo log已經恢復到最新的狀態,但是InnoDB還沒有恢復到崩潰前的最新狀態;所以下一步我們就需要根據最新的undo log把InnoDB崩潰前的內存結構都恢復出來。

    1.2 構建InnoDB崩潰前的狀態

    構建InnoDB崩潰前的狀態,主要是恢復崩潰前最新事務系統的狀態;通過該狀態我們可以知道那些事務已經提交,那些事務還未提交,以及那些事務還未開始。

    我們從前面兩章的介紹,回滾段不管在內存中還是在文件中都是組織undo log的重要數據結構;所以我們首先需要把回滾段的內存結構恢復出來,然后根據內存中的回滾段,把活躍的事務恢復出來。其具體過程在函數trx_sys_init_at_db_start()中實現,其大致步驟如下:

    1. 通過trx_rsegs_init()掃描文件中的回滾段結構,來把rseg的內存結構恢復出來。
    2. 通過trx_rseg_mem_create()把last_page_no,last_offset,last_trx_no,last_del_marks從文件中讀取上來。
    3. 然后通過trx_undo_lists_init()把rseg的四個鏈表:insert_undo_list,insert_undo_cached,update_undo_list,update_undo_cached從磁盤上恢復出來。
    4. 在rseg內存結構恢復好之后,我們再通過trx_lists_init_at_db_start()把活躍的事務從rseg中恢復出來。
    5. 通過trx_resurrect_insert()恢復活躍的插入類型的事務。
    6. 通過trx_resurrect_update()恢復活躍的更新類型的事務。

    至此,我們就已經把InnoDB崩潰前的內存和文件狀態都已經恢復出來了;其實這個時候InnoDB已經可以對外提供服務了,(畢竟內存和文件狀態都就緒后我們也就可以保持一致性了);那么最后一步的事務回滾就可以交給后臺線程來慢慢做事務回滾,不影響主線程對外提供服務了。

    1.3 事務回滾

    事務需要回滾主要有兩種情況:

    1. 事務發生異常:如發生在崩潰恢復時;其活躍事務雖然被恢復出來,但是無法繼續,需要將其回滾。
    2. 事務被顯式回滾:如用戶打開一個事務,執行完某些操作后需要將其回滾。

    那么在回滾時,我們就需要借助undo log中的舊數據來把事務恢復到之前的狀態;其入口函數為row_undo_step();

    其操作就是通過undo log來讀取舊的數據記錄,然后做逆向操作;主要分為下面這么幾類:

    1. 對于標記刪除的記錄清理刪除標記。
    2. 對于in-place更新,將數據更新為老版本。
    3. 對于插入操作,刪除聚集索引記錄和二級索引記錄。
    4. 先通過row_undo_ins_remove_sec_rec()刪除二級索引記錄。
    5. 再通過row_undo_ins_remove_clust_rec()刪除聚集索引記錄。

    2. 多版本并發控制(MVCC)

    多版本并發控制簡單的說就是當前事務只能看見已經提交的數據記錄,看不到正在修改的數據記錄。所以我們只要弄清楚那些事務對于當前事務是已經提交的,那些事務對于當前事務是活躍的。

    為了實現上述的功能我們先介紹幾個比較關鍵的概念:

    trx::id:事務開始的邏輯時間,也叫事務ID,在事務開始時通過trx_start_low()分配。

    trx::no:事務結束的邏輯時間,在事務結束的時候通過trx_commit_low()分配。

    trx_sys::rw_trx_ids:當前活躍的事務ID;事務在開始時ID會被添加至此數據結構中;事務提交時ID會被從此數據結構中刪除。

    在構造一條數據記錄時,我們除了在數據記錄中添加用戶自主添加的數據列,系統還會自動分配一些系統列,具體包括:

    DATA_TRX_ID:修改過此行數據記錄的最新事務ID。

    DATA_ROLL_PTR:指向這條數據記錄的上一個版本的指針,上一版數據在undo log中。

    通過上面幾個數據結構的介紹,我們大概了解了一些基本概念;但是這對于一個事務來判斷那些事務對它是已經提交的,那些事務對它是活躍的還是遠遠不夠的;所以我們接下來介紹MVCC中最重要的一個數據結構:視圖,也就是read view。

    2.1 視圖

    每一個事務在讀取數據時都會被分配一個視圖,通過視圖就可以來判斷其他事務對數據記錄的可見性。下面我們來具體介紹一下視圖是如何運作的。

    分配:主要通過trx_assign_read_view()來給一個事務分配視圖;在事務的隔離級別是Consistent Snapshot 或 Read Repeatable時,事務開始時會給其分配;其他情況下當事務需要讀取數據時將會給其分配一個視圖。

    回收:事務結束時,會通過view_close()對其視圖進行回收。

    幾個關鍵數據結構:

    1. m_low_limit_id:高水位,分配時取trx_sys::max_trx_id,也就是取當前還沒有被分配的事務ID。
    2. m_up_limit_id:低水位,如果m_ids不為空,取其最小值,否則取trx_sys::max_trx_id。
    3. m_ids:在此視圖初始化時,通過copy_trx_ids()從trx_sys::rw_trx_ids拷貝一份活躍事務ID(不包含當前事務ID)。

    那么有了上面這些數據我們就可以判斷那些事務對于此視圖是活躍的,那些事務對于此視圖是已經提交的。

    那些事務對于此視圖是活躍的:

    1. trx_id > read_view::m_low_limit_id
    2. read_view::m_up_limit_id < trx_id < read_view::m_low_limit_id,并且 trx_id 屬于 trx_t::read_view::m_ids

    如果給定一個trx_id滿足上面兩個條件其中之一,那么這個事務對于此視圖就是活躍的。

    那些事務對于此視圖是已經提交的:

    1. trx id < read_view::m_up_limit_id
    2. read_view::m_up_limit_id < trx id < read_view::m_low_limit_id,并且 trx id 不屬于 trx_t::read_view::m_ids

    如果給定一個trx_id滿足上面兩個條件其中之一,那么這個事務對于此視圖就是已經提交的。

    2.2 數據可見性

    通過上面的介紹,那么一個事務就可以通過此事務的視圖來對數據記錄判斷可見性了。

    具體是通過ReadView::changes_visible()來判斷可見性的,具體如下:

    假設一個事務為T,trx_id為記錄R中的DATA_TRX_ID:

    1. trx_id > read_view::m_low_limit_id,T 不可見 R
    2. trx_id < read_view::m_up_limit_id,T 可見 R
    3. read_view::m_up_limit_id =< trx_id <= read_view::m_low_limit_id時,如果trx_id 屬于 trx_t::read_view::m_ids 時,T 不可見 R。否則可見 R

    如果T對R不可見,就需要R中的DATA_ROLL_PTR來構造出上一個數據頁版本,直至記錄可見。

    我們通過下面一個例子來說明可見性。

    undo log 的清理

    我們通過之前的系列文章已經了解到undo log在磁盤和內存中是如何組織的;undo log是如何分配的;以及undo log是如何使用的。那么undo log會一直記錄下去么?當然不是,有些undo log如果沒用的話是會被回收清理的。

    那么下面這將會介紹那些undo log可以清理,以及undo log是怎么進行清理的。

    1. 幾個關鍵的數據結構

    在介紹undo log 清理之前,先介紹幾個關鍵的數據結構;這幾個數據結構對于undo log的清理實現是至關重要的。

    trx_sys->serialisation_list: 里面存放的是正在提交的事務,按照trx_t::no有序的排列;事務會在開始提交時通過 trx_serialisation_number_get() 添加至該數據結構,事務結束提交時通過trx_erase_lists()將該事務從該數據結構中移除。

    read_view::m_low_limit_no:擁有該read_view的對象,對于trx_t::no小于read_view::m_low_limit_no的undo log都不在需要;該變量的取值時trx_sys->serialisation_list中最早的一個事務的trx_t::no;因為trx_sys->serialisation_list內有序存放的正在提交的事務,如果一個事務的trx_t::no比該數值還小,那么這個事務一定已經提交了。

    TRX_RSEG_HISTORY與TRX_UNDO_HISTORY_NODE:這兩個值我們之前在《undolog的組織形式》里簡單介紹過,這兩個值共同將回滾段中的history list組織起來;在事務提交時,如果是update/delete類型的undo log,將其undo log header以頭插法的方式通過trx_purge_add_update_undo_to_history()加入到該回滾段的history list中,如果是insert類型的undo log其空間會被當場釋放,這是因為insert記錄沒有舊的版本;因此history list中的undo log header是以trx_t::no降序排列的,這里需要注意一下:history list里面的節點是undo log header。下面我們通過一幅圖來具體說明下磁盤上history list的結構。

    2. 那些undo log可以清理?

    對于一個事務來說,早于read_view::m_low_limit_no的undo log都不需要訪問了;那么如果存在一個read view,其read_view::m_low_limit_no比所有read view的m_low_limit_no都要小,那么小于此read_view::m_low_limit_no的undo log就不在被所有活躍事務所需要了,那么這些undo log就可以清理了。

    在read_view初始化時,會使用頭插法通過view_open()插入到一個全局視圖鏈表(MVCC::m_views)中,在事務結束時通過view_close()會從全局視圖鏈表中將此read view移除;因為是順序插入,所以此鏈表中最后一個還沒有close的視圖就可以看做是最老的一個視圖;小于此視圖的undo log可以被清理,一般將此視圖賦值給purge_sys::view。

    現在我們已經可以決定那些undo log是可以被清理的,那么下一步我們還需要找到具體那些undo log可以清理。

    在事務提交時,此事務對應的回滾段會通過trx_serialisation_number_get()加入到purge_sys::purge_queue中。

    purge_sys::purge_queue是一個以回滾段中第一個提交事務的trx_t::no為key的優先級隊列。

    如此一來,從purge_sys::purge_queue取出的回滾段中一定包含最老提交的事務,將此事務的trx_t::no與purge_sys::view對比,即可判斷出此事務相關的undo log是否可以被清理。

    purge_sys::purge_queue的詳細信息如下圖:

    3. undo log怎么清理?

    解決了那些undo log可以清理的問題后,下面接著繼續看undo log怎么進行清理的問題。

    當放入history list的undo log且不會再被訪問時,需要進行清理操作,另外數據頁上面的標記刪除的操作也需要清理掉,有一些purge線程負責這些操作,去入口函數為srv_do_purge() -> trx_purge(),其大致流程如下:

    1. 通過trx_sys->mvcc->clone_oldest_view()獲取最老的視圖復制給purge_sys::view,方便之后真正purge undo log時判斷其是否不會再被訪問到了。
    2. 通過trx_purge_attach_undo_recs()獲取需要被purge的undo log,其大致流程如下:
    3. 通過trx_purge_fetch_next_rec()循環獲取可以被purge的undo log,默認一次最多獲取300個undo log record,可以通過innodb_purge_batch_size來調整。
    4. 循環獲取可以被purge的undo log record大致流程如下:
    5. 從purge_sys::purge_queue取出第一個回滾段,從其history list上讀取最老還未被purge的事務的undo log header。
    6. 從此undo log header依次讀取undo log record。
    7. 讀取完畢后,重新統計此回滾段最老還未被purge的事務的位點,然后重新放入purge_sys::purge_queue;最后回到第一步。
    8. 將這些undo log分發給purge工作線程,purge工作線程的入口函數為row_purge_step()->row_purge()->row_purge_record()。

    這里purge undo log record時主要分為兩種情況:清理TRX_UNDO_DEL_MARK_REC記錄或者清理TRX_UNDO_UPD_EXIST_REC記錄

    1. 清理TRX_UNDO_DEL_MARK_REC類型的記錄,需要通過row_purge_del_mark()將所有的聚集索引與二級索引記錄都清除掉。
    2. 清理TRX_UNDO_UPD_EXIST_REC類型的記錄,需要通過row_purge_upd_exist_or_extern()將舊的二級索引清理掉。
    3. 通過trx_purge_truncate()來對history list進行清理,其大致流程如下:
    4. 遍歷所有回滾段,并通過trx_purge_truncate_rseg_history()對回滾段中的history list進行清理,其大致流程如下:
    5. 將history list最后一個事務的undo log header讀取出來。
    6. 判斷此undo log是否已經被purge,如果已經被purge則繼續;如果沒有被purge則退出。
    7. 將此事務所有的undo log釋放,并從history list上刪除;會到第一步。
    8. 在這之后,如果發現某些undo tablespace空間占用過大,被標記需要通過trx_purge_truncate_marked_undo()進行對其truncate,其大致流程如下:
    9. 創建一個undo_trunc.log的標記文件,來表明當前undo tablespace正在進行truncate;這是為了保證在truncate中間發生重啟時可以順利重建此undo tablespace。
    10. 通過trx_undo_truncate_tablespace()接口來對其文件做真正的truncate。
    11. 刪除undo_trunc.log標記文件,表明undo tablespace的truncate已經完成。

    注意:當一個undo tablespace被標記為需要truncate時,不會再有事務從此undo tablespace分配回滾段,而且進行truncate時必須保證該undo tablespace上所有的undo log都已經被purge。

    4. 最后

    通過上面的介紹,我們知道了undo log那些記錄可以被清理以及是怎么清理的,但是清理undo log過程中還有很多繁雜的細節;比如清理索引時涉及到對B樹的操作,以及舊版本數據的構建,XA事務,BLOB等等;這類內容暫時略過后面有機會繼續介紹。

    參考內容

    MySQL 8.0.23's source code
    MySQL 8.0 Reference Manual
    MySQL Server Team Blog
    庖丁解InnoDB之Undo LOG
    數據庫故障恢復機制的前世今生
    淺析數據庫并發控制機制
    Jeremy Cole's github A little fun with InnoDB multi-versioning
    The basics of the InnoDB undo logging and history system
    MySQL · 引擎特性 · InnoDB undo log 漫游
    InnoDB:undo log(1)
    InnoDB:undo log(2)

    原文鏈接:http://click.aliyun.com/m/1000349864/

    本文為阿里云原創內容,未經允許不得轉載。

網站首頁   |    關于我們   |    公司新聞   |    產品方案   |    用戶案例   |    售后服務   |    合作伙伴   |    人才招聘   |   

友情鏈接: 餐飲加盟

地址:北京市海淀區    電話:010-     郵箱:@126.com

備案號:冀ICP備2024067069號-3 北京科技有限公司版權所有