mysql的update底層一定是先刪除再插入嗎? 1. 場景分析 2. 附錄: 推薦閱讀

目錄:

  1. 場景分析
    1.1 不更新主鍵
  • 1.1.1 就地更新(in-place update)
  • 1.1.2 先刪除掉舊記錄,再插入新記錄
    1.2. 更新主鍵
  1. 附錄
    2.1 delete操作的兩個階段
  • 階段一:delete_mark階段
  • 階段二:purge階段

先說結論:不一定是。

可以分爲三種場景:

  1. 不更新主鍵
    1.1 更新後的列和更新前的列佔用存儲空間都一樣大(就地更新);
    1.2 更新後的列和更新前的列佔用存儲空間不一樣大(事務提交後,同步purge刪除後插入);
  2. 更新主鍵(事務提交前:舊數據先delete_mark,事務提交後:異步purge刪除)

1. 場景分析

1.1 不更新主鍵

1.1.1 就地更新(in-place update)

更新記錄時,對於被更新的每個列來說,如果更新後的列和更新前的列佔用的存儲空間都一樣大,那麼就可以進行 就地更新 (in-place update),也就是直接在原記錄的基礎上修改對應列的值。

1.1.2 先刪除掉舊記錄,再插入新記錄

在不更新主鍵的情況下,如果有任何一個被更新的列更新前和更新後佔用的存儲空間大小不一致,那麼就需要先把這條舊的記錄從聚簇索引頁面中刪除掉,然後再根據更新後列的值創建一條新的記錄插入到頁面中。

請注意(事務提交後):我們這裏所說的刪除並不是delete mark操作,而是真正的刪除掉,也就是把這條記錄從 正常記錄鏈表中移除並加入到垃圾鏈表中。加入到垃圾鏈表的過程並不是另外的purge線程處理的,而是由用戶線程同步執行真正的刪除(purge)操作。真正刪除後緊接着就要根據各個列更新後的值創建新紀錄插入。

這裏如果新創建的記錄佔用的存儲空間大小不超過舊記錄佔用的空間,那麼可以直接重用被加入到垃圾鏈表中的舊記錄所佔用的存儲空間,否則的話需要在頁面中新申請一段空間以供新記錄使用,如果本頁面內已經沒有可用的空間的話,那就需要進行頁面分裂操作,然後再插入新記錄。

1.2. 更新主鍵

在聚簇索引中,記錄是按照主鍵值的大小連成了一個單向鏈表的,如果我們更新了某條記錄的主鍵值,意味着這條記錄在聚簇索引中的位置將會發生改變,比如你將記錄的主鍵值從1更新爲10000,如果還有非常多的記錄的主鍵值分佈在 1 ~ 10000 之間的話,那麼這兩條記錄在聚簇索引中就有可能離得非常遠,甚至中間隔了好多個頁面。針對 UPDATE 語句中更新了記錄主鍵值的這種情況, InnoDB 在聚簇索引中分了兩步處理:

  • 將舊記錄進行 delete mark 操作

高能注意:這裏是delete mark操作!這裏是delete mark操作!這裏是delete mark操作!也就是說在 UPDATE語句所在的事務提交前,對舊記錄只做一個 delete mark 操作,在事務提交後才由專門的線程做purge操作,把它加入到垃圾鏈表。這裏一定要和我們上邊所說的在不更新記錄主鍵值時,先真正刪除舊記錄,再插入新記錄的方式區分開!

之所以只對舊記錄做delete mark操作,是因爲別的事務同時也可能訪問這條記錄,如果把它真
正的刪除加入到垃圾鏈表後,別的事務就訪問不到了。這個功能就是所謂的MVCC,

  • 根據更新後各列的值創建一條新記錄,並將其插入到聚簇索引中(需重新定位插入的位置)。

由於更新後的記錄主鍵值發生了改變,所以需要重新從聚簇索引中定位這條記錄所在的位置,然後把它插進去。

針對 UPDATE 語句更新記錄主鍵值的這種情況,在對該記錄進行 delete mark 操作前,會記錄一條類型爲TRX_UNDO_DEL_MARK_REC 的 undo日誌 ;之後插入新記錄時,會記錄一條類型爲TRX_UNDO_INSERT_REC 的 undo日誌 ,也就是說每對一條記錄的主鍵值做改動時,會記錄2條 undo日誌 。

2. 附錄:

2.1 delete操作的兩個階段

我們知道插入到頁面中的記錄會根據記錄頭信息中的next_record屬性組成一個單向鏈表,我們把這個鏈表稱之爲正常記錄鏈表
被刪除的記錄其實也會根據記錄頭信息中的next_record屬性組成一個鏈表,只不過這個鏈表中的記錄佔用的存儲空間可以被重新利用,所以也稱這個鏈表爲垃圾鏈表Page Header部分有一個稱之爲 PAGE_FREE的屬性,它指向由被刪除記錄組成的垃圾鏈表中的頭節點。

假設此刻某個頁面中的記錄分佈情況是這樣的(這個不是undo_demo表中的記錄,只是我們隨便舉的一個例子):

爲了突出主題,在這個簡化版的示意圖中,我們只把記錄的 delete_mask 標誌位展示了出來。從圖中可以看出,正常記錄鏈表中包含了3條正常記錄, 垃圾鏈表 裏包含了2條已刪除記錄,在 垃圾鏈表 中的這些記錄佔用的存儲空間可以被重新利用。

頁面的 Page Header 部分的 PAGE_FREE 屬性的值代表指向 垃圾鏈表 頭節點的指
針。假設現在我們準備使用 DELETE 語句把 正常記錄鏈表 中的最後一條記錄給刪除掉,其實這個刪除的過程需要經歷兩個階段:

階段一:delete_mark階段

  • 階段一:僅僅將記錄的 delete_mask 標識位設置爲 1 ,其他的不做修改(其實會修改記錄的 trx_id 、roll_pointer 這些隱藏列的值)。這個階段稱之爲 delete mark

可以看到, 正常記錄鏈表 中的最後一條記錄的 delete_mask 值被設置爲 1 ,但是並沒有被加入到 垃圾鏈表 。此時這條記錄處於一箇中間狀態。在刪除語句所在的事務提交之前,被刪除的記錄一直都處於這種所謂的 中間狀態 。

ps:爲啥會有這種奇怪的中間狀態呢?其實主要是爲了實現一個稱之爲MVCC的功能。

階段二:purge階段

  • 階段二::當該刪除語句所在的事務提交之後,會有專門的線程後來真正的把記錄刪除掉。所謂真正的刪除就是把該記錄從正常記錄鏈表中移除,並且加入到垃圾鏈表中。並且還要調整一些頁面的其他信息。這個階段稱之爲 purge 。

推薦閱讀

《MySQL 是怎樣運行的:從根兒上理解 MySQL》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章