《SQL 反模式》 學習筆記

第一章 引言


GoF 所著的的《設計模式》,在軟件領域引入了「設計模式」(design pattern)的概念。html

然後,Andrew Koenig 在 1995 年造了 反模式(anti-pattern) (又稱反面模式)這個詞,靈感來自於 GoF 所著的的《設計模式》。前端

反模式指的是在實踐中常常出現但又低效或是有待優化的設計模式,是用來解決問題的帶有共同性的不良方法。它們已經通過研究並分類,以防止往後重蹈覆轍,並能在研發還沒有投產的系統時辨認出來。mysql

因此,反模式是特殊的設計模式,而這種設計模式是欠妥的,起到了反效果。程序員

但有的時候,出於權衡考量,也會使用反模式。正則表達式

例如數據庫的結構中使用的反規範化設計。sql

下面的每一章,都會列舉一種特定場景下的反模式,而後再給出避免使用反模式的建議。數據庫

有個別章節,我略去了反模式,直接寫解決方案了。編程

第二章 亂穿馬路


假設有 Product 和 Account 兩個實體。設計模式

一、一對一關係

假設:Product 只有一個 Account(即 Account 也只有一個 Product)。數組

方案:只用一張表,用兩個字段(Product + Account)關聯便可。

如無必要,就別用多個表,這會增長複雜度(除非考慮將來的拓展性等其餘狀況)。

二、一對多關係

假設:Product 能夠有多個 Account。

方案1:兩張表,一個 Product 表,一個 ProductAccount 表,此表存 ProductId + AccountName。

ProductAccount 表稱之爲從屬表

方案2:只用一張表,即 Product 表,而後此表有個 Account 字段,存以逗號分隔的 AccountName。

此爲反模式,不推薦使用。

方案3:還有一種拓展性更好的、也是本人工做中更經常使用的作法,直接用下面 」三、多對多關係「 的方案 。

三、多對多關係

假設:Product 能夠有多個 Account,Account 也能夠有多個 Product。

方案:用三張表,一個 Product 表,一個 Account 表,一個 ProductAccount 表,此表存 ProductId + AccountId。

ProductAccount 表稱之爲交叉表

第三章 單純的樹


一、需求

創建一張表,存放(帖子的)評論(可嵌套回覆評論)。

二、方案1:鄰接表

添加 parent_id 列,指向同一張表的id。

這樣的設計叫作鄰接表。這多是程序員們用來存儲分層結構數據中最普通的方案了。

缺點:

  • 查詢一個節點的全部後代很複雜
  • 從一棵樹中刪除一個節點會變得比較複雜。若是須要刪除一棵子樹,你不得不執行屢次查詢來找到全部的後代節點(其實這點跟上一個點實質同樣),而後逐個從最低級別開始刪除這些節點以知足外鍵完整性。

[拓展]

某些品牌的數據庫管理系統提供擴展的 SQL 語句,來支持在鄰接表中存儲分層數據結構。

  • SQL-99 標準定義了遞歸查詢的表達式規範,使用 WITH 關鍵字加上公共表表達式。
  • oracle 可使用層次化查詢 connect by 遍歷表數據。
  • postgreSQL 數據庫中,咱們使用 RECURSIVE 參數配合 with 查詢來實現遍歷。若是安裝了 tablefunc 擴展,也可使用 PG 版本的 connectby 函數。這個沒有Oracle那麼強大,可是能夠知足基本要求。
  • mysql 暫不支持。

這裏的遞歸查詢暫時不深究,待寫。

三、方案2:路徑枚舉

創建一個 path 字段,存路徑,如1/4/6/7/

缺點:

  • 數據庫不能確保路徑的格式老是正確或者路徑中的節點確實存在。依賴於應用程序的邏輯代碼來維護路徑的字符串,而且驗證字符串的正確性的開銷很大。
  • 不管將 VARCHAR 的長度設定爲多大,依舊存在長度限制,於是並不可以支持樹結構的無限擴展。

    能夠用 PG 的 text 類型,最高支持存儲 1G 的字符串,應該是夠了。

四、方案3:嵌套集

創建 nsleftnsright 字段,存儲子孫節點的相關信息,而不是節點的直接祖先.

每一個節點經過以下的方式肯定 nsleft 和nsright 的值:nsleft 的數值小於該節點全部後代的ID,同時 nsright 的值大於該節點全部後代的ID。這些數字和 comment_id 的值並無任何關聯。

肯定這三個值(nsleft,comment_id,nsrigh)的簡單方法是對樹進行一次深度優先遍歷,在逐層深刻的過程當中依次遞增地分配 nsleft 的值,並在返回時依次遞增地分配 nsright 的值。

