1.選擇優化的數據類型
更小的通常更好
一般情況下選擇可以正確存儲數據的最小數據類型。更小的類型通常更快,占用更少的硬盤、內存、CPU等。但要確保沒有遞歸需要存儲的值的范圍。
簡單就好
簡單數據類型的操作通常需要更少的CPU周期。整形比字符操作代價更低。列子:
1.用MySQL內建的類型而不是字符串來存儲日期和h時間
2.另外一個用整型存儲IP地址
盡量避免NULL
通常情況下指定列為NULL。如果查詢中包含NULL列,MySQL更難優化。因為可為NULL的列被索引時,索引統計和值比較都比較復雜。列被指定為NULL每個索引記錄都要一個額外字節。
通常把NULL列改為NOT NULL 帶來的提升比較小。所以調優時沒有必要在schema中查找并修改這種情況。在設計的時候,避免設計NULL列。
1.1.整數類型
TINYINT : 8位
SMALLINT:16
MEDIUMINT:24
INT: 32
BIGINT: 64
長度 -2(N-1) ~2(N-1)
整數類型有可選的UNSIGNED類型,標識不允許負值,這個大致可以使正數的范圍提高一倍。
有符號和無符號類型使用相同的存儲空間,并且具有相同的性能。
整數計算一般使用64位的BIGINT整數
1.2.實數類型
DECIMAL存儲比BIGINT還大的整數
DECIMAL類型用于儲存精確的小數
浮點和DECIMAL類型可以指定精度
在對小數進行精確計算的時候使用DECIMAL---財務數據。但在數據量比較大的時候,可以考慮使用BIGINT代替DECIMAL,將需要的貨幣單位根據小數乘以相應的倍數即可。避免浮點數不精確和DECIMAL精確計算代價高的問題
1.3.字符串類型
VARCHAR
1.存儲可變長字符串。比定長類型更節省空間,因為它僅需要必要的空間。
列外:當表使用ROW_FORMAT= FIXED創建的話,每一行都會使用定長存儲。浪費空間。
2.VARCHAR使用1-2個額外字節記錄字符串的長度:列的最大長度<=255字節,則只使用1個字節表示,否則使用2個字節。假設使用Latin1字符集,VARCHAR(10)需要11個存儲空間,VARCHAR(1001)需要1002個存儲空間。
3.Update時可能使行變得比原來更長,導致額外工作。如果行占用的空間增長,并且在頁內沒有更多的空間可以存儲,在這種情況下不同引擎處理方式不一樣。MyISAM會將行拆成不同的片段存儲,InnoDB則需要分裂頁來使得行可以放進頁內。
符合使用VARCHAR:
1.字符串的最大長度比平均長度大很多;
2.列的更新很少,碎片不是問題。
3.InnoDB將過長的VARCHAR存儲為BLOB
CHAR
CHAR是定長的:MYSQL總是根據定義的字符串長度來分配足夠的空間,并且會剔除末尾的空格。
CHAR適合存儲很短的字符串,或者所有的字符串都接近于同一個長度。
對于經常改變的數據CHAR也比VARCHAR更好,因為定長CHAR不容易產生碎片。對應非常短的列CHAR比VARCHAR在空間上也更有效率。
二進制數據存儲一般使用BINARY和CARBINARY類型。
使用枚舉(ENUM)代替字符串類型
枚舉是按照內部存儲的整數而不是定義的字符串進行排序的。所以我們一般按照需要的順序來定義枚舉序列。另外也可以在查詢中使用FIELD()函數顯示的指定排序,但這會導致MySQL無法利用索引消除排序。
CREATE TABLE t1 (e ENUM('finsh', 'apple', 'dog') NOT NULL);
INSERT INTO t1 (e) VALUES ('finsh'), ('dog'),('apple');
這三行數據實際存儲為整數,而不是字符串
SELECT e+0 FROM t1;
1.4.日期和時間類型
DATETIME
能保存大范圍的值,從1001年到9999年,精度為秒。他把時間和日期封裝到YYYYMMDDHHMMSS的整數中,與時區無關。使用8個字節的存儲空間。
TIMESTAMP
保存了從1970年1月1日午夜以來的秒數,和UNIX時間戳相同。只用4個字節的存儲空間,比DATETIME小的多;只能表示從1970到2038年。
TIMESTAMP依賴于時區。MYSQL服務器、操作系統,以及客戶端鏈接都有時區設置。
插入時沒有指定第一個TIMESTAMP列的值,MySQL設置這個列的值為當前時間。TIMESTAMP列默認為NULL。
除了特殊行為之外,一般盡量使用TIMESTAMP的值,因為它比DATETIME效率更高。不推薦將其轉為Unix時間戳,因為不會帶來任何收益。整數格式保存時間的格式通常不方便處理,所以通常不這么做。
如果存儲比秒更小的單位,MYSQL目前沒有提供合適的數據類型,不過可以使用其他格式:BIGINT存儲微妙級別的時間戳,或者使用DOUBLE存儲秒之后的小數部分。
4.5.位數據類型
位類型,技術上來說都是字符串類型。
BIT
使用BIT列在一列或多列中存儲一個或多個true/false值。BIT(1)定義包含一個位的字段、BIT(2)定義包含2個位,以此類推。最大長度64個位。
MYSQL把BIT當作字符串類型,所以當檢索BIT(1)的值的時候,結果時包含二進制0或1的字符串,而不是ASCII碼的0或1。
而在數字上下文檢索的時候,結果是將字符串轉為數字。
如果在一個bit的存儲空間保存true/false值,另一個方法是創建一個為空的CHAR(0)列。該列可以保存空值NULL或者長度為0 的字符串(空字符串)
SET
如果需要保存很多true/false值,可以考慮合并這些列到一個SET數據類型,它在MySQL內部是以一系列打包的位的集合來表示的。這樣有效地利用了空間。缺點是改變列的成本太高,ALTER TABLE是非常昂貴的操作。一般來說,也無法在SET列上通過索引進行查找。
4.6.選擇標識符
標識列又稱自增長列,可以不手動插入的列,系統提供默認值。為標識列選擇合適的數據非常重要,一般選擇時要考慮存儲類型,還需要考慮MYSQL對這種類型怎么比較和計算。例如,MySQL在內部使用整數存儲ENUM和SET類型,然后在比較時轉為字符串。
技巧:
1.選擇最小數據類型。TINYINT選INT都滿足的情況下選擇TINYINT。
2.整數類型時標識列最好的選擇,因為他們很快并且可以使用AUTO INCREMENT;
3.避免使用ENUM和SET類型。
4.如果可以,避免使用字符串類型作為標識列,因為他們很消耗空間。并且比數字類型慢。尤其是在MyISAM表使用字符串要小心,因為它默認會對字符串使用壓縮引擎,會導致查詢變慢。
5.對于一些隨機字符串也要注意。UUID() 、MD5等生成的字符串會任意分布在很大的空間內。會導致INSERT以及一些語句變得很慢:
- 因為插入值會隨機的寫到索引的不同位置,所以insert語句很慢。這回導致頁分裂、磁盤隨機訪問,以及對于聚簇存儲引擎產生新的聚簇索引碎片。
- SELECT語句變慢,因為邏輯上相鄰的行會分布在磁盤和內存不同的地方
- 隨機值導致緩存對所有類型的查詢語句效果都很差,因為會使得緩存賴以工作的訪問局部性原理失效。整個數據都變熱。
如果存儲UUID,應該移除“-”號;或者更好的做法是用UNHEX()函數轉換UUID為16字節的數字,并存儲在一個BINARY(16)列中。檢索時可以通過HEX()函數來格式化為十六進制格式。
1.7.特殊類型數據
某些類型的數據類型并不直接與內置類型一致。比如低于秒級精度的時間戳。
另一個例子時一個IPv4地址。人們常使用VARCHAR(15)
來存儲IP地址。然而,它們實際上時32位無符號整數,不是字符串。所以應該使用無符號整數存儲IP地址。MySQL提供INET_ATON()
和INET_NTOA()
函數來進行轉換。
2.注意MySQL schema設計中的陷阱
1.太多的列
MySQL的存儲引擎API工作時需要在服務器和存儲引擎之間通過緩沖行格式拷貝數據,然后在服務器層將緩沖內容解碼成各個列。從緩沖中將編碼通過的列轉成數據結構的代價是非常高的。
2.太多關聯
所謂的 "實體-屬性-值"(EVA)設計模式是一個糟糕的設計模式。一般,單個查詢在12個表以內做關聯。
3.全能的枚舉
防止過度使用枚舉
4.變相的枚舉
枚舉列允許在列中存儲一組定義值中的單個值,集合(SET)允許在列中存儲一組定義值的一個或多個值。有時候這個可能會導致混亂。
ex:
CTEATE TABLE ...(
is default set('Y', 'N') NOT NULL default 'N'
這里真和假兩種情況不會同時出現,那么毫無疑問應該使用枚舉替代集合列。
5.非此發明的NULL
之前我們提到避免使用NULL,并建議盡可能的采用替代方案。
- 字符串NULL用空字符串代替"NULL"
- 不要走極端,改用NULL還是得用NULL
- MySQL會在所以存儲NULL,ORACLE不會。
3.范式和反范式
1.范式的優點和缺點
- 范式化的更新操作通常比反范式化要快
- 當數據較好的范式化時,只有很少或沒有重復數據,所以只需要修改很少的數據。
- 范式化的表通常很小,可以更好的放在內存中,所以執行會更快
- 很少有多余的數據意味著檢索列表數據時更少需要DISTINCT或者 GROUP BY語句。
范式化的缺點通常是需要關聯。通常稍微復雜一些的查詢語句在符合范式的schema上都可能需要至少一次關聯,也許更多。這不但代價昂貴,也可能使一些索引策略無效。例如,范式化將列存放在不同的表中,而這些列如果在一個表中本可以屬于同一個索引。
2.反范式的優點和缺點
反范式化的schema因為所有數據都在一張表中,可以很好的避免關聯。如果不需要關聯表,則對大部分查詢最差的情況--即使表沒有使用索引--全表掃描。當數據比內存大時可能比關聯要快的多,因為這避免了隨機I/O。
3.混用范式和反范式化
常見的反范式化數據的方法時復制或者緩存,在不同的表中存儲相同的特列,在5.0的版本更新中,可以使用觸發器更新緩存
4.緩存表和匯總表
術語“緩存表”和“匯總表”沒有標準的定義。我們用術語“緩存表”來表示存儲那些可以比較簡單的從schema其他表獲取(獲取速度慢)數據的表。而術語“匯總表”則保存的是使用“GROUP BY” 語句聚合數據的表。
1.物化視圖
預先計算并且存儲在磁盤上的表,可以通過各種各樣的策略刷新和更新。MySQL并不原生支持物化視圖。一般使用Flexviews。
2.計數器表
如果在表中保存計數器,則在更新計算器的時候可能碰到并發問題。計數器在Web應用中很常見。緩存用戶朋友數,文件下載次數等。
假設有個計數器表,只有一行數據記錄網站的點擊數:
CREATE TABLE hit_counter(
cnt int unsigned not null
) ENGINE = InnoDB
網站每次點擊都會導致計數器更新
UPDATE hit_counter SET cnt = cnt + 1;
問題在一這個事務只能串行執行。要獲得更高的并發性能,也可以將計數器保存在多行中,每次隨機選擇一行進行更新,這樣對計數器表結構進行如下修改:
CREATE TABLE hit_counter (
slot tinyint unsigned not null primary key,
cnt int unsigned not null
) ENGINE = InnoDB
然后預先在這個表增加100行數據。現在選擇一個隨機的slot進行更新:
UPDATE hit_counter SET cnt = cnt + 1 WHERE slot = RAND() * 100;
要獲得統計結果,使用下面的查詢:
SELECT SUM(cnt) FROM hit_counter;
一個常見的需求是每隔一段新的時間開始一個新的計數器(每天一個)。如果要這么做,可以修個一個表設計:
CREATE TABLE daily_hit_counter (
day date not null,
slot tinyhit unsigned not null,
cnt int unsigned not null,
primary key(day, slot)
) ENGINE = InnoDB;
在這個場景中,可以不用預先生成行, 而用 ON DUPLICATEKEY UPDATE 代替:
INSERT INTO daily_hit_counter(day, slot, cnt)
VALUES(CURRENT_DATE, RAND() * 100, 1)
ON DUPLICATE KEY UPDATE cnt = cnt + 1
若果希望減少表的行數,避免表變得太大,可以寫一個周期行執行任務,將所有結果合并到0號slot,并且刪除其他slot。
mysql> UPDATE daily_hit_counter as c INNER JOIN(
SELECT day, SUM(cnt) AS cnt , MIN(slot) AS mslot
FROM daily_hit_counter
GROUP BY day
) AS x USING (day)
SET c.cnt = IF(c.slot = x.mslot, x.cnt, 0),
c.slot = IF (c.slot = x.mslot, 0, c.slot);
mysql> DELETE FROM daily_hit_counter WHERE slot <> 0 AND cnt = 0;
更快的度,更慢的寫
提升查詢速度,經常會撿一些額外的索引,增加多余列,甚至是創建緩存表和匯總表。這寫方法增加了查詢的負擔,也需要額外的維護任務,但在設計高性能數據庫時,這些都是常見技巧:雖然寫操作變慢了,但是顯著提高了度的性能。但是寫操作并不是付出的唯一代價,還可能同時增加讀操作和寫操作的開發難度。
5.加快ALTER TABLE 的操作的速度
MySQL的ALTER TABLE 操作性能對大表來說是個大問題。MySQL 執行大部分修改表結構的方法時用新的結構創建一個空表,從舊表中查出所有的數據插入新表,然后刪除舊表。這樣操作可能花費很長時間。常見的場景有兩種:一種時現在一臺不提供服務的機器上執行ALTER TABLE 操作,然后和提供服務的機器進行主庫切換;另外一種是進行:影子拷貝,是用要求的表結構創建一張和源無關的新表,然后通過重命名和刪表操作交換兩張表。
不是所有的ALTER TABLE 操作都會引起表重建。例如,有兩種方法可以修改和刪除一個列的默認值。一種是MODIFY COLUMN ,另一種是ALTER COLUMN,這個操作會修改 .frm 文件而不涉及表數據,所以這個操作是非常快的
1.只修改.frm文件
這項操作有一定的風險。有時候MySQL會在沒有必要的時候也重建表。下面的一些操作是有可能不需要重建的:
- 移除(不是增加)一個表的AUTO_INCREMENT屬性
- 增加、移除,或更改ENUM和SET常量。如果移除的是已經有行數據用到其他值的常量,查詢將會返回一個空字符串。
基本的技術是為想要的表結構創建一個新的.frm文件,然后用它替換掉已經存在的那張表的.frm文件
2.快速創建MyISAM索引
為了高效的載入數據到MyISAM,有個常用的技巧是先禁用索引、載入數據,然后重新啟用索引。這個技巧能發揮作用是因為構建索引的工作被延遲到數據完全載入之后,這個時候已經可以通過排序來構建索引了。這樣做會快很多,并且使得索引樹的碎片減少,更加的緊湊。
不過這個方法對唯一性索引無效,因為DISABLE KEYS 只對非唯一性索引有效。MyISAM會在內存中構造唯一性索引,并且位載入的每一行檢查唯一性。一旦索引大小超過了有效內存的大小,載入操作將變得越來越慢。
現代版本的InnoDB中,有個類似的技巧,依賴于InnoDB的快速在線索引創建功能。技巧是,先刪除所有的非唯一性索引,然后增加新的列,最后重新創建刪除掉的索引。Percona可以自動完成這些操作。
1. 用需要的表結構創建一張表,但是不包括索引。
2. 載入數據到表中以構建.MYD文件
3. 按照需要的結構創建另外一個空表,這次包含索引。這回創建需要的.frm和.MYI文件
4. 獲取讀鎖并刷新表
5. 重命名第二張表的.frm和.MYI文件,讓MySQL認為這是第一張表的文件
6. 釋放讀鎖
7. 使用REPAIR TABLE 來重建表的索引。該操作會通過排序來構建所有的索引,包括唯一索引。
這些操作步驟對大表來說快很多
6.總結
- 盡量避免過度設計,例如會導致機器復雜查詢的schema設計,或者有很多列的表設計(很多的意思是介于有點多和非常多之間)
- 使用小而簡單的數據類型,除非真實數據模型中有相關需要,否則應該盡可能避免使用NULL值
- 注意可變長字符串,其在臨時表和排序時可能導致悲觀的按最大長度分配內存
- 盡量使用整型定義標識列
- 避免使用MySQL已經遺棄的特性,例如浮點數的精度,或者整數的顯示寬度
- 小心使用ENUM 和SET。雖然他們用起來很方便,但是不要濫用,否則會變成陷阱。最好避免使用BIT
- 范式是好的,但是反范式有時候也是必需的,并且能夠帶來好處。預先計算、緩存或生成匯總表也可能獲得很大的好處。
- ALTER TABLE 是讓人痛苦的操作,因為在大部分情況下,它會鎖表并且重建整張表。我們展示了一些特殊場景的方法,但是大部分嘗盡,必須使用其他更加常規的方法,例如在備機執行ALTER TABLE 并在完成后把他切為主庫。