<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      MySQL在Uber的華麗轉(zhuǎn)身:Postgres輸在哪?【轉(zhuǎn)】

      個人其它平臺技術(shù)文章:

      • 知乎ID:磚一塊一塊搬
      • 小紅書ID:碼農(nóng)有道

      在知乎上有同學(xué)提問關(guān)于 Postgres 和 MySQL 的區(qū)別,原本只是簡單的回答,結(jié)果越講越多,干脆整理成一篇文章分享出來。

      簡單來說,這兩種數(shù)據(jù)庫的核心差異,主要體現(xiàn)在主鍵索引和二級索引的實現(xiàn)方式,以及底層的數(shù)據(jù)存儲與更新機制上。

      接下來,我們就來詳細看看這兩者的不同之處。

      索引

      索引是一種用于加速查詢的數(shù)據(jù)結(jié)構(gòu),通常采用 B+ 樹實現(xiàn)。這種結(jié)構(gòu)通過多層節(jié)點進行鍵值查找,數(shù)據(jù)庫內(nèi)部通常以“頁(page)”的形式組織這些節(jié)點。查找過程中,系統(tǒng)會從根節(jié)點出發(fā),逐層遍歷樹結(jié)構(gòu),逐步排除不包含目標數(shù)據(jù)的頁面,直到最終定位到包含目標鍵的葉子頁面。

      葉子節(jié)點中存儲的是有序的鍵(key)以及對應(yīng)的值(value)。一旦定位到目標鍵,便可直接獲取相應(yīng)的值。與此同時,該頁面會被緩存在數(shù)據(jù)庫的共享緩沖區(qū)中,以便后續(xù)查詢可以重用,從而提升查詢效率。

      圖片

      在 B+ 樹索引中,“鍵”指的是創(chuàng)建索引時指定的列,而“值”是什么,不同的數(shù)據(jù)庫有不同的實現(xiàn)方式。接下來,我們就來看看 Postgres 和 MySQL 在這方面的具體差異。

      MySQL

      在 MySQL 中,主鍵索引的“值”其實就是整行數(shù)據(jù)本身,即該行的所有字段內(nèi)容。這也是為什么主鍵索引通常被稱為“聚簇索引”。

      需要說明的是,這里的描述是基于行存儲的數(shù)據(jù)庫系統(tǒng)。對于采用列存儲、圖數(shù)據(jù)庫或文檔型數(shù)據(jù)庫的系統(tǒng)來說,由于存儲模型不同,“值”的定義也會有所區(qū)別。

      當通過主鍵索引查找某個鍵對應(yīng)的數(shù)據(jù)時,只需定位到該鍵所在的頁面,就可以直接獲取整行數(shù)據(jù),無需額外的 I/O 操作去讀取其他列,因為其值包含了整行數(shù)據(jù)本身。

      而在二級索引中,鍵是你創(chuàng)建索引時指定的列,但“值”不再是整行數(shù)據(jù),而是一個指針,用于定位這條完整數(shù)據(jù)所在的位置。通常來說,二級索引葉子節(jié)點中的值就是對應(yīng)行的主鍵值。

      也正因如此,MySQL 要求每張表必須有一個主鍵索引,所有二級索引最終都要通過主鍵來定位數(shù)據(jù)。如果沒有顯式定義主鍵,MySQL 會自動為你生成一個隱藏主鍵。

      圖片

      Postgres

      在 Postgres 中,從技術(shù)上講并沒有“主鍵索引”的概念,所有索引本質(zhì)上都是二級索引。它們都指向系統(tǒng)管理的 tuple ID(元組 ID),根據(jù)這些 ID 再定位到實際存儲在堆中的數(shù)據(jù)頁。需要注意的是,堆中的表數(shù)據(jù)是無序的,并不像聚簇索引的葉子節(jié)點那樣按鍵值順序排列。

      舉個例子,如果你依次插入第 1 到第 100 行數(shù)據(jù),這些行可能會被存儲在同一個頁面中;但如果你隨后更新了第 1 到第 20 行,這些更新后的數(shù)據(jù)很可能會被寫入到其他頁面,從而導(dǎo)致數(shù)據(jù)的物理分布變得無序。

      而在 MySQL 的聚簇索引中,插入操作需要保證數(shù)據(jù)按鍵值(也就是索引字段值)順序?qū)懭耄@就限制了數(shù)據(jù)的物理排列方式。因此,Postgres 中的表通常被稱為“堆組織表(heap organized tables)”,而不是“索引組織表(index organized tables)”。

      此外需要特別注意的是,在 Postgres 中,更新和刪除操作本質(zhì)上其實都是插入操作。每次執(zhí)行更新或刪除時,系統(tǒng)都會生成一個新的元組 ID(tuple ID),而原有的元組 ID 會被保留下來,用于支持 MVCC(多版本并發(fā)控制)機制。這個細節(jié)我們稍后會在文章中進一步探討。

      實際上,僅有元組 ID 并不足以定位具體數(shù)據(jù),還需要知道該元組所在的數(shù)據(jù)頁編號。這兩個信息組合在一起,稱為 c_tid。想一想,如果只有元組 ID 而不知道它在哪一頁,是無法直接定位數(shù)據(jù)的。

      由于在 Postgres 中,索引只保存元組的位置信息,因此必須多執(zhí)行一次 I/O 操作來加載對應(yīng)的數(shù)據(jù)頁,才能獲取完整的行數(shù)據(jù)。

      圖片

      查詢代價

      我們來看下面這張示例表:

      # 表 T;
      # 主鍵列 PK 上有主索引,C2 列上有二級索引,C1 沒有索引;
      # C1 和 C2 是文本類型,PK 是整數(shù)。

      | PK | C1 | C2 |
      |----|----|----|
      | 1  | x1 | x2 |
      | 2  | y1 | y2 |
      | 3  | z1 | z1 |

      現(xiàn)在,我們對比在 MySQL 和 Postgres 中執(zhí)行以下 SQL 查詢時的差異:

      SELECT * FROM T WHERE C2 = 'x2';

      在 MySQL 中,這條查詢會涉及兩次 B+ 樹查找操作。首先,通過 C2 列的二級索引查找字段 C2 值為 'x2' 的記錄,獲取其對應(yīng)的主鍵值 1;接著再通過主鍵索引查找主鍵為 1 的那一行數(shù)據(jù),從而獲取整行記錄( * 表示所有的字段)。

      有些人可能會認為,這只是兩次 I/O 操作,其實并非如此。B+ 樹查找的時間復(fù)雜度是 O(logN),當索引規(guī)模較大時,一次查找可能涉及多個節(jié)點,每個節(jié)點對應(yīng)一個頁面,因此可能觸發(fā)多次 I/O 操作。

      而在 Postgres 中,執(zhí)行這條查詢時,首先通過 C2 列的二級索引查找匹配的元組 ID,然后進行一次堆訪問,從堆中加載包含完整行數(shù)據(jù)的頁面。

      從訪問路徑來看,大多數(shù)情況下,一次索引查找加一次堆訪問要比兩次 B+ 樹查找更高效,也就是說,Postgres 在這種場景下的查詢性能往往優(yōu)于 MySQL。

      為了讓這個例子更貼近實際,我們進一步假設(shè):C2 列的值并不唯一,也就是說,可能有多行記錄的 C2 值都是 'x2'。在這種情況下,查詢過程中會返回多個匹配的元組 ID(在 MySQL 中則是多個主鍵值)。

      問題在于,這些匹配的行可能分布在多個不同的數(shù)據(jù)頁上,導(dǎo)致大量隨機讀操作。在 MySQL 中,這意味著要重復(fù)進行多次主鍵索引查找(當然,查詢優(yōu)化器也可能根據(jù)匹配記錄數(shù)量,選擇走索引掃描而非逐條查找),但無論是在 MySQL 還是 Postgres 中,這種情況最終都不可避免地帶來頻繁的隨機 I/O。

      為盡量減少這種隨機訪問,Postgres 會采用 位圖索引掃描(Bitmap Index Scan) 的方式來優(yōu)化查詢流程:首先將所有匹配結(jié)果按照頁面而非單條元組進行聚合,然后一次性批量加載這些數(shù)據(jù)頁,盡可能降低 I/O 次數(shù)。接下來再在內(nèi)存中進行過濾,最終返回滿足條件的記錄。

      接下來,我們來看一個查詢的例子。

      SELECT * FROM T WHERE PK BETWEEN 1 AND 3;

      在主鍵索引上的范圍查詢方面,MySQL 的表現(xiàn)更為出色。它只需進行一次查找,定位到第一個匹配的鍵,然后沿著 B+ 樹葉子節(jié)點的鏈表依次向后遍歷,獲取相鄰的鍵,并在遍歷過程中直接讀取對應(yīng)的整行數(shù)據(jù)。

      相比之下,Postgres 在這方面就顯得吃力一些。雖然它的二級索引同樣會在 B+ 樹的葉子節(jié)點上進行遍歷,找到所有匹配的鍵,但它只會收集對應(yīng)的元組 ID 和頁面信息,但此時工作并未就此完成。Postgres 還需要執(zhí)行額外的隨機讀操作,從堆中加載這些元組對應(yīng)的完整行數(shù)據(jù)。而這些數(shù)據(jù)行很可能分布在堆的不同位置,尤其是在頻繁更新的情況下,數(shù)據(jù)往往不會連續(xù)、緊湊地存放在一起。

      對于更新頻繁的場景來說,這正是 Postgres 的一項劣勢。因此,為表設(shè)置合適的 FillFactor(填充因子)非常重要,它可以在一定程度上可以緩解數(shù)據(jù)分散帶來的性能問題。

      接著,我們來看一個更新操作的例子:

      UPDATE T SET C1 = 'XX1' WHERE PK = 1;

      在 MySQL 中,如果更新的是一個未被索引的列,那么只需在主鍵索引的葉子節(jié)點中,直接修改該行對應(yīng)的字段值即可。由于所有的二級索引都通過主鍵進行定位,而主鍵值本身沒有發(fā)生變化,因此不需要更新任何索引結(jié)構(gòu)。

      而在 Postgres 中,即使更新的是一個沒有索引的列,系統(tǒng)也會生成一個新的元組并賦予新的元組 ID,這意味著,所有原本指向舊元組的二級索引項都需要更新為指向新元組的位置。換句話說,雖然索引列本身并未發(fā)生變化,但由于底層的元組位置發(fā)生了改變,相關(guān)索引也“可能”需要更新,從而引發(fā)大量的寫入 I/O。

      早在 2016 年,Uber 就曾明確表達過對這一機制的不滿,這也是他們將數(shù)據(jù)庫從 Postgres 遷移到 MySQL 的主要原因之一。

      這里之所以說“可能”需要更新所有二級索引,是因為 Postgres 中存在一個名為 HOT(Heap Only Tuple)的優(yōu)化機制。

      這個優(yōu)化的原理是:在滿足一定條件的情況下,允許二級索引保留原有的元組 ID,而不立即更新為新生成的元組 ID。此時,Postgres 會在堆頁面內(nèi),通過在舊元組和新元組之間建立一個鏈表,從舊元組跳轉(zhuǎn)到新元組,實現(xiàn)不同版本之間的關(guān)聯(lián)。這樣就可以避免更新索引,從而減少寫入 I/O 的開銷。

      數(shù)據(jù)類型很重要

      在 MySQL 中,主鍵的數(shù)據(jù)類型至關(guān)重要,因為所有二級索引都需要存儲對應(yīng)的主鍵信息。舉例來說,如果主鍵使用的是 UUID,那么每條二級索引記錄中都要包含這個較長的主鍵值,導(dǎo)致二級索引變得臃腫,不僅占用更多的存儲空間,也會帶來更高的讀 I/O 開銷。

      而在 Postgres 中,二級索引并不存儲主鍵的實際值,而是統(tǒng)一使用固定大小為 4 字節(jié)的元組 ID(tid)來指向堆中的數(shù)據(jù)。正因如此,即使主鍵是 UUID,二級索引的大小也不會受到影響。

      撤銷日志(Undo Logs)

      幾乎所有現(xiàn)代數(shù)據(jù)庫都支持多版本并發(fā)控制(MVCC)。以最常見的“讀已提交”(Read Committed)隔離級別為例,如果一個事務(wù) tx1 對某一行數(shù)據(jù)進行了更新但尚未提交,而此時另一個并發(fā)事務(wù) tx2 試圖讀取這行數(shù)據(jù),那么它應(yīng)當讀取更新前的舊值,而不是尚未提交的新值。

      大多數(shù)數(shù)據(jù)庫(包括 MySQL)通過撤銷日志(Undo Logs)來實現(xiàn)這一機制。

      當某個事務(wù)修改了一行數(shù)據(jù)時,變更會直接寫入共享緩沖池中的頁面,也就是說,該頁面始終保存的是最新的數(shù)據(jù)。隨后,事務(wù)會將“如何撤銷這次變更”的信息寫入撤銷日志(Undo Log),以便在需要時可以還原該行數(shù)據(jù)的舊狀態(tài)。這樣,當并發(fā)事務(wù)基于自身的隔離級別需要讀取舊版本數(shù)據(jù)時,系統(tǒng)就可以通過解析 Undo Log 構(gòu)造出對應(yīng)的舊數(shù)據(jù)行。

      你可能會疑惑:將未提交的修改直接寫入頁面,真的安全嗎?如果后臺進程把這個頁面刷入磁盤,而事務(wù)還沒提交,此時數(shù)據(jù)庫突然崩潰了怎么辦?

      這正是Undo Log存在的意義。在數(shù)據(jù)庫啟動時,如果檢測到之前發(fā)生了崩潰,會在恢復(fù)階段利用 Undo Log 回滾那些未提交的變更,從而確保數(shù)據(jù)一致性不被破壞。

      當然,Undo Log 會對并發(fā)事務(wù)帶來額外的開銷,尤其是在存在長事務(wù)的情況下。為了構(gòu)造舊版本數(shù)據(jù),系統(tǒng)需要執(zhí)行額外的 I/O 操作,而且Undo Log 也有可能被寫滿,從而導(dǎo)致事務(wù)失敗。

      我曾遇到過一個真實案例:某數(shù)據(jù)庫系統(tǒng)在崩潰恢復(fù)時,光是回滾一個運行了 3 小時但未提交的長事務(wù),,就耗費了一個多小時。由此可見,盡量避免長事務(wù),是非常有必要的。

      而 Postgres 的處理方式則完全不同。每一次更新、插入或刪除操作,都會生成該行數(shù)據(jù)的一個新副本,并分配一個新的元組 ID(tuple ID),同時記錄創(chuàng)建和刪除該元組的事務(wù) ID。借助這些信息,Postgres 可以安全地將變更寫入數(shù)據(jù)頁,并允許并發(fā)事務(wù)根據(jù)自身的事務(wù) ID 判斷應(yīng)讀取舊版本還是新版本的數(shù)據(jù)。這個設(shè)計非常巧妙。

      當然,再巧妙的方案也并非沒有代價。我們之前就提到過,為每次變更生成新的元組 ID,會對二級索引帶來額外開銷。此外,Postgres 還需要在適當時機清理那些不再需要的舊元組,這項清理工作則由 Vacuum 機制負責執(zhí)行。

      Processes vs Threads

      MySQL 使用的是線程模型,而 Postgres 則采用的是進程模型。兩者各有優(yōu)劣,我在另一篇文章中已做過詳細分析。圖片就個人而言,在數(shù)據(jù)庫系統(tǒng)中,我更傾向于使用線程。原因很簡單,線程更輕量,而且能共享父進程的虛擬內(nèi)存地址空間。而進程則需要獨立的虛擬內(nèi)存,其進程控制塊(PCB)也比線程控制塊(TCB)更大,資源開銷更高。

       

      轉(zhuǎn)自

      MySQL在Uber的華麗轉(zhuǎn)身:Postgres輸在哪?
      https://mp.weixin.qq.com/s/BEoOsLXQtwKchB_Wk45Bvw?clicktime=1753496836&enterid=1753496836&exptype=unsubscribed_card_recommend_article_u2i_mainprocess_coarse_sort_pcfeeds&ranksessionid=1753496829_1&scene=169&subscene=200

      posted @ 2025-07-26 21:43  paul_hch  閱讀(8)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 潍坊市| 亚洲自拍偷拍激情视频| 国产精品黄色精品黄色大片| 亚洲高清 一区二区三区| 亚洲欧美一区二区三区在线| 久青草国产综合视频在线| 亚洲精品自拍在线视频| 欧美白妞大战非洲大炮| 人妻内射一区二区在线视频| 国产高颜值不卡一区二区| 亚洲综合一区二区三区在线| 国产精品久久久福利| 欧美精品一产区二产区| 成人做受视频试看60秒| 亚洲中文字幕av无码区| 日韩人妻少妇一区二区三区| 亚洲成av人片无码天堂下载| 亚洲成aⅴ人片久青草影院| 亚洲av色香蕉一区二区| 国产精品人妻中文字幕| 朔州市| 亚洲精品一区二区美女| 亚洲av影院一区二区三区| 蜜芽久久人人超碰爱香蕉| 绥棱县| 三人成全免费观看电视剧高清| 国产精品亚欧美一区二区三区| 国产在线午夜不卡精品影院| 亚洲综合精品香蕉久久网| aaa少妇高潮大片免费看| 国自产拍偷拍精品啪啪一区二区| 日韩精品一区二区三区激情| 国产午夜A理论毛片| 久久久久无码中| 99精品人妻少妇一区| 国产精品成人一区二区不卡| 国产女人在线视频| 久久国产成人高清精品亚洲| 九九热在线精品视频首页| 免费黄色大全一区二区三区| 国精偷拍一区二区三区|