最後結果形如:

缺點:
若是簡單快速地查詢是整個程序中最重要的部分,嵌套集是最佳選擇——比操做單獨的節點要方便快捷不少。然而,嵌套集的插入和移動節點是比較複雜的,由於須要從新分配左右值,若是你的應用程序須要頻繁的插入、刪除節點,那麼嵌套集可能並不適合。

五、方案4:閉包表(推薦)

閉包表是解決分級存儲的一個簡單而優雅的解決方案,它記錄了樹中全部節點間的關係,而不只僅只有那些直接的父子關係。

在設計評論系統時,咱們額外建立了一張叫作 TreePaths 的表,它包含兩列:ancestordescendant,每一列都是一個指向評論表的id的外鍵。

TreePaths 表結構以下:

六、總結

設計 查詢子 查詢樹 插入 刪除 引用完整性
鄰接表 1 簡單 困難 簡單 簡單
遞歸查詢 1 簡單 簡單 簡單 簡單
枚舉路徑 1 簡單 簡單 簡單 簡單
嵌套集 1 困難 簡單 困難 困難
閉包表 2 簡單 簡單 簡單 簡單



鄰接表是最方便的設計,而且不少軟件開發者都瞭解它。

若是你使用的數據庫支持W ITH 或者 CONNECT BY PRIOR 的遞歸查詢,那能使得鄰接表的查詢更爲高效。

閉包表是最通用的設計,而且本章所描述的設計中只有它能容許一個節點屬於多棵樹。它要求一張額外的表來存儲關係,使用空間換時間的方案減小操做過程當中由冗餘的計算所形成的消耗。

我以前作過的評論功能,需求都會盡可能簡化,例如弄成扁平化,只能回覆評論一次,即不能評論評論的評論。若是下次鄙人真的要實現這個複雜的評論功能了,關於閉包表的具體設計及操做實現,準備回頭再看原書。

第四章 須要 ID


一、什麼是僞主鍵

在這樣的表中,須要引入一個對於表的域模型無心義的新列來存儲一個僞值。這一列被用做這張表的主鍵,從而經過它來肯定表中的一條記錄。這種類型的主鍵列咱們一般稱其爲僞主鍵或者代理鍵

能夠把 僞主鍵 理解成 僞鍵 或者 主鍵。

二、僞主鍵的做用

  • 確保一張表中的數據不會出現重複行;

    按照關係型數據庫的定義,表裏是不能夠出現重複行的,可是實際中確實會出現,怎麼辦,引入僞鍵就不會重複了。

  • 在查詢中引用單獨的一行記錄;
  • 支持外鍵。

三、各家數據庫產品中的僞主鍵

僞主鍵直到 SQL:2003 才成爲一個標準,於是每一個數據庫都使用本身特有的 SQL 擴展來實現僞主鍵,甚至不一樣數據庫中對於僞主鍵都有不一樣的名稱(不一樣的表述),以下表:

名稱 數據庫
AUTO_INCREMENT MySQL
GENERATOR Firebird, InterBase
IDENTITY DB2 Derby, Microsoft SQL Server, Sybase
ROWID SQLite
SEQUENCE DB2 Firebird, Informix, Ingres, Oracle, PostgreSQL
SERIAL MySQL, PostgreSQL

雖然各家數據庫產品的僞主鍵叫法不一樣,可是給僞主鍵指派的列名,確是出奇的一致,那就是 id

第五章 不用鑰匙的入口


一、反模式 —— 不用外鍵

有時你被迫使用不支持外鍵約束的數據庫產品(好比 MySQL 的 MyISAM 存儲引擎,或者比 SQLite 3.6.19 早的版本)。

若是是這種狀況,那你不得不使用別的方法來彌補。

二、推薦:使用外鍵

外鍵的好處:

  • 自動維持引用完整性(不然須要本身寫監控腳本)
  • 級聯更新/刪除(不然須要本身寫邏輯代碼)

總結來看就是:避免編寫沒必要要的代碼,節省了大量開發、調試以及維護時間

軟件行業中每千行代碼的平均缺陷數約爲 15~50 個。在其餘條件相同的狀況下,越少的代碼,意味着越少的缺陷。

外鍵的缺點:

  • 須要多一點額外的系統開銷。

但這是值得的。

第六章 實體-屬性-值


一、需求

表支持可變(可拓展)屬性(列)。

例如:你有一個 Prodcut 表,記錄了兩種類型的產品:

  • 產品1:product_type = "電影",此外還有 product_name、total_duration(總時長) 屬性。
  • 產品2:product_type = "圖書",此外還有 product_name、total_page(總頁數) 屬性。

