從集合的無序性看待關係型數據庫中的"序"

1.集合的特徵

關係型數據庫,一方面它是數據庫,能夠存儲數據,另外一方面,它是關係的,也就是基於關係模型的。在關係型數據庫中,專門爲關係模型設計了對應的"關係引擎",關係引擎中包含了語句分析器、優化器、查詢執行器。語句分析器用於分析語句是否正確,優化器用於生成查詢的執行計劃,查詢執行器按照優化器生成的執行計劃去執行查詢操做,並將相關操做指令交給存儲引擎,由存儲引擎跟底層的數據(磁盤/緩存)打交道。算法

這裏咱們不談數據存儲,而是站在數據庫的角度上談關係模型的一個特性:基於集合理論。sql

高中數學裏,咱們都學過集合有三個特徵:肯定性、互異性和無序性。其中肯定性和互異性是成爲集合的條件,無序性是集合的特性。數據庫

肯定性指的是集合中的元素必須是明確的,不能在集合中存放一個可能大於2,也可能大於3這樣的元素。在關係型數據庫中,這一特徵能夠不用考慮,由於數據只要存到數據庫中,數據就必定是肯定的。緩存

互異性是指集合中的元素不能重複。在關係型數據庫中,記錄是否重複的問題沒有嚴格的規範。一方面,各類各樣的業務邏輯不容許在數據庫的角度上嚴格限制記錄的重複性。另外一方面,能夠經過設置主鍵或者惟一索引來保證記錄之間不重複,也就是"惟一性"。在不少狀況下,具備惟一性的表能優化查詢,減小記錄的檢索次數。性能

無序性是指集合中的元素之間是無序的。在關係型數據庫中,對集合的無序性實現的最完整。甚至能夠說,無序性貫穿了整個關係型數據庫,尤爲是關係引擎中的優化器更是關注"序"這個概念。優化

所以,本文主要圍繞集合的"序",來解釋關係型數據庫中和"序"有關的行爲。spa

2.集合的無序性

這裏的"序",不是指數據按大小排過序,也不是指物理存儲數據時排過序,而是站在集合的角度或關係引擎的角度(若不理解,就當是數據庫角度)上看"數據是否有序"的概念,它是邏輯上的"序"。設計

舉個例子就能理解。集合A(1,2,3,4)和集合B(2,1,4,3),看上去集合A中元素是有序的,集合B中元素是無序的。指針

若是要從集合A和集合B中取出小於2的元素,不管是集合A仍是集合B都要比較4次才能獲得最終結果,由於集合並不知道元素2的後面是否還有比2小的值。並且對於集合來講,根本就沒有前面和後面的概念。code

也就是說,對於集合A來講,2是第二個元素是錯誤的說法。集合中不該該有"第"這種說法(數據庫也如此,只要檢索的對象不是order by後的結果或遊標對象,取第幾行這種概念將老是按照物理存儲順序去訪問的)。

若是咱們使用下圖的模式去看待這兩個集合,也用這種模式去看到數據庫表中的記錄,不少時候更有助於理解sql語法的本質和sql優化。

因此,這兩個集合是等價的集合,只不過這裏的集合A,在咱們人眼中碰巧有序而已。這也是數據庫中索引的意義,咱們人爲地將集合中的元素排序,人爲地告訴優化器它們是有序的,即便關係引擎依舊認爲它是無序的。

咱們能夠站在集合總體的角度上看待"序"這個概念。集合不關注其內部的元素是什麼,它只關注它自身是一個容器,包含了一堆知足集合條件的元素。當須要找出集合中某個或某些具體的元素時,須要掃描整個集合。

3.表中記錄的無序性

站在數據庫層面來說,關係型數據庫表中的數據是無序的,也就是咱們俗稱的"堆heap"。咱們應該這樣看待表中的每行記錄:

你可能會疑惑,使用(B+樹)索引不是能夠將表中全部數據排序後存儲嗎?沒錯,但在關係引擎看來,這個表仍然是集合的,它是一個亂序的總體,其內每行數據也都認爲是無序的。只不過在檢索數據的時候,優化器在生成執行計劃時能發現已經存在索引,它知道這些數據是根據索引排過序的,藉今生成成本更低的執行計劃。

