數(shù)據(jù)庫操作是當(dāng)今 Web 應(yīng)用程序中的主要瓶頸。 不僅是 DBA(數(shù)據(jù)庫管理員)需要為各種性能問題操心,程序員為做出準(zhǔn)確的結(jié)構(gòu)化表,優(yōu)化查詢性能和編寫更優(yōu)代碼,也要費(fèi)盡心思。 在本文中,我列出了一些針對程序員的 MySQL 優(yōu)化技術(shù)。
在我們開始學(xué)習(xí)之前,我補(bǔ)充一點(diǎn):你可以在 Envato Market 上找到大量的 MySQL 腳本和實(shí)用程序。
1.優(yōu)化查詢的查詢緩存
大部分MySQL服務(wù)器都有查詢緩存功能。這是提高性能的最有效的方法之一,這是由數(shù)據(jù)庫引擎私下處理的。當(dāng)同一個(gè)查詢被多次執(zhí)行,結(jié)果會直接從緩存里提取,這樣速度就很快。
主要的問題是,這對程序員來說太簡單了,不容易看到,我們很多人都容易忽略。我們實(shí)際上是可以組織查詢緩存執(zhí)行任務(wù)的。
// query cache does NOT work
$r = mysql_query("SELECT username FROM user WHERE signup_date >= CURDATE()");
// query cache works!
$today = date("Y-m-d");
$r = mysql_query("SELECT username FROM user WHERE signup_date >= '$today'");
查詢緩存在第一行不執(zhí)行的原因在于CURDTE()功能的使用。這適用于所有的非確定性功能,就像NOW()和RAND()等等。。。因?yàn)楣δ芊祷氐慕Y(jié)果是可變的。MySQL決定禁用查詢器的查詢緩存。我們所需要做的是通過添加一額外一行PHP,在查詢前阻止它發(fā)生。
2. EXPLAIN你的選擇查詢
使用EXPLAIN關(guān)鍵詞可以幫助了解MySQL是怎樣運(yùn)行你的查詢的。這有助于發(fā)現(xiàn)瓶頸和查詢或表結(jié)構(gòu)的其它問題。
EXPLAIN的查詢結(jié)果會展示哪一個(gè)索引被使用過,表示怎樣掃描和儲存的,等等。。。
選擇一個(gè)SELECT查詢(一個(gè)有連接的復(fù)雜查詢會更好),在它的前面添加關(guān)鍵詞EXPLAIN,這樣就可以直接使用數(shù)據(jù)庫了。結(jié)果會以一個(gè)漂亮的表來展示。例如,就好比我執(zhí)行連接時(shí)忘了添加一欄的索引:
現(xiàn)在它只會從表2里面掃描9和16行,而非掃描7883行。經(jīng)驗(yàn)法則是乘以所有“行”那一欄的數(shù)字,你的查詢性能會跟結(jié)果數(shù)字成比例的。
3. 獲取唯一行時(shí)使用LIMIT 1
有時(shí)當(dāng)你查表時(shí),你已經(jīng)知道你正在查找的結(jié)果只有一行。你可能正在獲取唯一記錄,或者你可能只是查詢是否存在滿足你的WHERE子句條件的記錄。
在這種情況下,將LIMIT 1添加到查詢條件中可以提高性能。這樣,數(shù)據(jù)庫引擎將在找到剛剛第一個(gè)記錄之后停止掃描記錄,而不是遍歷整個(gè)表或索引。
// do I have any users from Alabama?
// what NOT to do:
$r = mysql_query("SELECT * FROM user WHERE state = 'Alabama'");
if (mysql_num_rows($r) > 0) {
// ...
}
// much better:
$r = mysql_query("SELECT 1 FROM user WHERE state = 'Alabama' LIMIT 1");
if (mysql_num_rows($r) > 0) {
// ...
}
4. 索引搜索字段
索引不僅僅是為了主鍵或唯一鍵。如果你會在你的表中按照任何列搜索,你就都應(yīng)該索引它們。
正如你所看到的,這個(gè)規(guī)則也適用于如 "last_name LIKE 'a%'"的部分字符串搜索。當(dāng)從字符串的開頭搜索時(shí),MySQL就可以使用那一列的索引。
你也應(yīng)該明白什么樣搜索可以不使用有規(guī)律的索引。例如,當(dāng)搜索一個(gè)單詞時(shí)(例如,"WHERE post_content LIKE '%apple%'"),你將不會看到普通索引的好處。你最好使用 mysql 全文搜索或者構(gòu)建你自己的索引解決方案。
5. 索引并對連接使用同樣的字段類型
如果你的應(yīng)用程序包含許多連接查詢, 你需要確保連接的字段在兩張表上都建立了索引。 這會影響MySQL如何內(nèi)部優(yōu)化連接操作。
此外,被連接的字段,需要使用同樣類型。例如, 如果你使用一個(gè)DECIMAL字段, 連接另一張表的INT字段, MySQL將無法使用至少一個(gè)索引。 即使字符編碼也需要使用相同的字符類型。
// looking for companies in my state
$r = mysql_query("SELECT company_name FROM users
LEFT JOIN companies ON (users.state = companies.state)
WHERE users.id = $user_id");
// both state columns should be indexed
// and they both should be the same type and character encoding
// or MySQL might do full table scans
6. 不要ORDER BY RAND()
起初這是一個(gè)聽起來挺酷的技巧, 讓許多菜鳥程序員陷入了這個(gè)陷阱。但你可能不知道,一旦你開始在查詢中使用它,你創(chuàng)建了非??膳碌牟樵兤款i。
如果你真的需要對結(jié)果隨機(jī)排序, 這有一個(gè)更好的方法。補(bǔ)充一些額外代碼,你將可以防止當(dāng)數(shù)據(jù)成指數(shù)級增長時(shí)造成的瓶頸。關(guān)鍵問題是,MySQL必須在排序之前對表中的每一行執(zhí)行RAND()操作(這需要處理能力),并且僅僅給出一行。
// what NOT to do:
$r = mysql_query("SELECT username FROM user ORDER BY RAND() LIMIT 1");
// much better:
$r = mysql_query("SELECT count(*) FROM user");
$d = mysql_fetch_row($r);
$rand = mt_rand(0,$d[0] - 1);
$r = mysql_query("SELECT username FROM user LIMIT $rand, 1");
所以挑選一個(gè)小于結(jié)果數(shù)的隨機(jī)數(shù),并將其用作LIMIT子句中的偏移量。
7. 避免使用SELECT *
從數(shù)據(jù)表中讀取的數(shù)據(jù)越多,查詢操作速度就越慢。它增加了磁盤操作所需的時(shí)間。此外,當(dāng)數(shù)據(jù)庫服務(wù)器與Web服務(wù)器分開時(shí),由于必須在服務(wù)器之間傳輸數(shù)據(jù),將會有更長的網(wǎng)絡(luò)延遲。
這是一個(gè)好習(xí)慣:當(dāng)你使用SELECT語句時(shí)總是指定你需要的列。
// not preferred
$r = mysql_query("SELECT * FROM user WHERE user_id = 1");
$d = mysql_fetch_assoc($r);
echo "Welcome {$d['username']}";
// better:
$r = mysql_query("SELECT username FROM user WHERE user_id = 1");
$d = mysql_fetch_assoc($r);
echo "Welcome {$d['username']}";
// the differences are more significant with bigger result sets
8. 幾乎總是有一個(gè)id字段
在每個(gè)以id列為PRIMARY KEY的數(shù)據(jù)表中,優(yōu)先選擇AUTO_INCREMENT或者INT。 也可以優(yōu)選使用UNSIGNED,因?yàn)樵撝挡荒転樨?fù)的。
即使你擁有一個(gè)具有唯一用戶名字段的用戶表,也不要將其作為主鍵。 VARCHAR字段作為主鍵(檢索)速度較慢。通過內(nèi)部ID引用所有的用戶數(shù)據(jù),你的代碼中將更加結(jié)構(gòu)化。
有些后臺操作是由MySQL引擎本身完成的,它在內(nèi)部使用主鍵字段。當(dāng)數(shù)據(jù)庫設(shè)置越復(fù)雜(集群,分區(qū)等...),這就變得更加重要了。
這個(gè)規(guī)則的一個(gè)可能的例外是“關(guān)聯(lián)表”,用于兩個(gè)表之間的多對多類型的關(guān)聯(lián)。例如,“posts_tags”表中包含兩列:post_id,tag_id,用于保存表名為“post”和“tags”的兩個(gè)表之間的關(guān)系。這些表可以具有包含兩個(gè)id字段的PRIMARY鍵。
9. 相比VARCHAR優(yōu)先使用ENUM
ENUM枚舉類型是非??焖俸途o湊的。在內(nèi)部它們像TINYINT一樣存儲,但它們可以包含和顯示字符串值。這使他們成為某些領(lǐng)域的完美候選。
如果有一個(gè)字段只包含幾種不同的值,請使用ENUM而不是VARCHAR。例如,它可以是名為“status”的列,并且只包含諸如“active”,“inactive”,“pending”,“expired”等的值...
關(guān)于如何重構(gòu)你的數(shù)據(jù)表,甚至有一種方法是可以從MySQL本身得到“建議”。 當(dāng)你有一個(gè)VARCHAR字段,它實(shí)際上建議你將該列類型更改為ENUM。這通過調(diào)用PROCEDURE ANALYZE()來完成。
10. 使用PROCEDURE ANALYSE()獲取建議
PROCEDURE ANALYSE() 將使用MySQL分析列結(jié)構(gòu)和表中的實(shí)際數(shù)據(jù),為你提供一些建議。它只有在數(shù)據(jù)表中有實(shí)際數(shù)據(jù)時(shí)才有用,因?yàn)檫@在分析決策時(shí)很重要。
例如,如果你創(chuàng)建了一個(gè)INT類型的主鍵,但沒有太多行,MySQL則可能建議您改用MEDIUMINT?;蛘呷绻闶褂肰ARCHAR字段,如果表里只有很少的取值,你可能會得到一個(gè)建議是將其轉(zhuǎn)換為ENUM。
你也可以在其中一個(gè)表視圖中單擊phpmyadmin中的“建議表結(jié)構(gòu)”鏈接來執(zhí)行此操作。
請記住,這些只是建議。 如果你的數(shù)據(jù)表變得越來越大,他們甚至可能不是正確的建議。至于如何修改最終是你來決定。
11. 如果可以的話使用NOT NULL
除非你有非常重要的理由使用NULL值,否則你應(yīng)該設(shè)置你的列為NOT NULL。
首先,問一下你自己在空字符串值和NULL值之間(對應(yīng)INT字段:0 vs. NULL)是否有任何的不同.如果沒有理由一起使用這兩個(gè),那么你就不需要一個(gè)NULL字段(你知道在Oracle中NULL和空字符串是一樣的嗎?)。
NULL列需要額外的空間,他們增加了你的比較語句的復(fù)雜度。如果可以的話盡量避免它們。當(dāng)然,我理解一些人,他們也許有非常重要的理由使用NULL值,這不總是一件壞事。
摘自MySQL 文檔:
"NULL列在行記錄它們的值是否為NULL時(shí)需要額外的空間。例如MyISAM 表,每一個(gè)NULL列擁有額外的一個(gè)比特,聚集在最近的字節(jié)。"
12. 預(yù)處理語句
使用預(yù)處理語句有諸多好處,包括更高的性能和更好的安全性。
預(yù)處理語句默認(rèn)情況下會過濾綁定到它的變量,這對于避免SQL注入攻擊極為有效。當(dāng)然你也可以指定要過濾的變量。但這些方法更容易出現(xiàn)人為錯(cuò)誤,也更容易被程序員遺忘。這在使用框架或 ORM 的時(shí)候會出現(xiàn)一些問題。
既然我們關(guān)注性能,那就應(yīng)該說說這個(gè)方面的好處。當(dāng)在應(yīng)用中多次使用同一個(gè)查詢的時(shí)候,它的好處特別明顯。既然向同一個(gè)預(yù)備好的語句中傳入不同的參數(shù)值,MySQL 對這個(gè)語句也只會進(jìn)行一次解析。
同時(shí),最新版本的 MySQL 在傳輸預(yù)備好的語句時(shí)會采用二進(jìn)制形式,這樣做的作用非常明顯,而且對減少網(wǎng)絡(luò)延遲很有幫助。
曾經(jīng)有一段時(shí)間,許多程序員為了一個(gè)重要的原因則避免使用預(yù)處理語句。這個(gè)原因就是,它們不會被MySQL 緩存。不過在 5.1 版本的某個(gè)時(shí)候,查詢緩存也得到的支持。
想在 PHP 中使用預(yù)處理語句,你可以看看 mysqli 擴(kuò)展 或使用數(shù)據(jù)抽象層,如 PDO。
// create a prepared statement
if ($stmt = $mysqli->prepare("SELECT username FROM user WHERE state=?")) {
// bind parameters
$stmt->bind_param("s", $state);
// execute
$stmt->execute();
// bind result variables
$stmt->bind_result($username);
// fetch value
$stmt->fetch();
printf("%s is from %s\n", $username, $state);
$stmt->close();
}
13. 無緩沖查詢
通常當(dāng)你從腳本執(zhí)行一個(gè)查詢,在它可以繼續(xù)后面的任務(wù)之前將需要等待查詢執(zhí)行完成。你可以使用無緩沖的查詢來改變這一情況。
在PHP 文檔中對 mysql_unbuffered_query() f函數(shù)有一個(gè)很好的解釋:
"mysql_unbuffered_query() 發(fā)送SQL查詢語句到MySQL不會像 mysql_query()那樣自動地取并緩沖結(jié)果行。這讓產(chǎn)生大量結(jié)果集的查詢節(jié)省了大量的內(nèi)存,在第一行已經(jīng)被取回時(shí)你就可以立即在結(jié)果集上繼續(xù)工作,而不用等到SQL查詢被執(zhí)行完成。"
然而,它有一定的局限性。你必須在執(zhí)行另一個(gè)查詢之前讀取所有的行或調(diào)用mysql_free_result() 。另外你不能在結(jié)果集上使用mysql_num_rows() 或 mysql_data_seek() 。
14. 使用 UNSIGNED INT 存儲IP地址
很多程序員沒有意識到可以使用整數(shù)類型的字段來存儲 IP 地址,所以一直使用 VARCHAR(15) 類型的字段。使用 INT 只需要 4 個(gè)字節(jié)的空間,而且字段長度固定。
必須確保列是 UNSINGED INT 類型,因?yàn)?IP 地址可能會用到 32 位無符號整型數(shù)據(jù)的每一個(gè)位。
在查詢中可以使用 INET_ATON() 來把一個(gè)IP轉(zhuǎn)換為整數(shù),用 INET_NTOA() 來進(jìn)行相反的操作。在 PHP 也有類似的函數(shù),ip2long() 和 long2ip()。
$r = "UPDATE users SET ip = INET_ATON('{$_SERVER['REMOTE_ADDR']}') WHERE user_id = $user_id";
15. 固定長度(靜態(tài))的表會更快
(譯者注:這里提到的表的長度,實(shí)際是指表頭的長度,即表中每條數(shù)據(jù)占用的空間大小,而不是指表的數(shù)據(jù)量)
如果表中所有列都是“固定長度”,那么這個(gè)表被認(rèn)為是“靜態(tài)”或“固定長度”的。不固定的列類型包括 VARCHAR、TEXT、BLOB等。即使表中只包含一個(gè)這些類型的列,這個(gè)表就不再是固定長度的,MySQL 引擎會以不同的方式來處理它。
固定長度的表會提高性能,因?yàn)?MySQL 引擎在記錄中檢索的時(shí)候速度會更快。如果想讀取表中的某一地,它可以直接計(jì)算出這一行的位置。如果行的大小不固定,那就需要在主鍵中進(jìn)行檢索。
它們也易于緩存,崩潰后容易重建。不過它們也會占用更多空間。例如,如果你把一個(gè) VARCHAR(20) 的字符改為 CHAR(20) 類型,它會總是占用 20 個(gè)字節(jié),不管里面存的是什么內(nèi)容。
你可以使用“垂直分區(qū)”技術(shù),將長度變化的列拆分到另一張表中。來看看:
16. 垂直分區(qū)
垂直分區(qū)是為了優(yōu)化表結(jié)構(gòu)而對其進(jìn)行縱向拆分的行為。
示例 1: 你可能會有一張用戶表,包含家庭住址,而這個(gè)不是一個(gè)常用數(shù)據(jù)。這時(shí)候你可以選擇把表拆分開,將住址信息保存到另一個(gè)表中。這樣你的主用戶表就會更小。如你所知,表越小越快。
示例 2: 表中有一個(gè) "last_login" 字段,用戶每次登錄網(wǎng)站都會更新這個(gè)字段,而每次更新都會導(dǎo)致這個(gè)表緩存的查詢數(shù)據(jù)被清空。這種情況下你可以將那個(gè)字段放到另一張表里,保持用戶表更新量最小。
不過你也需要確保不會經(jīng)常聯(lián)合查詢分開后的兩張表,要不然你就得忍受由這帶來的性能下降。
17. 拆分大型DELETE或INSERT語句
如果你需要在網(wǎng)站上執(zhí)行大型DELETE或INSERT查詢,則需要注意不要影響網(wǎng)絡(luò)流量。當(dāng)執(zhí)行大型語句時(shí),它會鎖表并使你的Web應(yīng)用程序停止。
Apache運(yùn)行許多并行進(jìn)程/線程。 因此它執(zhí)行腳本效率很高。所以服務(wù)器不期望打開過多的連接和進(jìn)程,這很消耗資源,特別是內(nèi)存。
如果你鎖表很長時(shí)間(如30秒或更長),在一個(gè)高流量的網(wǎng)站,會導(dǎo)致進(jìn)程和查詢堆積,處理這些進(jìn)程和查詢可能需要很長時(shí)間,最終甚至使你的網(wǎng)站崩潰。
如果你的維護(hù)腳本需要刪除大量的行,只需使用LIMIT子句,以避免阻塞。
while (1) {
mysql_query("DELETE FROM logs WHERE log_date = '2009-10-01' LIMIT 10000");
if (mysql_affected_rows() == 0) {
// done deleting
break;
}
// you can even pause a bit
usleep(50000);
}
18. 越小的列越快
對于數(shù)據(jù)庫引擎來說,磁盤空間可能是最需要注意的瓶頸。對性能而言,“小”和“緊縮”有助于減少磁盤傳輸量。
MySQL 文檔中有一個(gè)列表,列舉了各種數(shù)據(jù)類型所需要的存儲空間。
如果數(shù)據(jù)表預(yù)計(jì)只會有少量的行,那就沒必要把主鍵定義為 INT 類型,可以用 MEDIUMINT、SMALLINT 甚至 TINYINT 來代替。(譯者注:對于日期數(shù)據(jù),)如果不需要時(shí)間部分,就應(yīng)該使用 DATE 而不是 DATETIME。
請確保留出合理的數(shù)據(jù)成長空間,不然就可能造成像Slashdot那樣的結(jié)果(譯者注:Slashdot 因?yàn)閿?shù)據(jù)增長將評論表的主鍵改為了 INT 型,但沒有修改其父表中的相應(yīng)的數(shù)據(jù)類型,雖然一個(gè) ALTER 語句就可以解決問題,但是需要至少停止某些業(yè)務(wù)三個(gè)小時(shí))。
19. 選擇正確的存儲引擎
MySQL 有兩個(gè)主要的存儲引擎:MyISAM 和 InnoDB,它們各有利弊。
MyISAM 適用于讀請求特別多的應(yīng)用,但不適用于有大量寫請求的情況。甚至你只是要更新一行中的某個(gè)字段,都會造成整張表被鎖,然后直到這個(gè)查詢完成,其它進(jìn)程都不能從這張表讀取數(shù)據(jù)。MyISAM 在計(jì)算 SELECT COUNT(*) 這種類型的查詢時(shí)速度非???。
InnoDB 是一個(gè)復(fù)雜的存儲引擎,在多數(shù)小型應(yīng)用中它比 MyISAM 慢。但是它支持行級鎖,有更好的尺度。它還支持一些高級特性,比如事務(wù)。
20. 使用對象關(guān)系映射器(ORM, Object Relational Mapper)
通過使用ORM(對象關(guān)系映射器),你可以獲得一定的性能提升。ORM可以完成的一切事情,手動編碼也可完成。但這可能意味著需要太多額外的工作,并且需要高水平的專業(yè)知識。
ORM以“延遲加載”著稱。這意味著它們僅在需要時(shí)獲取實(shí)際值。但是你需要小心處理他們,否則你可能最終創(chuàng)建了許多微型查詢,這會降低數(shù)據(jù)庫性能。
ORM還可以將多個(gè)查詢批處理到事務(wù)中,其操作速度比向數(shù)據(jù)庫發(fā)送單個(gè)查詢快得多。
目前我最喜歡的PHP-ORM是Doctrine。我寫了一篇關(guān)于如何安裝Doctrine與CodeIgniter的文章(install Doctrine with CodeIgniter)。
21. 小心使用持久連接
持久連接意味著減少重建連接到MySQL的成本。 當(dāng)持久連接被創(chuàng)建時(shí),它將保持打開狀態(tài)直到腳本完成運(yùn)行。 因?yàn)锳pache重用它的子進(jìn)程,下一次進(jìn)程運(yùn)行一個(gè)新的腳本時(shí),它將重用相同的MySQL連接。
理論上看起來不錯(cuò)。 但從我個(gè)人(和許多其他人)的經(jīng)驗(yàn)看來,這個(gè)功能可能會導(dǎo)致更多麻煩。 你可能會出現(xiàn)連接數(shù)限制問題、內(nèi)存問題等等。
Apache總是并行運(yùn)行的,它創(chuàng)建許多子進(jìn)程。 這是持久連接在這種環(huán)境中不能很好工作的主要原因。 在你考慮使用mysql_pconnect()之前,請咨詢你的系統(tǒng)管理員。
原文出處: Burak Guzel 譯文出處:開源中國