二、反模式

對於某些程序員來講,當他們須要支持可變屬性時,第一反應即是建立另外一張表,將屬性當成行來存儲。

這樣的設計稱爲實體—屬性—值,簡稱EAV。有時也稱之爲:開放架構、無模式或者名—值對。

例如:

  • Prodcut 表:id
  • ProdcutAttr 表:id、product_id、attr_name、attr_value

    ProdcutAttr 表數據形如:

    • (1,1, "product_type", "電影")
    • (2,1, "product_name", "阿甘正傳")
    • (3,1, "total_duration", 120)
    • (4,2, "product_type", "圖書")
    • (5,2, "product_name", "簡愛")
    • (6,2, "total_page", 300)

三、推薦

(1)單表繼承

最簡單的設計是將全部相關的類型都存在一張表中,爲全部類型的全部屬性都保留一列。同時,使用一個屬性來定義每一行表示的子類型。在這個例子中,這個屬性稱做issue_type

對於全部的子類型來講,既有一些公共屬性,但同時又有一些子類型特有屬性。這些子類型特有屬性列必須支持空值,由於根據子類型的不一樣,有些屬性並不須要填寫,從而對於一條記錄來講,那些非空的項會變得比較零散。

例如:

  • Prodcut 表:id、product_type、product_name、total_duration、total_page

    Prodcut 表數據形如:

    • (1,"電影", "阿甘正傳", 120, NULL)
    • (2,"圖書", "簡愛", NULL, 300)

缺點:

  • 沒有任何的元信息來記錄哪一個屬性屬於哪一個子類型

適用場景:

  • 當數據的子類型不多,以及子類型特殊屬性不多
  • 使用 Active Record 模式來訪問單表數據庫時
(2)實體表繼承

爲每一個子類型建立一張獨立的表。每一個表包含那些屬於基類的共有屬性,同時也包含子類型特殊化的屬性。

例如:

  • ProdcutMovie 表:id、product_name、total_duration
  • ProdcutBook 表:id、product_name、total_page

缺點:

  • 很難將通用屬性和子類特有的屬性區分開來

    能夠建立一個視圖聯合這些表,僅選擇公共的列。

  • 若是將一個新的屬性增長到通用屬性中,必須爲每一個子類表都加一遍。
(3)類表繼承

此種方法模擬了繼承,把表當成面向對象裏的類。建立一張基類表,包含全部子類型的公共屬性。對於每一個子類型,建立一個獨立的表,經過外鍵和基類表相連。

這裏須要用到數據庫產品自帶的表繼承功能。

例如:

  • Prodcut 表:id、product_name
  • ProdcutMovie 表:id、total_duration
  • ProdcutBook 表:id、total_page
(4)半結構化數據模型

使用一個BLOB 列來存儲數據,用 XML 或者 JSON 格式——同時包含了屬性的名字和值。Martin Fowler 稱這個模式爲:序列化大對象塊(Serialized BLOB)

優勢:優異的擴展性

缺點:就是在這樣的一個結構中,SQL 基本上沒有辦法獲取某個指定的屬性。你不能在一行blob 字段中簡單地選擇一個獨立的屬性,並對其進行限制、聚合運算、排序等其餘操做。你必須獲取整個blob 字段結構並經過程序去解碼而且解釋這些屬性。

但如今的數據庫,例如 PG,能夠直接支持使用 JSON(B) or XML 的數據類型。因此不會存在必須整個獲取再解析的麻煩了。

第七章 多態關聯


一、需求

怎麼聲明一個指向多張表的外鍵?

例如,Comments 表的外鍵(issue_id)要引用 Bugs 表 or FeatureRequests 表。形如(這種寫法是無效的):

FOREIGN KEY  (issue id)
REFERENCES Bugs (issue_id) OR FeatureRequests (issue_id)

二、反模式

有一個解決方案已經流行到足以正式命名了,那就是:多態關聯。有時候也叫作雜亂關聯。

例如:
除了 Comments 表 issue_id 這個外鍵以外,你必須再添加一列:issue_type,這個額外的列記錄了當前行所引用的表名,取值範圍是 "Bugs" / "FeatureRequests"。

缺點:沒有任何保障數據完整性的手段來確保 Comments.issue_id 中的值在其父表中存在。

當你使用一個面向對象的框架(諸如Hibernate)時,多態關聯彷佛是不可避免的。這種類型的框架經過良好的邏輯封裝來減小使用多態關聯的風險(即依賴上層程序代碼而不是數據庫的元數據)。若是你選擇了一個成熟、有信譽的框架,那能夠相信框架的做者已經完整地實現了相關的邏輯代碼,不會形成錯誤。