轉換成上面集合的說法,索引的意義就是讓集合B(2,1,4,3)變成集合A(1,2,3,4),讓集合中的元素可以使用"第幾"這種概念。

例如,在集合的層次上要找出值大於2的元素,須要掃描整個集合,即須要比較4次才能獲得結果。同理,沒有使用索引的表也同樣會進行表掃描才能獲得最終結果。當使用索引後,也就是人爲地將集合B變成集合A,對於咱們人來講,只要從前向後找,當找到第一個大於2的元素後(請注意,不是等於2,而是大於2,這兩種行爲是有區別的),就知道它後面的全部元素必定是大於2的。對於數據庫來講,索引就是咱們人爲告訴優化器這個表排序過,優化器天然知道只要找到符合條件的記錄。

題外話:從這裏是否感覺到了關係引擎和優化器之間的關係?

  1. 對於關係引擎來講,整個表都是無序的。若是沒有優化器組件,關係引擎(查詢執行器,後文將直接使用關係引擎來表示查詢執行器的行爲)會進行表掃描,若是沒有索引,關係引擎也會進行表掃描。可是有了優化器,且通過"咱們的提醒"後,優化器就能決定關係引擎的執行計劃:走索引。
  2. 另外,既然咱們能隱式"提醒"優化器表存在索引,那麼咱們也能顯式"提醒"優化器優化器有別的索引,甚至強制"提醒"優化器沒有索引。這就是爲何關係型數據庫中都會有"hint"關鍵字的緣由。
  3. 絕大多數狀況下,咱們應該徹底信任優化器,相信它能幫咱們選出成本最低的執行計劃。但優化器有時候也會"聰明反被聰明誤",選出一條不怎麼好甚至性能極低的執行計劃,這時咱們要hint強制干涉,由咱們本身告訴優化器應該怎麼走索引、走哪條索引。
  4. 從這方面看,一個不支持hint功能的數據庫系統是不合格的數據庫系統,好比老版本的PostgreSQL。固然,在2012年它已經添加了hint的擴展功能。

再來看看無序表聯接的問題。當無序表1和無序表2進行內聯接、外聯接時,咱們應該這樣看待聯接的本質:

這也是站在關係引擎角度上看聯接的本質。因爲無序,關係引擎只能對兩個表都進行表掃描並逐一比對,這樣就造成了咱們常說的"笛卡爾積"。無疑,這樣的效率很低。爲了提升聯接時的效率,應該儘量多地減小記錄的掃描次數,這是聯接語句優化的本質。

雖然老是提到索引,感受索引能帶來性能上的提升,但不管如何請記得,對於關係引擎來講,表是集合的,是無序的,無論它是否有索引,是否人爲排序過。至於索引,它是優化器才認識的東西,關係引擎不認識。

4.集合的"序"和物理存儲順序之間的關係

先簡單說說數據庫是如何存儲數據的。

在數據庫系統中,表中的全部數據都存儲在數據庫文件中,這是磁盤上的文件。但在數據庫看來,表中的每一行數據都是存放在"頁"上的。頁是數據庫操做的最小單位,例如想讀取某一行數據時(假如走索引,不會表掃描),存儲引擎會將這行數據所在的一整頁地加載到內存中,並掃描這一頁。

數據頁中,使用槽位(slot)來記錄每一行數據,每一個槽表示一行數據。例如,下圖是一個頁面的大體示意圖(不一樣數據庫系統有所不一樣,但不影響理解):

這個頁面中每插入一行數據,就分配一個槽位,並在槽位圖上標記這個槽的位置(好比距離頁面頂端的偏移字節是多少),這樣就能知道這一行數據在頁中的位置。

而咱們所說的"物理存儲順序"就是槽位圖標記的順序。注意,不是頁面空間上的先後位置。由於槽位的順序和頁面空間的位置多是不一致的,例以下圖:

在頁面的空間位置上,slot2對應的記錄行在3的後面,可是掃描這個頁面的時候,將先掃描slot2,再掃描slot3。也就是說,物理順序是slot位圖的順序(或者說,將先返回slot2對應的行)。

在堆表中,slot位圖的和頁面的空間位置是徹底對應的。刪除一行數據,這行數據的槽位會保留,只不過槽位圖上的偏移會指向0。當插入新數據的時候,這個新數據可能會直接插入到這個槽位中(若是這個槽位裝不下這行數據,則會尋找其餘的槽位)。

而在有索引的狀況下,slot的順序和頁面空間的位置順序可能不同,這關乎到索引的類型。例如插入2,它是1和3中間的值,按理說應該插在slot1對應行的後面,但這樣會使得slot3向後移動。而這樣的設計,可讓數據直接插在頁面的尾部,只須要對slot號碼從新編號便可。性能要提升很多。

上面說的是單個頁內部的數據行順序問題。除了頁內順序,還有頁間的順序。例如頁面2緊跟在頁面1的後面,咱們稱之爲"頁面是連續的",但若是頁面2在頁面1的前面,或者頁面1和頁面2中間隔了不少其餘的頁,咱們稱之爲"頁面不連續"。在頁面不連續的時候,存儲引擎須要不斷地進行頁面跳躍,反映到磁盤上就是須要不斷的尋址。而咱們知道,機械硬盤花在尋址的時間上遠遠高於讀取數據的時間。這也稱之爲"頁面碎片",當碎片較多的時候,它會對性能形成極大的影響。

本文不會去詳細介紹這些東西。在這裏,惟一須要知道的就是"數據存儲的物理順序並非空間上的先後順序"。

那麼,集合的"序"和物理存儲順序之間有什麼關係呢?

在關係引擎看來表中的數據是無序的,但即便無序,數據也已經持久化到磁盤上了。它總要找出一個能掃描全部數據的方案。在不走索引的狀況下,優化器無其餘路可選,它只能按照物理存儲順序進行表掃描。在這以後,若是沒有排序算法對數據進行排序,那麼以後全部的操做都按照這個順序訪問數據。

所以,★★★★★★物理存儲順序是無序的起點,是數據隨機性的起點。★★★★★★虛擬表之因此無序,就是從這裏物理存儲順序開始的,當表掃描(或加載部分頁面)完成後,已經加載完成的數據已經固定在內存中,是有固定順序的,這時候已經不適合稱之爲"無序",而應該稱之爲"隨機"。

5.查詢結果(虛擬表)的無序性、隨機性

除了數據庫中的實體表,在查詢的時候,中途生成的虛擬表都是無序的,但order by和distinct後的結果除外。關於order by的結果,見後文的說明。

簡單的幾個例子。

首先建立示例表,並查看錶結構和數據以下:

MariaDB [test]> create table Student1 (sid int ,name char(20),age int,class char(20));
MariaDB [test]> insert into Student1 values(3,'zhangsan',21,'Java');
                                            (6,'zhaoliu',19,'Java'),
                                            (2,'huanger',23,'Python'),
                                            (1,'chenyi',22,'Java'),
                                            (4,'lisi',20,'C#'),
                                            (5,'wangwu',21,'Python'),
                                            (7,'qianqi',22,'C'),
                                            (8,'sunba',20,'C++'),
                                            (9,'yangjiu',24,'Java');

MariaDB [test]> select * from Student1;
+------+----------+------+--------+
| sid  | name     | age  | class | +------+----------+------+--------+ | 3 | zhangsan | 21 | Java | | 6 | zhaoliu | 19 | Java | | 2 | huanger | 23 | Python | | 1 | chenyi | 22 | Java | | 4 | lisi | 20 | C# | | 5 | wangwu | 21 | Python | | 7 | qianqi | 22 | C | | 8 | sunba | 20 | C++ | | 9 | yangjiu | 24 | Java | +------+----------+------+--------+

