導語
- 來自一線的實戰經驗
- 每一條軍規背後都是血淋淋教訓
- 不要華麗,只要實用
- 若有一條讓你受益,慰矣
- 主要針對數據庫開發人員
總是在吃過虧後,才記得曾經有人提醒過
目錄
一,核心軍規(5)
二,字段類軍規(6)
三,索引類軍規(5)
四,SQL類軍規(15)
五,約定類軍規(5)
一,核心軍規
儘量不在數據庫做運算
- 別讓腳趾頭想事情
- 那是腦瓜子的職責
- 讓數據庫多做她擅長的事情
-
- 儘量不在數據庫做運算
- 複雜運算移到程序端CPU
- 儘可能簡單應用MySQL
- 舉例:md5() / order by rand()
- 一年內的單表數據量預估
-
- 純INT不超過1000w
- 含CHAR不超過500w
- 合理分表不超載
-
- USERID
- DATE
- AREA
- ...
- 建議單庫不超過300-400個表
- 表字段數少而精
-
- IO高效,全表遍歷,表修復快,提高併發,alter table快
- 單表多少字段合適?
- 單表1G體積 500w行評估
-
- 順序讀1G文件需N秒
- 單行不超過200byte
- 單表不超過50個純INT字段
- 單表不超過20個CHAR(10)字段
- 單表字段數上線控制在20-50個
- 平衡是門藝術
-
- 嚴格遵循三大範式?
- 效率優先、提升性能
- 沒有絕對的對與錯
- 適當時犧牲範式,加入冗餘
- 但會增加代碼複雜度
- 數據庫併發像城市交通
-
- 非線性增長
- 拒絕3B
-
- 大SQL(Big SQL)
- 大事務(Big transaction)
- 大批量(Big batch)
- 詳細解析見後
- 儘量不在數據庫做運算
- 控制單表數據量
- 保持表身段苗條
- 平衡範式與冗餘
- 拒絕3B
二,字段類軍規
用好數值字段類型
- 三類數值類型
-
- TINYINT(1 byte)
- SMALLINT(2B)
- MEDIUMINT(3B)
- INT(4B), BIGINT(8B)
- FLOAT(4B), DOUBLE(8B)
- DECIMAL(M,D)
- 數字型VS字符串型索引
-
- 更高效
- 查詢更快
- 佔用空間更小
- 舉例:用無符號INT存儲IP,而非CHAR(15)
-
- INT UNSIGNED
- INET_ATON()
- INET_NTOA()
- 優先使用ENUM或SET
-
- 字符串
- 可能值已知且有限
- 存儲
-
- ENUM佔用1字節,轉爲數值運算
- SET視節點定,最多佔用8字節
- 比較時需要加'單引號(即使是數值)
- 舉例
-
- `sex` enum('F','M') COMMENT '性別'
- `c1` enum('0','1','2','3') COMMENT '職介審覈'
- 避免使用NULL字段
-
- 很難進行查詢優化
- NULL列加索引,需要額外空間
- 含NULL符合索引無效
- 舉例
-
- `a` char(32) DEFAULT NULL
- `b` int(10) NOT NULL
- `c` int(10) NOT NULL DEFAULT 0
- TEXT類型處理性能遠低於VARCHAR
-
- 強制生成硬盤臨時表
- 浪費更多空間
- VARCHAR(65535)==>64k(注意UTF-8)
- 儘量不用TEXT/BLOB數據類型
- 若必須使用則拆分到單獨的表
- 舉例:CREATE TABLE t1( id INT NOT NULL AUTO_INCREMENT, data text NOT NULL, PRIMARY KEY(id))ENGINE=innodb;
字段類軍規小結
- 用好數值字段類型
- 將字符轉化爲數字
- 優先使用枚舉ENUM/SET
- 避免使用NULL字段
- 少用並拆分TEXT/BLOB
- 不在數據庫裏存圖片
謹慎合理添加索引
- 謹慎合理添加索引
-
- 改善查詢
- 減慢更新
- 索引不是越多越好
- 能不加的索引儘量不加
-
- 綜合評估數據密度和數據分佈
- 最好不超過字段數20%
- 結合核心SQL優先考慮覆蓋索引
- 舉例
-
- 不要給"性別"列創建索引
- 區分度
-
- 單字母區分度:26
- 4字母區分度:26*26*26*26=456,,976
- 5字母區分度:26^5=11,881,376
- 6字母區分度:26^6=308,915,776
- 字符字段必須建前綴索引
-
- `pinyin` varchar(100) default null comment '小區拼音', key `idx_pinyin`(`pinyin`(8))) engine=innodb
- 不在索引列進行數學運算或函數運算
-
- 無法使用索引
- 導致全表掃描
- 舉例
-
- BAD:select * from table where to_days(current_date)-to_days(date_col)<=10
- GOOD:select * from table where date_col>=date_sub('2011-10-22',interval 10 day)
- 對主鍵建立聚簇索引
- 而建索引存儲主鍵值
- 主鍵不應更新修改
- 案子增順序插入值
- 忌用字符串做主鍵
- 聚簇索引分裂
- 推薦用獨立於業務的AUTO_INCREMENT列或全局ID生成器做代理主鍵
- 若不指定主鍵,InnoDB會用唯一且非空值索引代替
- 線上OLTP系統(線下系統另論)
-
- 外鍵可節省開發量
- 有額外開銷
- 逐行操作
- 可’到達‘其他表,意味着鎖
- 高併發時容易死鎖
- 由程序保證約束
- 謹慎合理添加索引
- 字符字段必須建前綴索引
- 不在索引列做運算
- 自增列或全局ID做INNODB主鍵
- 儘量不用外鍵
SQL語句儘可能簡單
- 大SQL VS多個簡單SQL
-
- 傳統設計思想
- BUT MySQL NOT
- 一條SQL之恩能夠在一個CPU運算
- 5000+QPS的高併發中,1秒大SQL意味着?
- 可能一條大SL就把整個數據庫堵死
- 拒絕大SQL,拆解成多條簡單SQL
-
- 簡單SQL緩存命中率更高
- 減少鎖表時間,特別是MyISAM
- 用上多CPU
- 保持事務/DB連接短小精悍
-
- 事務/連接使用原則:即開即用,用完即關
- 與事務無關操作放到事務外面,減少鎖資源的佔用
- 不破壞一致性前提下,使用多個短事務代替長事務
- 舉例
-
- 發帖時的圖片上傳等待
- 大量的sleep連接
- 線上OLTP系統(線下庫另論)
-
- 儘可能少用存儲過程
- 儘可能少用觸發器
- 減用使用MySQL函數對結果進行處理
- 由客戶端程序負責
- 用select *時
-
- 更多消耗CPU、內存、IO、網絡帶寬
- 先向數據庫請求所有列,然後丟掉不需要列?
- 儘量不使用select*,只取需要數據列
-
- 更安全的設計:減少表變化帶來的影響
- 爲使用covering index提供可能性
- select/join減少硬盤臨時表生成,特別是有TEXT/BLOB時
- 舉例
-
- select * from tag where id=999184
- select keyword from tag where id=999184
- 同一字段,將or改爲in()
-
- or效率:O(n)
- in效率:O(log n)
- 當N很大時,or會慢很多
- 注意控制IN的個數,建議n小於200
- 舉例
-
- select * from opp where phone ='123456' or hple ='1235516'
- select * from opp where phone in('123456' ,'1235516')
- 不同字段,將or改爲union
-
- 減少對不同字段進行"or"查詢
- merge index往往很弱智
- 如果有足夠信心:set global optimizer_switch='index_merge=off'
- 舉例
-
- select * frmo opp where phone='010-88886666' or cellphone='13800138000'
- select * from opp where phone='010-88886666' union select * from opp where cellphone='13800138000'
- 避免負向查詢
-
- not, !=, <>, !<, !>, not exists, not in, not like等
- 米麪%前綴模糊查詢
-
- B+ tree
- 使用不了索引
- 導致全表掃描
- 舉例
-
- MySQL> select * from post where title like '北京%';
- 298 rows in set(0.01sec)
- MySQL> select * from post where title like '%北京%';
- 572 rows in set(3.27sec)
- 幾個有趣的例子:
-
- count(col) VS count(*)
- count(*) VS count(1)
- count(1) VS count(0) VS count(100)
- 結論
-
- count(*) = count(1)
- count(0) = count(1)
- count(1) = count(100)
- count(*) != count(col)
- WHY?
- MyISAM VS InnoDB
-
- 不帶where count()
- 帶where count()
- count(*)的資源開銷大,儘量不用少用
- 計數統計
-
- 實時統計:用memcache,雙向更新,凌晨跑基準
- 非實時統計:儘量用單獨統計表,定期重算
- 傳統分頁
-
- select * from table limit 10000,10;
- LIMIT原理:
-
- limit 10000,10
- 偏移量越大則越慢
- 推薦分頁:
-
- select * from table where id>=23423 limit 11; #10+1(每頁10條)
- select * from table where id>=23434 limit 11;
- 分頁方式二:
-
- select * from table where id >=(select id from table limit 10000,1)limit 10;
- 分頁方式三:
-
- select * from table inner join (select id from table limit 10000,1) using(id);
- 分頁方式四:
-
- 程序取ID:select id from table limit 10000,10;
- select * from table where id in(123,456,...)
- 可能需按場景分析並重組索引
LIMIT的高效分頁
- 示例
-
- MySQL> select sql_no_cache * from post limit 10,10;
- 10 row in set(0.01sec)
- MySQL> select sql_no_cache * from post limit 20000,10;
- 10 row in set(0.13sec)
- MySQL> select sql_no_cache * from post limit 80000,10;
- 10 rows in set(0.58sec)
- MySQL> select sql_no_cache id from post limit 80000,10;
- 10 rows in set(0.02sec)
- MySQL> select sql_no_cache * from post where id>=323423 limit 10;
- 10 rows in set(0.01sec)
- MySQL> select * from post where id >=(select sql_no_cache id from post limit 80000,1) limit 10;
- 10 rows in set(0.02sec)
- 若無需對結果進行去重,則用UNION ALL
-
- UNION有去重開銷
- 舉例
-
- MySQL> select * from detail20091128 UNION ALL
- select * from detail20110427 union all
- select * from detail20110426 union all
- select * from detail20110425 union all
- select * from detail20110424 union all
- select * from detail20110423;
- 高併發DB不建議進行兩個表以上的JOIN
- 適當分解連接保證高併發
-
- 可換成大量早期數據
- 使用了多個MyISAM表
- 對大表的小ID IN()
- 連接引用同一個表多次
- 舉例
-
-
- MySQL> select * from tag join tag_post n tag_post.tag_id=tag.id join post on tag_post.post_id=post.id where tag.tag='二手玩具'
- ->
- MySQL> select * from tab where tag='二手玩具';
- MySQL> select * from tag_post where tag_id=1321;
- MySQL> select * from post where post.id in(123,456,314,141);
-
- GROUP BY實現
-
- 分組
- 自動排序
- 無需排序:order by null
- 特定排序:group by desc/asc
- 舉例
- MySQL> select phone, count(*) from post group by phone limit 1;
- 1 row in set(2.19sec)
- MySQL> select phone,count(*) from post group by phone order by null limit 1;
- 1 row in set(2.02sec)
- 原則:數字對數字,字符對字符
- 數值列於字符類型比較
-
- 同時轉換爲雙精度
- 進行比對
- 字符列與數值類型比較
-
- 字符列整列轉數值
- 不會使用索引查詢
- 舉例:字符列與數值類型比較
-
- 字段: `remark` varchar(50) not null comment '備註,默認爲空';
- MySQL> select `id`,`gift_code` from gift where `deal_id` = 640 and remark=115127;
- 1 row in set(0.14sec)
- MySQL> select `id`,`gift_code` from pool_gift where `deal_id`=640 and remark='115127';
- 1 row in set(0.005sec)
- 批量數據快導入:
-
- 成批裝載比單行裝載更快,不需要每次刷新緩存
- 無索引時裝載比索引裝載更快
- insert values, values, values減少索引刷新
- load data比insert快約20倍
- 儘量不用insert...select
-
- 延遲
- 同步出錯
- 大批量更新凌晨操作,避開高峯
- 凌晨不限制
- 白天上線默認爲100條/秒(特殊再議)
- 舉例:
-
- update post set tag=1 where id in (1,2,3);
- sleep 0.01;
- update post set tag=1 where id in (4,5,6);
- sleep 0.01;
- ...
- show profile
- MySQLsla
- MySQLdumpslow
- explain
- show slow log
- show processlist
- show query_response_time(percona)
SQL類軍規小結
- SQL語句儘可能簡單
- 保持事務(連接)短小
- 儘可能避免使用SP/TRIG/FUNC
- 儘量不用select *
- 改寫or語句
- 避免負向查詢和%前綴模糊查詢
- 減少count(*)
- limit的高效分頁
- 用union all 而非union
- 分解連接保證高併發
- group by去除排序
- 統數據類型的列植比較
- load data導數據
- 打散大批量更新
- know every SQL
五,約定類軍規
隔離線上線下
- 構建數據庫的生態環境
-
- 開發無線上庫操作權限
- 原則:線上連線上,線下連線下
-
- 實時數據用real庫
- 模擬環境用sim庫
- 測試用qa庫
- 開發用dev庫
- 案例
- MySQL子查詢
-
- 大部分情況優化較差
- 特別where彙總使用in id的子查詢
- 一般可用join改寫
- 舉例:
- MySQL> select * from table 1 where id in(select id from table2);MySQL> insert into table1 (select * from table2); //可能導致複製異常
- 永遠不在程序端對數據庫顯式加鎖
-
- 外部鎖對數據庫不可控
- 高併發時是災難
- 極難調試和排查
- 併發扣款等一致性問題
-
- 採用事務
- 相對值修改
- commit前二次校驗衝突
- 字符集
-
- MySQL4.1以前只有latin1
- 爲多語言支持增加多字符集
- 也帶來N多問題
- 保持簡單
- 統一字符集:UTF8
- 校對規則:utf8_general_ci
- 亂碼:set names utf8
- 庫表等名稱統一用小寫
-
- linux VS windows
- MySQL庫表大小寫敏感
- 字段名大小寫不敏感
- 索引命名默認爲"idx_字段名"
- 庫名用縮寫,儘量在2~7個字母
-
- datasharing==>ds
- 注意避免用保留字命名
- ...
約定類軍規小些
- 隔離線上線下
- 禁止未經DBA確認的子查詢上線
- 永遠不在程序端顯式加鎖
- 統一字符集爲UTF8
- 統一命名規範