三、推薦

(1)交叉表

把 Comments 表向下拆分,分出兩個多的交叉表,即 BugsCommentsFeatureRequestsComments

(2)共用的超級表

基於 Bugs 表和 FeatureRequests 表,建立共用的超級表:Issues

第八章 多列屬性


一、反模式 —— 可拓展的列

例如: 有一個 Bug 表,每一個 Bug 自身可能會有多個 tag。

CREATE TABLE Bug
bug_id SERIAL PRIMARY KEY
description VARCHAR (1000)
tagl VARCHAR (20)
tag2 VARCHAR (20)
tag3 VARCHAR (20)

每次要修改 Bug 自身的最大 tag 數,會動表結構,可拓展性不好。

二、推薦

在原有 Bug 表的基礎上,再建立一個 BugTag 表。包含下面幾列:

  • bug_id
  • tag

第九章 元數據分裂


一、反模式

用形如 Crevenue200二、Crevenue200三、Crevenue2004 的多列,來記錄銷售額。

這裏的問題在於部分數據存在於列名中,即混淆了元數據和數據

還有一種常見的反模式是,將數據(年份)追加在基本表名以後。

二、推薦

若是是由於同一張表數據量太多致使這種反模式,建議:

(1)水平分區(or 分片)

你僅須要定義一些規則來拆分一張邏輯表,數據庫會爲你管理餘下的全部事情。物理上來講,表的確是被拆分了,但你依舊能夠像查詢單一表那樣執行SQL 查詢語句。

分區在 SQL 標準中並無定義,所以每一個不一樣的數據庫實現這一功能的方式都是非標準的。

(2)垂直分區(or 分片)

鑑於水平分區是根據行來對錶進行拆分的,垂直分區就是根據列來對錶進行拆分

好比說,會在Products 表中爲每一個單獨的產品存儲一份安裝文件。這種文件一般都很大,但BLOB 類型的列能夠存儲龐大的二進制數據。若是你有使用通配符「*」進行查詢的習慣,那麼將如此大的文件存儲在Products 表中,並且又不常用,很容易就會在查詢時遺漏這一點,從而形成沒必要要的性能問題。

正確的作法是將BLOB 列存在另外一張表中,和Products 表分離但又與其相關聯。

(3)建立關聯表

把列轉爲行。

第十章 取整錯誤


一、爲何

關於計算機二進制浮點數表示法致使的精度丟失和取整錯誤,能夠看我這一篇:《關於 JavaScript 的 精度丟失 與 近似舍入》,原理是同樣的。

二、怎麼辦

解決方案:使用 SQL 中的 NUMERICDECIMAL 類型來代替 FLOAT 及與其相似的數據類型進行固定精度的小數存儲。

哪怕不是存小數而是存整數,也不要用 FLOAT!一樣會存在錯誤隱患。

第十一章 每日新花樣


需求:限定列的有效值

一、反模式

一、CHECK 約束

缺點:

  • 添刪有效值不方便,須要從新 drop 並 create 約束。
  • 取列的有效值的 list 很麻煩,且不可複用。

二、域

缺點:屬於數據庫高級操做,殺雞焉用牛刀。不贅述了。


三、用戶自定義類型(UDT)

缺點:屬於數據庫高級操做,殺雞焉用牛刀。不贅述了。

二、推薦

一、使用枚舉類型 ENUM

優勢:

  • 添刪有效值很方便
  • 可複用。能夠把有效值寫在應用代碼中,結合 ORM,便可以 for 數據庫,也能夠 for 前端顯示(例如 展現在 select 組件)

二、建立一個單獨的表,存列的有效值,其餘表使用外鍵引用

優勢:

  • 上面 ENUM 的優勢都有。
  • 更加靈活、拓展性更強。

第十二章 幽靈文件


原始圖片文件能夠以二進制格式存儲在 BLOB 類型中,就像以前咱們存儲超長字段那樣。

然而,不少人選擇將圖片存儲在文件系統中,而後在數據庫裏用 VARCHAR 類型來記錄對應的路徑。這實際上是一種反模式

具體要不要用這種反模式,見仁見智,要按照具體使用場景來判斷。

如今廣泛仍是流行這種反模式,例如我司,由於靜態資源都是上傳到 OSS 託管,有 CDN 加成。

第十三章 亂用索引


第十四章 對未知的恐懼


一、需求:如何篩選出兩個列值不相等的行?

假設:咱們有 test 表:

id left right
1 111 222
2 333 333
3 444 NULL
4 NULL 555
5 NULL NULL

