爲尊重原作者,特標明原産地: https://github.com/XiaoMi/soar/blob/master/doc/heuristic.md
這是小米 soar 的默認啓發槼則滙總,也是 DBA 多年精華總結。熟讀各個案例,對於一般的 MySQL 優化有很高的幫助。
如果你不喜歡太理論的東西,或者沒時間去深入,舉一反三學習也未嘗不可。
啓發式槼則建議
建議使用 AS 關鍵字顯示聲明一個別名
Item: ALI.001
Severity: L0
Content: 在列或表別名 (如 "tbl AS alias") 中,明確使用 AS 關鍵字比隱含別名 (如 "tbl alias") 更易懂。
Case:
select name from tbl t1 where id < 1000
不建議給列通配符 '*' 設置別名
Item: ALI.002
Severity: L8
Content: 例: "SELECT tbl.* col1, col2" 上面這條 SQL 給列通配符設置了別名,這樣的 SQL 可能存在邏輯錯誤。您可能意在查詢 col1, 但是代替它的是重命名的是 tbl 的最後一列。
Case:
select tbl.* as c1,c2,c3 from tbl where id < 1000
別名不要與表或列的名字相同
Item: ALI.003
Severity: L1
Content: 表或列的別名與其真實名稱相同,這樣的別名會使得查詢更難去分辨。
Case:
select name from tbl as tbl where id < 1000
修改表的默認字符集不會改表各個字段的字符集
Item: ALT.001
Severity: L4
Content: 很多初學者會將 ALTER TABLE tbl_name [DEFAULT] CHARACTER SET 'UTF8' 誤認爲會修改所有字段的字符集,但實際上它只會影響後續新增的字段不會改表已有字段的字符集。如果想修改整張表所有字段的字符集建議使用 ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name;
Case:
ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name;
同一張表的多條 ALTER 請求建議合爲一條
Item: ALT.002
Severity: L2
Content: 每次表結構變更對線上服務都會産生影響,即使是能夠通過在線工具進行調整也請盡量通過合並 ALTER 請求的試減少操作次數。
Case:
ALTER TABLE tbl ADD COLUMN col int, ADD INDEX idx_col (`col`);
刪除列爲高危操作,操作前請注意檢查業務邏輯是否還有依賴
Item: ALT.003
Severity: L0
Content: 如業務邏輯依賴未完全消除,列被刪除後可能導致數據無法寫入或無法查詢到已刪除列數據導致程序異常的情況。這種情況下即使通過備份數據回滾也會丟失用戶請求寫入的數據。
Case:
ALTER TABLE tbl DROP COLUMN col;
刪除主鍵和外鍵爲高危操作,操作前請與 DBA 確認影響
Item: ALT.004
Severity: L0
Content: 主鍵和外鍵爲關係型數據庫中兩種重要約束,刪除已有約束會打破已有業務邏輯,操作前請業務開發與 DBA 確認影響,三思而行。
Case:
ALTER TABLE tbl DROP PRIMARY KEY;
不建議使用前項通配符查找
Item: ARG.001
Severity: L4
Content: 例如 "%foo",查詢參數有一個前項通配符的情況無法使用已有索引。
Case:
select c1,c2,c3 from tbl where name like '%foo'
沒有通配符的 LIKE 查詢
Item: ARG.002
Severity: L1
Content: 不包含通配符的 LIKE 查詢可能存在邏輯錯誤,因爲邏輯上它與等值查詢相同。
Case:
select c1,c2,c3 from tbl where name like 'foo'
參數比較包含隱式轉換,無法使用索引
Item: ARG.003
Severity: L4
Content: 隱式類型轉換有無法命中索引的風險,在高並發、大數據量的情況下,命不中索引帶來的後果非常嚴重。
Case:
SELECT * FROM sakila.film WHERE length >= '60';
IN (NULL)/NOT IN (NULL) 永遠非真
Item: ARG.004
Severity: L4
Content: 正確的作法是 col IN ('val1', 'val2', 'val3') OR col IS NULL
Case:
SELECT * FROM tb WHERE col IN (NULL);
IN 要慎用,元素過多會導致全表掃描
Item: ARG.005
Severity: L1
Content: 如:select id from t where num in (1,2,3) 對於連續的數值,能用 BETWEEN 就不要用 IN 了:select id from t where num between 1 and 3。而當 IN 值過多時 MySQL 也可能會進入全表掃描導致性能急劇下降。
Case:
select id from t where num in(1,2,3)
應盡量避免在 WHERE 子句中對字段進行 NULL 值判斷
Item: ARG.006
Severity: L1
Content: 使用 IS NULL 或 IS NOT NULL 將可能導致引擎放棄使用索引而進行全表掃描,如:select id from t where num is null; 可以在 num 上設置默認值 0,確保表中 num 列沒有 NULL 值,然後這樣查詢: select id from t where num=0;
Case:
select id from t where num is null
避免使用模式匹配
Item: ARG.007
Severity: L3
Content: 性能問題是使用模式匹配操作符的最大缺點。使用 LIKE 或正則表達式進行模式匹配進行查詢的另一個問題,是可能會返回意料之外的結果。最好的方案就是使用特殊的搜索引擎技術來替代 SQL,比如 Apache Lucene。另一個可選方案是將結果保存起來從而減少重複的搜索開銷。如果一定要使用 SQL,請考慮在 MySQL 中使用像 FULLTEXT 索引這樣的第三方擴展。但更廣泛地說,您不一定要使用 SQL 來解決所有問題。
Case:
select c_id,c2,c3 from tbl where c2 like 'test%'
OR 查詢索引列時請盡量使用 IN 謂詞
Item: ARG.008
Severity: L1
Content: IN-list 謂詞可以用於索引檢索,並且優化器可以對 IN-list 進行排序,以匹配索引的排序序列,從而獲得更有效的檢索。請注意,IN-list 必須只包含常量,或在查詢塊執行期間保持常量的值,例如外引用。
Case:
SELECT c1,c2,c3 FROM tbl WHERE c1 = 14 OR c1 = 17
引號中的字符串開頭或結尾包含空格
Item: ARG.009
Severity: L1
Content: 如果 VARCHAR 列的前後存在空格將可能引起邏輯問題,如在 MySQL 5.5 中 'a' 和 'a ' 可能會在查詢中被認爲是相同的值。
Case:
SELECT 'abc '
不要使用 hint,如:sql_no_cache, force index, ignore key, straight join 等
Item: ARG.010
Severity: L1
Content: hint 是用來強制 SQL 按照某個執行計劃來執行,但隨著數據量變化我們無法保证自己當初的預判是正確的。
Case:
SELECT * FROM t1 USE INDEX (i1) ORDER BY a;
不要使用負向查詢,如:NOT IN/NOT LIKE
Item: ARG.011
Severity: L3
Content: 請盡量不要使用負向查詢,這將導致全表掃描,對查詢性能影響較大。
Case:
select id from t where num not in(1,2,3);
一次性 INSERT/REPLACE 的數據過多
Item: ARG.012
Severity: L2
Content: 單條 INSERT/REPLACE 語句批量插入大量數據性能較差,甚至可能導致從庫同步延遲。爲了提升性能,減少批量寫入數據對從庫同步延時的影響,建議採用分批次插入的方法。
Case:
INSERT INTO tb (a) VALUES (1), (2)
最外層 SELECT 未指定 WHERE 條件
Item: CLA.001
Severity: L4
Content: SELECT 語句沒有 WHERE 子句,可能檢查比預期更多的行 (全表掃描)。對於 SELECT COUNT (*) 類型的請求如果不要求精度,建議使用 SHOW TABLE STATUS 或 EXPLAIN 替代。
Case:
select id from tbl
不建議使用 ORDER BY RAND ()
Item: CLA.002
Severity: L3
Content: ORDER BY RAND () 是從結果集中檢索隨機行的一種非常低效的方法,因爲它會對整個結果進行排序並丟棄其大部分數據。
Case:
select name from tbl where id < 1000 order by rand(number)
不建議使用帶 OFFSET 的 LIMIT 查詢
Item: CLA.003
Severity: L2
Content: 使用 LIMIT 和 OFFSET 對結果集分頁的複雜度是 O (n^2),並且會隨著數據增大而導致性能問題。採用 “書簽” 掃描的方法實現分頁效率更高。
Case:
select c1,c2 from tbl where name=xx order by number limit 1 offset 20
不建議對常量進行 GROUP BY
Item: CLA.004
Severity: L2
Content: GROUP BY 1 表示按第一列進行 GROUP BY。如果在 GROUP BY 子句中使用數字,而不是表達式或列名稱,當查詢列順序改變時,可能會導致問題。
Case:
select col1,col2 from tbl group by 1
ORDER BY 常數列沒有任何意義
Item: CLA.005
Severity: L2
Content: SQL 邏輯上可能存在錯誤;最多只是一個無用的操作,不會更改查詢結果。
Case:
select id from test where id=1 order by id
在不同的表中 GROUP BY 或 ORDER BY
Item: CLA.006
Severity: L4
Content: 這將強制使用臨時表和 filesort,可能産生巨大性能隱患,並且可能消耗大量内存和磁盤上的臨時空間。
Case:
select tb1.col, tb2.col from tb1, tb2 where id=1 group by tb1.col, tb2.col
ORDER BY 語句對多個不同條件使用不同方向的排序無法使用索引
Item: CLA.007
Severity: L2
Content: ORDER BY 子句中的所有表達式必須按統一的 ASC 或 DESC 方向排序,以便利用索引。
Case:
select c1,c2,c3 from t1 where c1='foo' order by c2 desc, c3 asc
請爲 GROUP BY 顯示添加 ORDER BY 條件
Item: CLA.008
Severity: L2
Content: 默認 MySQL 會對 'GROUP BY col1, col2, ...' 請求按如下順序排序 'ORDER BY col1, col2, ...'。如果 GROUP BY 語句不指定 ORDER BY 條件會導致無謂的排序産生,如果不需要排序建議添加 'ORDER BY NULL'。
Case:
select c1,c2,c3 from t1 where c1='foo' group by c2
ORDER BY 的條件爲表達式
Item: CLA.009
Severity: L2
Content: 當 ORDER BY 條件爲表達式或函數時會使用到臨時表,如果在未指定 WHERE 或 WHERE 條件返回的結果集較大時性能會很差。
Case:
select description from film where title ='ACADEMY DINOSAUR' order by length-language_id;
GROUP BY 的條件爲表達式
Item: CLA.010
Severity: L2
Content: 當 GROUP BY 條件爲表達式或函數時會使用到臨時表,如果在未指定 WHERE 或 WHERE 條件返回的結果集較大時性能會很差。
Case:
select description from film where title ='ACADEMY DINOSAUR' GROUP BY length-language_id;
建議爲表添加注釋
Item: CLA.011
Severity: L1
Content: 爲表添加注釋能夠使得表的意義更明確,從而爲日後的維護帶來極大的便利。
Case:
CREATE TABLE `test1` (`ID` bigint(20) NOT NULL AUTO_INCREMENT,`c1` varchar(128) DEFAULT NULL,PRIMARY KEY (`ID`)) ENGINE=InnoDB DEFAULT CHARSET=utf8
將複雜的裹腳布式查詢分解成幾個簡單的查詢
Item: CLA.012
Severity: L2
Content: SQL 是一門極具表現力的語言,您可以在單個 SQL 查詢或者單條語句中完成很多事情。但這並不意味著必須強制只使用一行代碼,或者認爲使用一行代碼就搞定每個任務是個好主意。通過一個查詢來獲得所有結果的常見後果是得到了一個笛卡兒積。當查詢中的兩張表之間沒有條件限制它們的關係時,就會發生這種情況。沒有對應的限制而直接使用兩張表進行聯結查詢,就會得到第一張表中的每一行和第二張表中的每一行的一個組合。每一個這樣的組合就會成爲結果集中的一行,最終您就會得到一個行數很多的結果集。重要的是要考慮這些查詢很難編寫、難以修改和難以調試。數據庫查詢請求的日益增加應該是預料之中的事。經理們想要更複雜的報告以及在用戶界面上添加更多的字段。如果您的設計很複雜,並且是一個單一查詢,要擴展它們就會很費時費力。不論對您還是項目來說,時間花在這些事情上面不值得。將複雜的意大利面條式查詢分解成幾個簡單的查詢。當您拆分一個複雜的 SQL 查詢時,得到的結果可能是很多類似的查詢,可能僅僅在數據類型上有所不同。編寫所有的這些查詢是很乏味的,因此,最好能夠有個程序自動生成這些代碼。SQL 代碼生成是一個很好的應用。盡管 SQL 支持用一行代碼解決複雜的問題,但也別做不切實際的事情。
Case:
這是一條很長很長的 SQL,案例略。
不建議使用 HAVING 子句
Item: CLA.013
Severity: L3
Content: 將查詢的 HAVING 子句改寫爲 WHERE 中的查詢條件,可以在查詢處理期間使用索引。
Case:
SELECT s.c_id,count(s.c_id) FROM s where c = test GROUP BY s.c_id HAVING s.c_id <> '1660' AND s.c_id <> '2' order by s.c_id
刪除全表時建議使用 TRUNCATE 替代 DELETE
Item: CLA.014
Severity: L2
Content: 刪除全表時建議使用 TRUNCATE 替代 DELETE
Case:
delete from tbl
UPDATE 未指定 WHERE 條件
Item: CLA.015
Severity: L4
Content: UPDATE 不指定 WHERE 條件一般是致命的,請您三思後行
Case:
update tbl set col=1
不要 UPDATE 主鍵
Item: CLA.016
Severity: L2
Content: 主鍵是數據表中記錄的唯一標識符,不建議頻繁更新主鍵列,這將影響元數據統計信息進而影響正常的查詢。
Case:
update tbl set col=1
不建議使用 SELECT * 類型查詢
Item: COL.001
Severity: L1
Content: 當表結構變更時,使用 * 通配符選擇所有列將導致查詢的含義和行爲會發生更改,可能導致查詢返回更多的數據。
Case:
select * from tbl where id=1
INSERT/REPLACE 未指定列名
Item: COL.002
Severity: L2
Content: 當表結構發生變更,如果 INSERT 或 REPLACE 請求不明確指定列名,請求的結果將會與預想的不同;建議使用 “INSERT INTO tbl (col1,col2) VALUES ...” 代替。
Case:
insert into tbl values(1,'name')
建議修改自增 ID 爲無符號類型
Item: COL.003
Severity: L2
Content: 建議修改自增 ID 爲無符號類型
Case:
create table test(`id` int(11) NOT NULL AUTO_INCREMENT)
請爲列添加默認值
Item: COL.004
Severity: L1
Content: 請爲列添加默認值,如果是 ALTER 操作,請不要忘記將原字段的默認值寫上。字段無默認值,當表較大時無法在線變更表結構。
Case:
CREATE TABLE tbl (col int) ENGINE=InnoDB;
列未添加注釋
Item: COL.005
Severity: L1
Content: 建議對表中每個列添加注釋,來明確每個列在表中的含義及作用。
Case:
CREATE TABLE tbl (col int) ENGINE=InnoDB;
表中包含有太多的列
Item: COL.006
Severity: L3
Content: 表中包含有太多的列
Case:
CREATE TABLE tbl ( cols ....);
可使用 VARCHAR 代替 CHAR, VARBINARY 代替 BINARY
Item: COL.008
Severity: L1
Content: 爲首先變長字段存儲空間小,可以節省存儲空間。其次對於查詢來說,在一個相對較小的字段内搜索效率顯然要高些。
Case:
create table t1(id int,name char(20),last_time date)
建議使用精確的數據類型
Item: COL.009
Severity: L2
Content: 實際上,任何使用 FLOAT, REAL 或 DOUBLE PRECISION 數據類型的設計都有可能是反模式。大多數應用程序使用的浮點數的取值範圍並不需要達到 IEEE 754 標準所定義的最大 / 最小區間。在計算總量時,非精確浮點數所積累的影響是嚴重的。使用 SQL 中的 NUMERIC 或 DECIMAL 類型來代替 FLOAT 及其類似的數據類型進行固定精度的小數存儲。這些數據類型精確地根據您定義這一列時指定的精度來存儲數據。盡可能不要使用浮點數。
Case:
CREATE TABLE tab2 (p_id BIGINT UNSIGNED NOT NULL,a_id BIGINT UNSIGNED NOT NULL,hours float not null,PRIMARY KEY (p_id, a_id))
不建議使用 ENUM 數據類型
Item: COL.010
Severity: L2
Content: ENUM 定義了列中值的類型,使用字符串表示 ENUM 裡的值時,實際存儲在列中的數據是這些值在定義時的序數。因此,這列的數據是字節對齊的,當您進行一次排序查詢時,結果是按照實際存儲的序數值排序的,而不是按字符串值的字母順序排序的。這可能不是您所希望的。沒有什麽語法支持從 ENUM 或者 check 約束中添加或刪除一個值;您只能使用一個新的集合重新定義這一列。如果您打算廢棄一個選項,您可能會爲歷史數據而煩惱。作爲一種策略,改變元數據 —— 也就是說,改變表和列的定義 —— 應該是不常見的,並且要注意測試和質量保证。有一個更好的解決方案來約束一列中的可選值:創建一張檢查表,每一行包含一個允許在列中出現的候選值;然後在引用新表的舊表上聲明一個外鍵約束。
Case:
create table tab1(status ENUM('new','in progress','fixed'))
當需要唯一約束時才使用 NULL,僅當列不能有缺失值時才使用 NOT NULL
Item: COL.011
Severity: L0
Content: NULL 和 0 是不同的,10 乘以 NULL 還是 NULL。NULL 和空字符串是不一樣的。將一個字符串和標準 SQL 中的 NULL 聯合起來的結果還是 NULL。NULL 和 FALSE 也是不同的。AND、OR 和 NOT 這三個布爾操作如果涉及 NULL,其結果也讓很多人感到困惑。當您將一列聲明爲 NOT NULL 時,也就是說這列中的每一個值都必須存在且是有意義的。使用 NULL 來表示任意類型不存在的空值。 當您將一列聲明爲 NOT NULL 時,也就是說這列中的每一個值都必須存在且是有意義的。
Case:
select c1,c2,c3 from tbl where c4 is null or c4 <> 1
BLOB 和 TEXT 類型的字段不可設置爲 NULL
Item: COL.012
Severity: L5
Content: BLOB 和 TEXT 類型的字段不可設置爲 NULL
Case:
CREATE TABLE `tbl` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` longblob, PRIMARY KEY (`id`));
TIMESTAMP 類型未設置默認值
Item: COL.013
Severity: L4
Content: TIMESTAMP 類型未設置默認值
Case:
CREATE TABLE tbl( `id` bigint not null, `create_time` timestamp);
爲列指定了字符集
Item: COL.014
Severity: L5
Content: 建議列與表使用同一個字符集,不要單獨指定列的字符集。
Case:
CREATE TABLE `tb2` ( `id` int(11) DEFAULT NULL, `col` char(10) CHARACTER SET utf8 DEFAULT NULL)
BLOB 類型的字段不可指定默認值
Item: COL.015
Severity: L4
Content: BLOB 類型的字段不可指定默認值
Case:
CREATE TABLE `tbl` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` blob NOT NULL DEFAULT '', PRIMARY KEY (`id`));
整型定義建議採用 INT (10) 或 BIGINT (20)
Item: COL.016
Severity: L1
Content: INT (M) 在 integer 數據類型中,M 表示最大顯示寬度。 在 INT (M) 中,M 的值跟 INT (M) 所佔多少存儲空間並無任何關係。 INT (3)、INT (4)、INT (8) 在磁盤上都是佔用 4 bytes 的存儲空間。
Case:
CREATE TABLE tab (a INT(1));
VARCHAR 定義長度過長
Item: COL.017
Severity: L2
Content: varchar 是可變長字符串,不預先分配存儲空間,長度不要超過 255,如果存儲長度過長 MySQL 將定義字段類型爲 text,獨立出來一張表,用主鍵來對應,避免影響其它字段索引效率。
Case:
CREATE TABLE tab (a varchar(3500));
消除不必要的 DISTINCT 條件
Item: DIS.001
Severity: L1
Content: 太多 DISTINCT 條件是複雜的裹腳布式查詢的症狀。考慮將複雜查詢分解成許多簡單的查詢,並減少 DISTINCT 條件的數量。如果主鍵列是列的結果集的一部分,則 DISTINCT 條件可能沒有影響。
Case:
SELECT DISTINCT c.c_id,count(DISTINCT c.c_name),count(DISTINCT c.c_e),count(DISTINCT c.c_n),count(DISTINCT c.c_me),c.c_d FROM (select distinct id, name from B) as e WHERE e.country_id = c.country_id
COUNT (DISTINCT) 多列時結果可能和你預想的不同
Item: DIS.002
Severity: L3
Content: COUNT (DISTINCT col) 計算該列除 NULL 之外的不重複行數,注意 COUNT (DISTINCT col, col2) 如果其中一列全爲 NULL 那麽即使另一列有不同的值,也返回 0。
Case:
SELECT COUNT(DISTINCT col, col2) FROM tbl;
DISTINCT * 對有主鍵的表沒有意義
Item: DIS.003
Severity: L3
Content: 當表已經有主鍵時,對所有列進行 DISTINCT 的輸出結果與不進行 DISTINCT 操作的結果相同,請不要畫蛇添足。
Case:
SELECT DISTINCT * FROM film;
避免在 WHERE 條件中使用函數或其他運算符
Item: FUN.001
Severity: L2
Content: 雖然在 SQL 中使用函數可以簡化很多複雜的查詢,但使用了函數的查詢無法利用表中已經建立的索引,該查詢將會是全表掃描,性能較差。通常建議將列名寫在比較運算符左側,將查詢過濾條件放在比較運算符右側。也不建議在查詢比較條件兩側書寫多餘的括號,這會對閲讀産生比較大的困擾。
Case:
select id from t where substring(name,1,3)='abc'
指定了 WHERE 條件或非 MyISAM 引擎時使用 COUNT (*) 操作性能不佳
Item: FUN.002
Severity: L1
Content: COUNT (*) 的作用是統計表行數,COUNT (COL) 的作用是統計指定列非 NULL 的行數。MyISAM 表對於 COUNT (*) 統計全表行數進行了特殊的優化,通常情況下非常快。但對於非 MyISAM 表或指定了某些 WHERE 條件,COUNT (*) 操作需要掃描大量的行才能獲取精確的結果,性能也因此不佳。有時候某些業務場景並不需要完全精確的 COUNT 值,此時可以用近似值來代替。EXPLAIN 出來的優化器估算的行數就是一個不錯的近似值,執行 EXPLAIN 並不需要真正去執行查詢,所以成本很低。
Case:
SELECT c3, COUNT(*) AS accounts FROM tab where c2 < 10000 GROUP BY c3 ORDER BY num
使用了合並爲可空列的字符串連接
Item: FUN.003
Severity: L3
Content: 在一些查詢請求中,您需要強制讓某一列或者某個表達式返回非 NULL 的值,從而讓查詢邏輯變得更簡單,擔憂不想將這個值存下來。使用 COALESCE () 函數來構造連接的表達式,這樣即使是空值列也不會使整表達式變爲 NULL。
Case:
select c1 || coalesce(' ' || c2 || ' ', ' ') || c3 as c from tbl
不建議使用 SYSDATE () 函數
Item: FUN.004
Severity: L4
Content: SYSDATE () 函數可能導致主從數據不一致,請使用 NOW () 函數替代 SYSDATE ()。
Case:
SELECT SYSDATE();
不建議使用 COUNT (col) 或 COUNT (常量)
Item: FUN.005
Severity: L1
Content: 不要使用 COUNT (col) 或 COUNT (常量) 來替代 COUNT (*), COUNT (*) 是 SQL92 定義的標準統計行數的方法,跟數據無關,跟 NULL 和非 NULL 也無關。
Case:
SELECT COUNT(1) FROM tbl;
使用 SUM (COL) 時需注意 NPE 問題
Item: FUN.006
Severity: L1
Content: 當某一列的值全是 NULL 時,COUNT (COL) 的返回結果爲 0, 但 SUM (COL) 的返回結果爲 NULL,因此使用 SUM () 時需注意 NPE 問題。可以使用如下方式來避免 SUM 的 NPE 問題: SELECT IF (ISNULL (SUM (COL)), 0, SUM (COL)) FROM tbl
Case:
SELECT SUM(COL) FROM tbl;
不建議使用觸發器
Item: FUN.007
Severity: L1
Content: 觸發器的執行沒有反餽和日志,隱藏了實際的執行步驟,當數據庫出現問題是,不能通過慢日志分析觸發器的具體執行情況,不易發現問題。在 MySQL 中,觸發器不能臨時關閉或打開,在數據遷移或數據恢複等場景下,需要臨時 drop 觸發器,可能影響到生産環境。
Case:
CREATE TRIGGER t1 AFTER INSERT ON work FOR EACH ROW INSERT INTO time VALUES(NOW());
不建議使用存儲過程
Item: FUN.008
Severity: L1
Content: 存儲過程無版本控制,配合業務的存儲過程升級很難做到業務無感知。存儲過程在拓展和移植上也存在問題。
Case:
CREATE PROCEDURE simpleproc (OUT param1 INT);
不建議使用自定義函數
Item: FUN.009
Severity: L1
Content: 不建議使用自定義函數
Case:
CREATE FUNCTION hello (s CHAR(20));
不建議對等值查詢列使用 GROUP BY
Item: GRP.001
Severity: L2
Content: GROUP BY 中的列在前面的 WHERE 條件中使用了等值查詢,對這樣的列進行 GROUP BY 意義不大。
Case:
select film_id, title from film where release_year='2006' group by release_year
JOIN 語句混用逗號和 ANSI 模式
Item: JOI.001
Severity: L2
Content: 表連接的時候混用逗號和 ANSI JOIN 不便於人類理解,並且 MySQL 不同版本的表連接行爲和優先級均有所不同,當 MySQL 版本變化後可能會引入錯誤。
Case:
select c1,c2,c3 from t1,t2 join t3 on t1.c1=t2.c1,t1.c3=t3,c1 where id>1000
同一張表被連接兩次
Item: JOI.002
Severity: L4
Content: 相同的表在 FROM 子句中至少出現兩次,可以簡化爲對該表的單次訪問。
Case:
select tb1.col from (tb1, tb2) join tb2 on tb1.id=tb.id where tb1.id=1
OUTER JOIN 失效
Item: JOI.003
Severity: L4
Content: 由於 WHERE 條件錯誤使得 OUTER JOIN 的外部表無數據返回,這會將查詢隱式轉換爲 INNER JOIN 。如:select c from L left join R using (c) where L.a=5 and R.b=10。這種 SQL 邏輯上可能存在錯誤或程序員對 OUTER JOIN 如何工作存在誤解,因爲 LEFT/RIGHT JOIN 是 LEFT/RIGHT OUTER JOIN 的縮寫。
Case:
select c1,c2,c3 from t1 left outer join t2 using(c1) where t1.c2=2 and t2.c3=4
不建議使用排它 JOIN
Item: JOI.004
Severity: L4
Content: 只在右側表爲 NULL 的帶 WHERE 子句的 LEFT OUTER JOIN 語句,有可能是在 WHERE 子句中使用錯誤的列,如:“... FROM l LEFT OUTER JOIN r ON l.l = r.r WHERE r.z IS NULL”,這個查詢正確的邏輯可能是 WHERE r.r IS NULL。
Case:
select c1,c2,c3 from t1 left outer join t2 on t1.c1=t2.c1 where t2.c2 is null
減少 JOIN 的數量
Item: JOI.005
Severity: L2
Content: 太多的 JOIN 是複雜的裹腳布式查詢的症狀。考慮將複雜查詢分解成許多簡單的查詢,並減少 JOIN 的數量。
Case:
select bp1.p_id, b1.d_d as l, b1.b_id from b1 join bp1 on (b1.b_id = bp1.b_id) left outer join (b1 as b2 join bp2 on (b2.b_id = bp2.b_id)) on (bp1.p_id = bp2.p_id ) join bp21 on (b1.b_id = bp1.b_id) join bp31 on (b1.b_id = bp1.b_id) join bp41 on (b1.b_id = bp1.b_id) where b2.b_id = 0
將嵌套查詢重寫爲 JOIN 通常會導致更高效的執行和更有效的優化
Item: JOI.006
Severity: L4
Content: 一般來說,非嵌套子查詢總是用於關聯子查詢,最多是來自 FROM 子句中的一個表,這些子查詢用於 ANY, ALL 和 EXISTS 的謂詞。如果可以根據查詢語義決定子查詢最多返回一個行,那麽一個不相關的子查詢或來自 FROM 子句中的多個表的子查詢就被壓平了。
Case:
SELECT s,p,d FROM tbl WHERE p.p_id = (SELECT s.p_id FROM tbl WHERE s.c_id = 100996 AND s.q = 1 )
不建議使用聯表刪除或更新
Item: JOI.007
Severity: L4
Content: 當需要同時刪除或更新多張表時建議使用簡單語句,一條 SQL 只刪除或更新一張表,盡量不要將多張表的操作在同一條語句。
Case:
UPDATE users u LEFT JOIN hobby h ON u.id = h.uid SET u.name = 'pianoboy' WHERE h.hobby = 'piano';
不要使用跨數據庫的 JOIN 查詢
Item: JOI.008
Severity: L4
Content: 一般來說,跨數據庫的 JOIN 查詢意味著查詢語句跨越了兩個不同的子系統,這可能意味著系統耦合度過高或庫表結構設計不合理。
Case:
SELECT s,p,d FROM tbl WHERE p.p_id = (SELECT s.p_id FROM tbl WHERE s.c_id = 100996 AND s.q = 1 )
建議使用自增列作爲主鍵,如使用聯合自增主鍵時請將自增鍵作爲第一列
Item: KEY.001
Severity: L2
Content: 建議使用自增列作爲主鍵,如使用聯合自增主鍵時請將自增鍵作爲第一列
Case:
create table test(`id` int(11) NOT NULL PRIMARY KEY (`id`))
無主鍵或唯一鍵,無法在線變更表結構
Item: KEY.002
Severity: L4
Content: 無主鍵或唯一鍵,無法在線變更表結構
Case:
create table test(col varchar(5000))
避免外鍵等遞歸關係
Item: KEY.003
Severity: L4
Content: 存在遞歸關係的數據很常見,數據常會像樹或者以層級方式組織。然而,創建一個外鍵約束來強制執行同一表中兩列之間的關係,會導致笨拙的查詢。樹的每一層對應著另一個連接。您將需要發出遞歸查詢,以獲得節點的所有後代或所有祖先。解決方案是構造一個附加的閉包表。它記錄了樹中所有節點間的關係,而不僅僅是那些具有直接的父子關係。您也可以比較不同層次的數據設計:閉包表,路徑枚舉,嵌套集。然後根據應用程序的需要選擇一個。
Case:
CREATE TABLE tab2 (p_id BIGINT UNSIGNED NOT NULL,a_id BIGINT UNSIGNED NOT NULL,PRIMARY KEY (p_id, a_id),FOREIGN KEY (p_id) REFERENCES tab1(p_id),FOREIGN KEY (a_id) REFERENCES tab3(a_id))
提醒:請將索引屬性順序與查詢對齊
Item: KEY.004
Severity: L0
Content: 如果爲列創建複合索引,請確保查詢屬性與索引屬性的順序相同,以便 DBMS 在處理查詢時使用索引。如果查詢和索引屬性訂單沒有對齊,那麽 DBMS 可能無法在查詢處理期間使用索引。
Case:
create index idx1 on tbl (last_name,first_name)
表建的索引過多
Item: KEY.005
Severity: L2
Content: 表建的索引過多
Case:
CREATE TABLE tbl ( a int, b int, c int, KEY idx_a (`a`),KEY idx_b(`b`),KEY idx_c(`c`));
主鍵中的列過多
Item: KEY.006
Severity: L4
Content: 主鍵中的列過多
Case:
CREATE TABLE tbl ( a int, b int, c int, PRIMARY KEY(`a`,`b`,`c`));
未指定主鍵或主鍵非 bigint
Item: KEY.007
Severity: L4
Content: 未指定主鍵或主鍵非 bigint,建議將主鍵設置爲 bigint unsigned。
Case:
CREATE TABLE tbl (a bigint);
ORDER BY 多個列但排序方向不同時可能無法使用索引
Item: KEY.008
Severity: L4
Content: 在 MySQL 8.0 之前當 ORDER BY 多個列指定的排序方向不同時將無法使用已經建立的索引。
Case:
SELECT * FROM tbl ORDER BY a DESC, b ASC;
添加唯一索引前請注意檢查數據唯一性
Item: KEY.009
Severity: L0
Content: 請提前檢查添加唯一索引列的數據唯一性,如果數據不唯一在線表結構調整時將有可能自動將重複列刪除,這有可能導致數據丟失。
Case:
CREATE UNIQUE INDEX part_of_name ON customer (name(10));
全文索引不是銀彈
Item: KEY.010
Severity: L0
Content: 全文索引主要用於解決模糊查詢的性能問題,但需要控制好查詢的頻率和並發度。同時注意調整 ft_min_word_len, ft_max_word_len, ngram_token_size 等參數。
Case:
CREATE TABLE `tb` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `ip` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`), FULLTEXT KEY `ip` (`ip`) ) ENGINE=InnoDB;
SQL_CALC_FOUND_ROWS 效率低下
Item: KWR.001
Severity: L2
Content: 因爲 SQL_CALC_FOUND_ROWS 不能很好地擴展,所以可能導致性能問題;建議業務使用其他策略來替代 SQL_CALC_FOUND_ROWS 提供的計數功能,比如:分頁結果展示等。
Case:
select SQL_CALC_FOUND_ROWS col from tbl where id>1000
不建議使用 MySQL 關鍵字做列名或表名
Item: KWR.002
Severity: L2
Content: 當使用關鍵字做爲列名或表名時程序需要對列名和表名進行轉義,如果疏忽被將導致請求無法執行。
Case:
CREATE TABLE tbl ( `select` int )
不建議使用複數做列名或表名
Item: KWR.003
Severity: L1
Content: 表名應該僅僅表示表裡面的實體内容,不應該表示實體數量,對應於 DO 類名也是單數形式,符合表達習慣。
Case:
CREATE TABLE tbl ( `books` int )
不建議使用使用多字節編碼字符 (中文) 命名
Item: KWR.004
Severity: L1
Content: 爲庫、表、列、別名命名時建議使用英文,數字,下劃線等字符,不建議使用中文或其他多字節編碼字符。
Case:
select col as 列 from tb
INSERT INTO xx SELECT 加鎖粒度較大請謹慎
Item: LCK.001
Severity: L3
Content: INSERT INTO xx SELECT 加鎖粒度較大請謹慎
Case:
INSERT INTO tbl SELECT * FROM tbl2;
請慎用 INSERT ON DUPLICATE KEY UPDATE
Item: LCK.002
Severity: L3
Content: 當主鍵爲自增鍵時使用 INSERT ON DUPLICATE KEY UPDATE 可能會導致主鍵出現大量不連續快速增長,導致主鍵快速溢出無法繼續寫入。極耑情況下還有可能導致主從數據不一致。
Case:
INSERT INTO t1(a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1;
用字符類型存儲 IP 地址
Item: LIT.001
Severity: L2
Content: 字符串字面上看起來像 IP 地址,但不是 INET_ATON () 的參數,表示數據被存儲爲字符而不是整數。將 IP 地址存儲爲整數更爲有效。
Case:
insert into tbl (IP,name) values('10.20.306.122','test')
日期 / 時間未使用引號括起
Item: LIT.002
Severity: L4
Content: 諸如 “WHERE col <2010-02-12” 之類的查詢是有效的 SQL,但可能是一個錯誤,因爲它將被解釋爲 “WHERE col <1996”; 日期 / 時間文字應該加引號。
Case:
select col1,col2 from tbl where time < 2018-01-10
一列中存儲一系列相關數據的集合
Item: LIT.003
Severity: L3
Content: 將 ID 存儲爲一個列表,作爲 VARCHAR/TEXT 列,這樣能導致性能和數據完整性問題。查詢這樣的列需要使用模式匹配的表達式。使用逗號分隔的列表來做多表聯結查詢定位一行數據是極不優雅和耗時的。這將使驗证 ID 更加困難。考慮一下,列表最多支持存放多少數據呢?將 ID 存儲在一張單獨的表中,代替使用多值屬性,從而每個單獨的屬性值都可以佔據一行。這樣交叉表實現了兩張表之間的多對多關係。這將更好地簡化查詢,也更有效地驗证 ID。
Case:
select c1,c2,c3,c4 from tab1 where col_id REGEXP '[[:<:]]12[[:>:]]'
請使用分號或已設定的 DELIMITER 結尾
Item: LIT.004
Severity: L1
Content: USE database, SHOW DATABASES 等命令也需要使用使用分號或已設定的 DELIMITER 結尾。
Case:
USE db
非確定性的 GROUP BY
Item: RES.001
Severity: L4
Content: SQL 返回的列既不在聚合函數中也不是 GROUP BY 表達式的列中,因此這些值的結果將是非確定性的。如:select a, b, c from tbl where foo="bar" group by a,該 SQL 返回的結果就是不確定的。
Case:
select c1,c2,c3 from t1 where c2='foo' group by c2
未使用 ORDER BY 的 LIMIT 查詢
Item: RES.002
Severity: L4
Content: 沒有 ORDER BY 的 LIMIT 會導致非確定性的結果,這取決於查詢執行計劃。
Case:
select col1,col2 from tbl where name=xx limit 10
UPDATE/DELETE 操作使用了 LIMIT 條件
Item: RES.003
Severity: L4
Content: UPDATE/DELETE 操作使用 LIMIT 條件和不添加 WHERE 條件一樣危險,它可將會導致主從數據不一致或從庫同步中斷。
Case:
UPDATE film SET length = 120 WHERE title = 'abc' LIMIT 1;
UPDATE/DELETE 操作指定了 ORDER BY 條件
Item: RES.004
Severity: L4
Content: UPDATE/DELETE 操作不要指定 ORDER BY 條件。
Case:
UPDATE film SET length = 120 WHERE title = 'abc' ORDER BY title
UPDATE 語句可能存在邏輯錯誤,導致數據損壞
Item: RES.005
Severity: L4
Content: 在一條 UPDATE 語句中,如果要更新多個字段,字段間不能使用 AND ,而應該用逗號分隔。
Case:
update tbl set col = 1 and cl = 2 where col=3;
永遠不真的比較條件
Item: RES.006
Severity: L4
Content: 查詢條件永遠非真,如果該條件出現在 where 中可能導致查詢無匹配到的結果。
Case:
select * from tbl where 1 != 1;
永遠爲真的比較條件
Item: RES.007
Severity: L4
Content: 查詢條件永遠爲真,可能導致 WHERE 條件失效進行全表查詢。
Case:
select * from tbl where 1 = 1;
不建議使用 LOAD DATA/SELECT ... INTO OUTFILE
Item: RES.008
Severity: L2
Content: SELECT INTO OUTFILE 需要授予 FILE 權限,這通過會引入安全問題。LOAD DATA 雖然可以提高數據導入速度,但同時也可能導致從庫同步延遲過大。
Case:
LOAD DATA INFILE 'data.txt' INTO TABLE db2.my_table;
請謹慎使用 TRUNCATE 操作
Item: SEC.001
Severity: L0
Content: 一般來說想清空一張表最快速的做法就是使用 TRUNCATE TABLE tbl_name; 語句。但 TRUNCATE 操作也並非是毫無代價的,TRUNCATE TABLE 無法返回被刪除的準確行數,如果需要返回被刪除的行數建議使用 DELETE 語法。TRUNCATE 操作還會重置 AUTO_INCREMENT,如果不想重置該值建議使用 DELETE FROM tbl_name WHERE 1; 替代。TRUNCATE 操作會對數據字典添加源數據鎖 (MDL),當一次需要 TRUNCATE 很多表時會影響整個實例的所有請求,因此如果要 TRUNCATE 多個表建議用 DROP+CREATE 的方式以減少鎖時長。
Case:
TRUNCATE TABLE tbl_name
不使用明文存儲密碼
Item: SEC.002
Severity: L0
Content: 使用明文存儲密碼或者使用明文在網路上傳遞密碼都是不安全的。如果攻擊者能夠截獲您用來插入密碼的 SQL 語句,他們就能直接讀到密碼。另外,將用戶輸入的字符串以明文的形式插入到純 SQL 語句中,也會讓攻擊者發現它。如果您能夠讀取密碼,黑客也可以。解決方案是使用單向哈希函數對原始密碼進行加密編碼。哈希是指將輸入字符串轉化成另一個新的、不可識別的字符串的函數。對密碼加密表達式加點隨機串來防禦 “字典攻擊”。不要將明文密碼輸入到 SQL 查詢語句中。在應用程序代碼中計算哈希串,只在 SQL 查詢中使用哈希串。
Case:
create table test(id int,name varchar(20) not null,password varchar(200)not null)
使用 DELETE/DROP/TRUNCATE 等操作時注意備份
Item: SEC.003
Severity: L0
Content: 在執行高危操作之前對數據進行備份是十分有必要的。
Case:
delete from table where col = 'condition'
建議使用 datetime 替換 timestamp 類型
Item: SKEY.005
Severity: L4
Content: 建議使用 datetime 替換 timestamp 類型,且默認值設置爲 1970-01-01 00:00:00。 datetime 類型能保存大範圍的值,從 1001 年到 9999 年,且與時區無關。使用 8 個字節的存儲空間(比 timestamp 多出 4 字節)
Case:
CREATE TABLE tbl (a datetime);
缺少數據庫必須字段 last_update_time 和 is_del
Item: SKEY.006
Severity: L4
Content: 數據庫必須字段 (`last_update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ' 最後更新時間 '; `is_del` TINYINT (1) UNSIGNED NOT NULL DEFAULT '0' COMMENT ' 是否刪除 0:未刪除 1:已刪除 ')
Case:
CREATE TABLE tbl (`last_update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最後更新時間'; `is_del` TINYINT (1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否刪除 0:未刪除 1:已刪除');
last_update_time 和 is_del 類型不對
Item: SKEY.006a
Severity: L4
Content: 數據庫必須字段 (`last_update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ' 最後更新時間 '; `is_del` TINYINT (1) UNSIGNED NOT NULL DEFAULT '0' COMMENT ' 是否刪除 0:未刪除 1:已刪除 ')
Case:
CREATE TABLE tbl (`last_update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最後更新時間'; `is_del` TINYINT (1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否刪除 0:未刪除 1:已刪除');
不建議使用大字段 TEXT BLOB
Item: SKEY.010
Severity: L1
Content: BLOB 和 TEXT 都是爲存儲很大的數據而設計的字符串數據類型,且性能開銷較大,請檢查是否有必要使用
Case:
CREATE TABLE tbl (a TEXT);
整形建議使用 unsigned
Item: SKEY.011
Severity: L1
Content: 請檢查整形是否有負數場景,如無特殊場景,建議使用 unsigned
Case:
CREATE TABLE tbl (a int unsigned);
'!=' 運算符是非標準的
Item: STA.001
Severity: L0
Content: "<>" 才是標準 SQL 中的不等於運算符。
Case:
select col1,col2 from tbl where type!=0
庫名或表名點後建議不要加空格
Item: STA.002
Severity: L1
Content: 當使用 db.table 或 table.column 格式訪問表或字段時,請不要在點號後面添加空格,雖然這樣語法正確。
Case:
select col from sakila. film
索引起名不槼範
Item: STA.003
Severity: L1
Content: 建議普通二級索引以 idx_爲前綴,唯一索引以 uniq_爲前綴。
Case:
select col from now where type!=0
起名時請不要使用字母、數字和下劃線之外的字符
Item: STA.004
Severity: L1
Content: 以字母或下劃線開頭,名字只允許使用字母、數字和下劃線。請統一大小寫,不要使用駝峰命名法。不要在名字中出現連續下劃線 '__',這樣很難辨認。
Case:
CREATE TABLE ` abc` (a int);
MySQL 對子查詢的優化效果不佳
Item: SUB.001
Severity: L4
Content: MySQL 將外部查詢中的每一行作爲依賴子查詢執行子查詢。 這是導致嚴重性能問題的常見原因。這可能會在 MySQL 5.6 版本中得到改善,但對於 5.1 及更早版本,建議將該類查詢分別重寫爲 JOIN 或 LEFT OUTER JOIN。
Case:
select col1,col2,col3 from table1 where col2 in(select col from table2)
如果您不在乎重複的話,建議使用 UNION ALL 替代 UNION
Item: SUB.002
Severity: L2
Content: 與去除重複的 UNION 不同,UNION ALL 允許重複元組。如果您不關心重複元組,那麽使用 UNION ALL 將是一個更快的選項。
Case:
select teacher_id as id,people_name as name from t1,t2 where t1.teacher_id=t2.people_id union select student_id as id,people_name as name from t1,t2 where t1.student_id=t2.people_id
考慮使用 EXISTS 而不是 DISTINCT 子查詢
Item: SUB.003
Severity: L3
Content: DISTINCT 關鍵字在對元組排序後刪除重複。相反,考慮使用一個帶有 EXISTS 關鍵字的子查詢,您可以避免返回整個表。
Case:
SELECT DISTINCT c.c_id, c.c_name FROM c,e WHERE e.c_id = c.c_id
執行計劃中嵌套連接深度過深
Item: SUB.004
Severity: L3
Content: MySQL 對子查詢的優化效果不佳,MySQL 將外部查詢中的每一行作爲依賴子查詢執行子查詢。 這是導致嚴重性能問題的常見原因。
Case:
SELECT * from tb where id in (select id from (select id from tb))
子查詢不支持 LIMIT
Item: SUB.005
Severity: L8
Content: 當前 MySQL 版本不支持在子查詢中進行 'LIMIT & IN/ALL/ANY/SOME'。
Case:
SELECT * FROM staff WHERE name IN (SELECT NAME FROM customer ORDER BY name LIMIT 1)
不建議在子查詢中使用函數
Item: SUB.006
Severity: L2
Content: MySQL 將外部查詢中的每一行作爲依賴子查詢執行子查詢,如果在子查詢中使用函數,即使是 semi-join 也很難進行高效的查詢。可以將子查詢重寫爲 OUTER JOIN 語句並用連接條件對數據進行過濾。
Case:
SELECT * FROM staff WHERE name IN (SELECT max(NAME) FROM customer)
不建議使用分區表
Item: TBL.001
Severity: L4
Content: 不建議使用分區表
Case:
CREATE TABLE trb3(id INT, name VARCHAR(50), purchased DATE) PARTITION BY RANGE(YEAR(purchased)) (PARTITION p0 VALUES LESS THAN (1990), PARTITION p1 VALUES LESS THAN (1995), PARTITION p2 VALUES LESS THAN (2000), PARTITION p3 VALUES LESS THAN (2005) );
請爲表選擇合適的存儲引擎
Item: TBL.002
Severity: L4
Content: 建表或修改表的存儲引擎時建議使用推薦的存儲引擎,如:innodb
Case:
create table test(`id` int(11) NOT NULL AUTO_INCREMENT)
以 DUAL 命名的表在數據庫中有特殊含義
Item: TBL.003
Severity: L8
Content: DUAL 表爲虛擬表,不需要創建即可使用,也不建議服務以 DUAL 命名表。
Case:
create table dual(id int, primary key (id));
表的初始 AUTO_INCREMENT 值不爲 0
Item: TBL.004
Severity: L2
Content: AUTO_INCREMENT 不爲 0 會導致數據空洞。
Case:
CREATE TABLE tbl (a int) AUTO_INCREMENT = 10;
請使用推薦的字符集
Item: TBL.005
Severity: L4
Content: 表字符集只允許設置爲 utf8mb4
Case:
CREATE TABLE tbl (a int) DEFAULT CHARSET = latin1;
不建議使用視圖
Item: TBL.006
Severity: L1
Content: 不建議使用視圖
Case:
create view v_today (today) AS SELECT CURRENT_DATE;
不建議使用臨時表
Item: TBL.007
Severity: L1
Content: 不建議使用臨時表
Case:
CREATE TEMPORARY TABLE `work` (`time` time DEFAULT NULL) ENGINE=InnoDB;