這裏面沒有任何索引,不管是關係引擎,仍是優化器,都認爲這個表是無序的,所以只能執行表掃描。而表掃描的過程是按照數據的"物理存儲順序"進行訪問的,sid=3的記錄先存進數據庫,就先訪問這個記錄(按照前文的物理存儲方式,這個說法是錯誤的,但如今忽略這個問題)。在沒有任何索引、沒有任何優化"提醒"時,優化器就會生成"按照物理存儲順序"的執行計劃去表掃描。

可是表掃描的結果對於咱們人類來講是無序的結果,更準確的說,是隨機的結果,是咱們沒法去預料的結果。由於堆中的數據在進行物理存儲時,可能會"見縫插針",而咱們根本不知道這根"針"插在哪一個"縫"裏,也不知道它前面的數據是什麼,後面的數據是什麼。例如,堆表中的部分數據刪除了,再插入一部分數據,新插入的數據有些可能會插入在表的尾部,有些也可能插在數據刪除後留下的"槽"(slot)中。

再來講明查詢執行過程當中生成的虛擬表。虛擬表是邏輯的概念,是SQL語句執行過程當中每個階段產生的數據集合,在咱們人的感官上,咱們會把這個集合當作虛擬表。但多數時候,它們並不真的是二維表結構的形式,只是內存中一段存儲了數據的緩存空間。少數時候,因爲算法或某些操做的須要,會實實在在地建立虛擬臨時表,例如使用DISTINCT子句對結果去重時,就會先生成一張臨時表用於排序並去重。

例如,在兩表聯接時咱們總說會產生"笛卡爾積",而後用一張二維表的形式去感覺這個笛卡爾積的虛擬表。

但在實際執行過程當中不會是這樣的表結構,而應該是下面這種結構。

也就是說,虛擬結果集中的數據是無序的,隨後對該虛擬結果集的操做也是隨機而沒法保證順序的。例如上面的笛卡爾積結果集,若是使用了WHERE子句篩選某些行,則篩選的過程是對笛卡爾積進行"表"掃描,但笛卡爾積本就是不保證順序的,因此當where篩選出多行時,這些行的順序可能會和咱們預料的結果有所不一樣。固然,優化器不會真的採用這樣的方案,但站在邏輯角度上看,由於虛擬結果集無序,要從中檢索數據只能進行"表"掃描。

再好比說TOP子句(MySQL、MariaDB中等價的是LIMIT子句),若是沒有結合ORDER BY子句,那麼TOP將從其前面的虛擬結果集中按某種順序挑出知足數量的行出來,挑出的這些行是咱們人沒法預料的,因此TOP的結果是隨機的。這裏的"某種順序"並不是有序,例如從上圖的笛卡爾積中選一行時,因爲笛卡爾積是無序的,挑選的這一行將受表A的物理存儲順序、表B的物理存儲順序影響。

只有使用了ORDER BY子句,才能保證TOP的結果是可預料而非隨機的,由於ORDER BY的虛擬結果集是有序的。事實上,ORDER BY的結果不該該叫"集",而應該叫"遊標對象",由於排序後的結果中,每一行都按照咱們期待的順序固定好了位置,以後TOP再去操做這樣的結果就必定能獲得咱們預料之中的數據。

6.爲何老是強調"無序"

經過前面的內容,咱們已經發現無序性的最大問題在於返回結果的沒法預測性。返回結果沒法預測,意味着數據增、刪、改的時候存在危險,意味着咱們可能對檢索的數據認知不足。

7.何時的數據是有序的?

前面說了一大堆,總結一下就是:數據都是無序的,每一步的檢索都有隨機性,沒法保證能達到咱們人的期待。可是,關係型數據庫中的全部數據都是無序,都是隨機的嗎?換句話說,上面的總結對嗎?答案是不對。

在關係型數據庫中,咱們除了考慮從存儲引擎到磁盤這段路程(受物理存儲順序影響),還要考慮語句執行過程當中內存中的虛擬表。在前面,咱們說虛擬表是無序的,這句話不許確。