正確結果是 id 爲 一、三、4 的行。

二、反模式

錯誤方法:直接使用 where "left" != "right" ,但 != 對 NULL 無效。

正以下面這個例子:

select 1 != 1; #f
select 1 != 2; #t
select 1 != NULL; #null(不是咱們想要的結果,應該返回 t)
select NULL != NULL; #null(不是咱們想要的結果,應該返回 f)

後兩種狀況結果爲 NULL,是由於 sql 是三值邏輯而不是二值邏輯,具體能夠看我以前的一篇:《SQL基礎教程》+《SQL進階教程》學習筆記,裏面有詳細介紹。

三、推薦

(1)將 NULL 視爲特殊值

將 NULL 視爲特殊值,額外用 IS ( NOT ) NULL 判斷:where "left" != "right" or ( "left" is null and "right" is not null ) or ( "left" is not null and "right" is null )

這種寫法很累贅。

(2)IS ( NOT ) DISTINCT FROM

直接用 IS DISTINCT FROM,即:where "left" IS DISTINCT FROM "right" ,不須要額外對 NULL 判斷。


IS ( NOT ) DISTINCT FROM 的支持狀況:

每一個數據庫對 IS ( NOT ) DISTINCT FROM 的支持是不一樣的。PostgreSQL、IBM DB2 和 Firebird 直接支持它,Oracle 和 Microsoft SQL Server 暫時還不支持。MySQL 提供了一個專有的表達式 <=>,它的工做邏輯和 IS NOT DISTINCT FROM 一致。

第十五章 模棱兩可的分組


一、反模式

例如,有 test 表:

id type name join_time
1 老師 趙老師 2020-01-01
2 老師 錢老師 2020-01-02
3 同窗 張三 2020-01-03
4 同窗 李四 2020-01-04
5 同窗 王五 2020-01-05

需求:咱們須要在 老師 or 同窗 分別裏找出 join_time 最先的一條記錄。

執行 SELECT "type", MIN("join_time"), "name" FROM "test" GROUP BY "type"

name 列就是有歧義的列,可能包含不可預測的和不可靠的數據:

  • 在 MySQL 中,返回的值是這一組結果中的第一條記錄。
  • 在 Postgres 中,會報錯。

二、推薦

解決方案:無歧義地使用列。

(1)只查詢功能依賴的列

最直接的解決方案就是將有歧義的列排除出查詢。

執行 SELECT "type" FROM "test" GROUP BY "type"

但這知足不了咱們的需求,pass。

(2)對額外的列使用聚合函數

執行 SELECT "type", MIN("join_time"), MIN("name") FROM "test" GROUP BY "type"

若是不能保證 MIN("join_time")MIN("name") 是指向同一行,那這個寫法就是錯的。有風險,pass。

(3)使用關聯子查詢
SELECT * FROM test as t1
WHERE NOT EXISTS 
(
	SELECT * FROM test as t2
	WHERE t1."type" = t2."type" and t1.join_time > t2.join_time 
)

缺點:性能很差。


[拓展] 用關聯子查詢寫出來的思路:

涉及 SQL 基礎的全程量化和存在量化的知識點,詳細可參考個人舊文:《SQL基礎教程》+《SQL進階教程》學習筆記

若是需求變成:咱們須要在 老師 or 同窗 分別裏找出 join_time 最晚的一條記錄,那隻須要把 t1.join_time > t2.join_time 變成 t1.join_time < t2.join_time 便可:

SELECT * FROM test as t1
WHERE NOT EXISTS 
(
	SELECT * FROM test as t2
	WHERE t1."type" = t2."type" and t1.join_time > t2.join_time 
)
(4)使用衍生表 JOIN
SELECT * FROM test as t1 
INNER JOIN
(
	SELECT "type", MIN("join_time") as "join_time" FROM test  
	GROUP BY "type" 
) as t2 
ON t1.join_time = t2.join_time

缺點:性能很差。


若是需求變成:咱們須要在 老師 or 同窗 分別裏找出 join_time 最晚的一條記錄,那隻須要把 MIN("join_time") 變成 MAX("join_time") 便可:

SELECT * FROM test as t1 
INNER JOIN
(
	SELECT "type", MAX("join_time") as "join_time" FROM test  
	GROUP BY "type" 
) as t2 
ON t1.join_time = t2.join_time
(5)直接使用 LEFT JOIN
SELECT * FROM test as t1 
LEFT JOIN test as t2
ON t1."type" = t2."type" 
AND  
(
		t1.join_time > t2.join_time 
)
WHERE t2."id" IS NULL

解釋:t1.join_time > t2.join_time 搭配 WHERE t2."id" IS NULL 是利用 LEFT JOIN 的特性,即若是找到匹配行則能夠生成多行,但若找不到匹配行,則另外一邊置 NUll。

缺點:性能稍好,可是較難維護。


若是需求變成:咱們須要在 老師 or 同窗 分別裏找出 join_time 最晚的一條記錄,那隻須要把 t1.join_time > t2.join_time 變成 t1.join_time < t2.join_time 便可:

SELECT * FROM test as t1 
LEFT JOIN test as t2
ON t1."type" = t2."type" 
AND  
(
		t1.join_time < t2.join_time 
)
WHERE t2."id" IS NULL
(6)窗口函數
SELECT
	* 
FROM
	(
	SELECT
		*,
		RANK() OVER ( PARTITION BY "type" ORDER BY "join_time" ASC ) AS "rank" 
	FROM
	  test  
	) as t1
WHERE
	"t1"."rank" = 1

關於更多窗口函數的介紹,可看個人舊文:《SQL基礎教程》+《SQL進階教程》學習筆記


若是需求變成:咱們須要在 老師 or 同窗 分別裏找出 join_time 最晚的一條記錄,那隻須要把 ASC 變成 DESC 便可:

SELECT
	* 
FROM
	(
	SELECT
		*,
		RANK() OVER ( PARTITION BY "type" ORDER BY "join_time" DESC ) AS "rank" 
	FROM
	  test  
	) as t1
WHERE
	"t1"."rank" = 1

第十六章 隨機選擇


相比於將整個數據集讀入程序中再取出樣例數據集,直接經過數據庫查詢拿出這些樣例數據集會更好。

本章的目標就是要寫出一個僅返回隨機數據樣本的高效 SQL 查詢。

一、傳統方法、random()

SELECT * FROM test ORDER BY random() limit 1

缺點:

  • 整個排序過程沒法利用索引
  • 性能很差。好不容易對整個數據集完成排序,但絕大多數的結果都浪費了,由於除了返回第一行以外,其餘結果都馬上被丟棄了。

二、推薦方法一、從 1 到最大值之間隨機選擇

一種避免對全部數據進行排序的方法,就是在 1 到最大的主鍵值之間隨機選擇一個。

但要考慮 1 到最大值之間有縫隙的狀況。

利用 JOIN

SELECT
	t1.* 
FROM
	test AS t1
	JOIN ( SELECT CEIL( random() * ( SELECT MAX ( "id" ) FROM test ) ) AS "id" ) AS t2
ON
	t1."id" >= t2."id"
ORDER BY
	t1."id" 
LIMIT 1

三、推薦方法二、使用偏移量選擇隨機行

計算總的數據行數,隨機選擇0 到總行數之間的一個值,而後用這個值做爲位移來獲取隨機行。

利用 OFFSET

SELECT
	* 
FROM
	test 
	LIMIT 1 OFFSET ( 
		SELECT CEIL( 
			random() * ( SELECT COUNT ( * ) FROM test )
		) - 1 
	)

四、推薦方法三、專有解決方案

每種數據庫均可能針對這個需求提供獨有的解決方案:

-- Microsoft SQL Server 2005 增長了一個 TABLE-SAMPLE 子句。
-- Oracle 使用了一個相似的 SAMPLE 子句,好比返回表中1%的記錄。
-- Postgres 也有相似的叫 TABLESAMPLE

可是這種採樣的方法返回結果的行數很不穩定,感受仍是不推薦了。

第十七章 可憐人的搜索引擎


一、需求

全文搜索

二、反模式

使用 LIKE 或者正則表達式進行模式匹配搜索。

缺點:使用模式匹配操做符的最大缺點就在於性能問題。它們沒法從傳統的索引上受益,所以必須進行全表遍歷。

三、推薦

解決方案:使用正確的工具。

(1)數據庫擴展

每一個大品牌的數據庫都有對全文搜索這個需求的解決方案。

例如,PostgreSQL 8.3 提供了一個複雜的可大量配置的方式,來將文本轉化爲可搜索的詞聚集合,而且讓這些文檔可以進行模式匹配搜索。即,爲了最大地提高性能,你須要將內容存兩份:一份爲原始文本格式,另外一份爲特殊的 TSVECTOR 類型的可搜索格式。

空間換時間。

① 步驟:

建表時建立 TSVECTOR 數據類型的列。

② 步驟:

你須要確保 TSVECTOR 列的內容和你所想要搜索的列的內容同步。PostgreSQL 提供了一個內置的觸發器來簡化這一操做。

觸發器寫法略,可看原書。

③ 步驟:

你也應該同時在 TSVECTOR 列上建立一個反向索引(GIN)