一方面,數據加載成功後,它們在內存中已是有序的,但對咱們人來講,咱們沒法看到這樣的"序",也就是說結果是隨機的,是咱們沒法預料的。

另外一方面,當有排序算法對虛擬表進行排序後,結果也是有序的,這樣的結果是符合咱們人所預料的。對於排序後的結果,ANSI將其稱之爲"遊標對象"。

常見的兩個涉及到排序算法的子句是ORDER BY和DISTINCT。此處以外,還有遊標自身,它的結果也是有序的。

對於ORDER BY子句,它會將它前面的虛擬結果集進行排序,排序的結果集中,每行記錄都固定好了位置,咱們能夠預料到任何一行數據處於哪一個位置,也知道它前面是什麼數據,後面是什麼數據。也就是說,排序後的數據是固定而非隨機的。

對於DISTINCT子句,在對其前面的虛擬結果集進行去重操做時,DISTINCT總會帶有排序操做(即便沒有指定order by,內部也自帶排序),排序以後再對結果集進行去重。例以下面的表。

id   name
---- -----
 5     e
 2     b
 4     d
 1     a
 2     x
 3     c

對id列去重時,它將對id列進行排序,獲得的結果將是:

id
----
 1
 2
 3
 4
 5

結果是有序的。但對於id=2的記錄來講,在去重時應該保留name=b的仍是name=x的記錄呢?沒法保證,由於DISTINCT只對id列排序,不對id以外的列排序。所以對於有重複值的記錄,DISTINCT只能返回一個隨機記錄,咱們沒法預料這個記錄是否是咱們想要的結果。

在SQL Server和Oracle中這沒什麼問題,由於使用DISTINCT後,它後面的過程不容許使用非DISTINCT列(DISTINCT後面還有ORDER BY和TOP,但涉及到列的只有ORDER BY子句),所以最終獲得的結果只有指定的去重列(id列)。可是在MySQL/MariaDB中,ORDER BY容許使用非DISTINCT列,例如select distinct id from t1 order by name,它將先按name排序,再按id排序,最後對id去重。也許你發現了,這種狀況下DISTINCT是在ORDER BY以後才執行的,沒錯,事實就是如此。

8.索引的"序"

不管是MySQL仍是SQL Server(Oracle的知識都忘光了,因此不說它了),均可以建立"彙集索引"和"非彙集索引"(MySQL中沒有這種稱呼,但它的主鍵索引就是彙集索引,非主鍵索引就是非彙集索引,非彙集索引有時候也稱之爲secondary index)。它們都是B+樹的組織結構。

對於彙集索引,B+樹的葉級頁包含了全部數據行以及全部列(有些時候還包括一個或兩個額外的列),它們是排過序的。可是這種排序是經過雙鏈表和指針的方式實現的。

例如,表中有數據行1,2,3,4,5,6,假設這幾行數據都較大,每兩行佔用一個數據頁面,那麼存儲數據的數據頁總共須要3頁(假設分別稱爲A、B、C頁)。

這3頁之間經過雙鏈表的方式組織起來,例如B頁的page header中記錄了它前一頁是A,後一頁是C,C頁的page header中記錄了它的前一頁是B,沒有後一頁(用0表示)。

而對於頁面內的數據,則是經過slot槽位偏移指針來組織的。在前面的"物理存儲順序"一節中已經說明了這個葉級頁是如何每行數據的。

不管是彙集索引仍是非彙集索引,都必需要有一列或多列能惟一識別每一行記錄。能夠經過同時建立惟一性索引的方式實現,在沒有建立惟一性索引時,系統內部會自動添加一個列,幫助索引列惟一識別每一行記錄,只不過當沒有重複值的時候,這一列佔用的空間未0。

總之,有了索引,不管是彙集仍是非彙集索引,都必定能保證每一行數據都惟一,每一行數據在排序時都有徹底肯定的位置。也就是說,當咱們走索引去檢索數據的時候,數據再也不無序,返回的結果也總能預料到。

相關文章
相關標籤/搜索