寫法略,可看原書。

④ 步驟:

在作完這一切以後,就能夠在全文索引的幫助下使用PostgreSQL 的文本搜索操做符@@來高效地執行搜索查詢。

寫法略,可看原書。

(2)本身實現 反向索引

太複雜,略。

(3)第三方搜索引擎

你沒必要使用 SQL 來解決全部問題。

兩個產品:Sphinx SearchApache Lucene

使用略,可看原書。

第十八章 意大利麪條式查詢


一、反模式

一條精心設計的複雜 SQL 查詢,相比於那些直接簡單的查詢來講,不得不使用不少的JOIN、關聯子查詢和其餘讓 SQL 引擎難以優化和快速執行的操做符。而程序員直覺地認爲越少的SQL 執行次數性能越好。

二、推薦

目標:減小 SQL 查詢數量

解決方案:

  • 分而治之,一步一個腳印
  • 你能夠將幾個查詢的結果進行 UNION 操做,從而最終獲得一個結果集

好處:

  • 性能更好
  • 便於開發、維護

第十九章 隱式的列


一、反模式

我所遇到的程序員使用SQL通配符時問得最多的問題是:「有沒有選擇除了幾個我不想要的列以外全部列的方法?

答案是「沒有」。

其實我仍是但願數據庫廠商能加上,如今網上有不少 hack 的方法,需求畢竟是在的。誒。

二、推薦

解決方案:明確列出列名,而不是使用通配符或者隱式列的列表。

第二十章 明文密碼


能夠參考我以前的文章:《數據庫裏帳號的密碼,須要怎樣安全的存放?—— 密碼哈希(Password Hash)》

第二十一章 SQL 注入


一、需求

防止 SQL 注入

二、反模式

(1)轉義

好比,在PHP 的 PDO 擴展中,可使用一個 quote()函數來定義一個包含引號的字符串或者還原一個字符串中的引號字符。

三、推薦

解決方案:不信任任何人。

(1)check 數據
  • 過濾輸入內容.好比在 PHP 中,可使用filter 擴展

    Node.js 的 joi 庫。

  • 正則表達式來匹配安全的子串

    用上一條的過濾庫也能夠實現。

  • 用類型轉換函數
(2)參數化動態內容

你應該使用查詢參數將其和 SQL 表達式分離。

沒有哪一種 SQL 注入的攻擊可以改變一個參數化了的查詢的語法結構。

缺點:

① 會影響優化器的效果,最終影響性能

好比說,假設在 Accounts 表中有一個 is_active 列。這一列中99%的記錄都是真實值。對 is_active = false 的查詢會得益於這一列上的索引,但對於 is_active = true 的查詢卻會在讀取索引的過程當中浪費不少時間。然而,若是你用了一個參數 is_active = ? 來構造這個表達式,優化器不知道在預處理這條語句的時候你最終會傳入哪一個值,所以頗有可能就選擇了錯誤的優化方案。

要規避這樣的問題,直接將變量內容插入到SQL 語句中會是更好的方法,不要去理會查詢參數。一旦你決定這麼作了,就必定要當心地引用字符串。

能夠結合下面的 」(3)將用戶與代碼隔離「 一塊兒使用。


② 這還不是一個通用的解決方案,由於查詢參數總被視爲是一個字面值

例如:

  • 多個值的列表不能夠當成單一參數: 例如 in(x,x,x)

    解決方案:使用了一些 PHP 內置的數組函數來生成一個佔位符數組。

  • 表名、列名、SQL 關鍵字 沒法做爲參數。

    解決方案:能夠用存儲過程;或者經過事先在應用邏輯代碼裏,先用字符串拼接的方式生成好 sql 代碼。


③ 很差調試

這意味着,若是你獲取到一個預先準備好的SQL 查詢語句,它裏面是不會包含任何實際的參數值的。當你調試或者記錄查詢時,很方便就能看到帶有參數值的SQL 語句,但這些值永遠不會以可讀的SQL 形式整合到查詢中去。

解決方案:調試動態化SQL 語句的最好方法,就是將準備階段的帶有佔位符的查詢語句和執行階段傳入的參數都記錄下來。(本身動手,豐衣足食。)

(3)數據訪問框架

你可能看過數據訪問框架的擁護者聲稱他們的庫可以抵禦全部SQL 注入的攻擊。對於全部容許你使用字符串方式傳入SQL 語句的框架來講,這都是扯淡。

沒有任何框架能強制你寫出安全的 SQL 代碼。一個框架可能會提供一系列簡單的函數來幫助你,但很容易就能繞開這些函數,而後使用一般的修改字符串的辦法來編寫不安全的SQL語句。

就是你得保證本身寫的是符合框架規範的寫法,否則人爲因素仍是會致使出錯。

(4)存儲過程

(5)將用戶與代碼隔離

將請求的參數做爲索引值去查找預先定義好的值,而後用這些預先定義好的值來組織 SQL 查詢語句。

例如把請求的參數通過 if else ,來分配進預先定義好的 SQL 查詢語句。

(6)找個可靠的人來幫你審查代碼

找到瑕疵的最好方法就是再找一雙眼睛一塊兒來盯着看。

有條件能夠結對編程。

第二十二章 僞鍵潔癖


一、反模式

不能忍受主鍵中間出現不連續的缺位。

重用主鍵並非一個好主意,由於斷檔每每是因爲一些合理的刪除或者回滾數據所形成的。

二、推薦

(1)克服內心障礙

它們不必定非得是連續值才能用來標記行。

將僞鍵當作行的惟一性標識,但它們不是行號。別把主鍵值和行號混爲一談。


問:怎麼抵擋一個但願清理數據庫中僞鍵斷檔的老闆的請求?
答:這是一個溝通方面的問題,而不是技術問題

(2)使用GUID

GUID (Globally Unique Identifier 全局惟一標識符)是一個128 位的僞隨機數(一般使用32 個十六進制字符表示)。

GUID 也稱 UUID (Universally unique identifier 通用惟一標識符)。

GUID 相比傳統的僞鍵生成方法來講,至少有以下兩個優點:

  • 能夠在多個數據庫服務器上併發地生成僞鍵,而不用擔憂生成一樣的值。
  • 沒有人會再抱怨有斷檔——他們會忙於抱怨輸入32 個十六進制字符作主鍵。

第二十三章 非禮勿視


一、反模式 —— 忽略錯誤處理

「我不會讓錯誤處理弄亂了個人代碼結構的。」

致使問題:

  • 代碼健壯性很差
  • 出現錯誤很差回溯
  • 用戶體驗差(用戶看不見代碼,他們只能看見輸出。當一個致命錯誤沒有被處理時,用戶就只能看到一
    個白屏,或者是一個不完整的異常信息。)

二、推薦 —— 優雅地從錯誤中恢復

一些計算機科學家推測在一個穩固的程序中,至少有50%的代碼是用來進行錯誤處理的

全部喜歡跳舞的人都知道,跳錯舞步是不可避免的。優雅的祕訣就是弄明白怎麼挽回。給本身一個瞭解錯誤產生緣由的機會,而後就能夠快速響應,在任何人注意到你出醜以前,神不知鬼不覺地回到應有的節奏上。

第二十四章 外交豁免權


一、反模式

技術債務(technical debt),是程序設計及軟件工程中的一個比喻。指開發人員爲了加速軟件開發,在應該採用最佳方案時進行了妥協,改用了短時間內能加速軟件開發的方案,從而在將來給本身帶來的額外開發負擔。這種技術上的選擇,就像一筆債務同樣,雖然眼前看起來能夠獲得好處,但必須在將來償還。軟件工程師必須付出額外的時間和精力持續修復以前的妥協所形成的問題及反作用,或是進行重構,把架構改善爲最佳實現方式。

二、推薦

  • 畫實體關係圖(ER 圖),更復雜一點的ER 圖包含了列、主鍵、索引和其餘數據庫對象。

    還有些工具可以經過SQL 腳本或者運行中的數據庫直接經過反向工程獲得 ER 圖。

  • 寫文檔
  • 源代碼管理
  • 測試

[拓展] 版本管理 之 管理數據庫:

版本管理工具管理了代碼,但並無管理數據庫。Ruby on Rails 提供了一種技術叫作「遷移」,用來將版本控制應用到數據庫實例的升級管理上。

大多數其餘的網站開發框架,包括PHP 的Doctrine、Python 的Django 以及微軟的 ASP.NET,都支持相似於Rails 的「遷移」這樣的特性。

我目前 Node.js 用的 sequelize 就包含了這種數據庫的遷移腳本。

缺點:但它們還不是完美的,只能處理一些簡單類型的結構變動。並且從根本上說,它們在原有版本控制服務以外又創建了一個版本系統。

第二十五章 魔豆


好詞好句


所謂專家,就是在一個很小的領域裏把全部錯誤都犯過了的人。 —— 尼爾斯·玻爾

規範僅僅在它有幫助時纔是好的。

Mitch Ratcliffe 說:「計算機是人類歷史中最容易讓你犯更多錯誤的發明……除了手槍和龍舌蘭以外。」

相關文章
相關標籤/搜索