轉載: https://code.tutsplus.com/tutorials/top-20-mysql-best-practices–net-7855
資料庫操作是當今 Web 應用程式中的主要瓶頸。 不僅是 DBA(資料庫管理者)需要為各種性能問題操心,程式員為做出準确的結構化表,優化查詢性能和編寫更優代碼,也要費盡心思。 在本文中,我列出了一些針對程式員的 MySQL 優化技術。
在我們開始學習之前,我補充一點:你可以在 Envato Market 上找到大量的 MySQL 腳本和實用程式。
1.優化查詢的查詢緩存
大部分MySQL伺服器都有查詢緩存功能。這是提高性能的最有效的方法之一,這是由資料庫引擎私下處理的。當同一個查詢被多次執行,結果會直接從緩存裡提取,這樣速度就很快。
主要的問題是,這對程式員來說太簡單了,不容易看到,我們很多人都容易忽略。我們實際上是可以組織查詢緩存執行任務的。
// 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'");
查詢緩存在第一行不執行的原因在于CURDTE()功能的使用。這适用于所有的非确定性功能,就像NOW()和RAND()等等。。。因為功能傳回的結果是可變的。MySQL決定禁用查詢器的查詢緩存。我們所需要做的是通過添加一額外一行PHP,在查詢前阻止它發生。
2. EXPLAIN你的選擇查詢
使用EXPLAIN關鍵詞可以幫助了解MySQL是怎樣運作你的查詢的。這有助于發現瓶頸和查詢或表結構的其它問題。
EXPLAIN的查詢結果會展示哪一個索引被使用過,表示怎樣掃描和儲存的,等等。。。
選擇一個SELECT查詢(一個有連接配接的複雜查詢會更好),在它的前面添加關鍵詞EXPLAIN,這樣就可以直接使用資料庫了。結果會以一個漂亮的表來展示。例如,就好比我執行連接配接時忘了添加一欄的索引:
現在它隻會從表2裡面掃描9和16行,而非掃描7883行。經驗法則是乘以所有“行”那一欄的數字,你的查詢性能會跟結果數字成比例的。
3. 擷取唯一行時使用LIMIT 1
有時當你查表時,你已經知道你正在查找的結果隻有一行。你可能正在擷取唯一記錄,或者你可能隻是查詢是否存在滿足你的WHERE子句條件的記錄。
在這種情況下,将LIMIT 1添加到查詢條件中可以提高性能。這樣,資料庫引擎将在找到剛剛第一個記錄之後停止掃描記錄,而不是周遊整個表或索引。
// 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. 索引搜尋字段
索引不僅僅是為了主鍵或唯一鍵。如果你會在你的表中按照任何列搜尋,你就都應該索引它們。
正如你所看到的,這個規則也适用于如 “last_name LIKE ‘a%’”的部分字元串搜尋。當從字元串的開頭搜尋時,MySQL就可以使用那一列的索引。
你也應該明白什麼樣搜尋可以不使用有規律的索引。例如,當搜尋一個單詞時(例如,”WHERE post_content LIKE ‘%apple%’”),你将不會看到普通索引的好處。你最好使用 mysql 全文搜尋或者建構你自己的索引解決方案。
5. 索引并對連接配接使用同樣的字段類型
如果你的應用程式包含許多連接配接查詢, 你需要確定連接配接的字段在兩張表上都建立了索引。 這會影響MySQL如何内部優化連接配接操作。
此外,被連接配接的字段,需要使用同樣類型。例如, 如果你使用一個DECIMAL字段, 連接配接另一張表的INT字段, MySQL将無法使用至少一個索引。 即使字元編碼也需要使用相同的字元類型。
// 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()
起初這是一個聽起來挺酷的技巧, 讓許多菜鳥程式員陷入了這個陷阱。但你可能不知道,一旦你開始在查詢中使用它,你建立了非常可怕的查詢瓶頸。
如果你真的需要對結果随機排序, 這有一個更好的方法。補充一些額外代碼,你将可以防止當資料成指數級增長時造成的瓶頸。關鍵問題是,MySQL必須在排序之前對表中的每一行執行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");
是以挑選一個小于結果數的随機數,并将其用作LIMIT子句中的偏移量。
7. 避免使用SELECT *
從資料表中讀取的資料越多,查詢操作速度就越慢。它增加了磁盤操作所需的時間。此外,當資料庫伺服器與Web伺服器分開時,由于必須在伺服器之間傳輸資料,将會有更長的網絡延遲。
這是一個好習慣:當你使用SELECT語句時總是指定你需要的列。
// 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. 幾乎總是有一個id字段
在每個以id列為PRIMARY KEY的資料表中,優先選擇AUTO_INCREMENT或者INT。 也可以優選使用UNSIGNED,因為該值不能為負的。
即使你擁有一個具有唯一使用者名字段的使用者表,也不要将其作為主鍵。 VARCHAR字段作為主鍵(檢索)速度較慢。通過内部ID引用所有的使用者資料,你的代碼中将更加結構化。
有些背景操作是由MySQL引擎本身完成的,它在内部使用主鍵字段。當資料庫設定越複雜(叢集,分區等…),這就變得更加重要了。
這個規則的一個可能的例外是“關聯表”,用于兩個表之間的多對多類型的關聯。例如,“posts_tags”表中包含兩列:post_id,tag_id,用于儲存表名為“post”和“tags”的兩個表之間的關系。這些表可以具有包含兩個id字段的PRIMARY鍵。
9. 相比VARCHAR優先使用ENUM
ENUM枚舉類型是非常快速和緊湊的。在内部它們像TINYINT一樣存儲,但它們可以包含和顯示字元串值。這使他們成為某些領域的完美候選。
如果有一個字段隻包含幾種不同的值,請使用ENUM而不是VARCHAR。例如,它可以是名為“status”的列,并且隻包含諸如“active”,“inactive”,“pending”,“expired”等的值…
關于如何重構你的資料表,甚至有一種方法是可以從MySQL本身得到“建議”。 當你有一個VARCHAR字段,它實際上建議你将該列類型更改為ENUM。這通過調用PROCEDURE ANALYZE()來完成。
10. 使用PROCEDURE ANALYSE()擷取建議
PROCEDURE ANALYSE() 将使用MySQL分析列結構和表中的實際資料,為你提供一些建議。它隻有在資料表中有實際資料時才有用,因為這在分析決策時很重要。
例如,如果你建立了一個INT類型的主鍵,但沒有太多行,MySQL則可能建議您改用MEDIUMINT。或者如果你使用VARCHAR字段,如果表裡隻有很少的取值,你可能會得到一個建議是将其轉換為ENUM。
你也可以在其中一個表視圖中單擊phpmyadmin中的“建議表結構”連結來執行此操作。
請記住,這些隻是建議。 如果你的資料表變得越來越大,他們甚至可能不是正确的建議。至于如何修改最終是你來決定。
11. 如果可以的話使用NOT NULL
除非你有非常重要的理由使用NULL值,否則你應該設定你的列為NOT NULL。
首先,問一下你自己在空字元串值和NULL值之間(對應INT字段:0 vs. NULL)是否有任何的不同.如果沒有理由一起使用這兩個,那麼你就不需要一個NULL字段(你知道在Oracle中NULL和空字元串是一樣的嗎?)。
NULL列需要額外的空間,他們增加了你的比較語句的複雜度。如果可以的話盡量避免它們。當然,我了解一些人,他們也許有非常重要的理由使用NULL值,這不總是一件壞事。
摘自MySQL 文檔:
“
NULL列在行記錄它們的值是否為NULL時需要額外的空間。例如MyISAM 表,每一個NULL列擁有額外的一個比特,聚集在最近的位元組。"
12. 預處理語句
使用預處理語句有諸多好處,包括更高的性能和更好的安全性。
預處理語句預設情況下會過濾綁定到它的變量,這對于避免SQL注入攻擊極為有效。當然你也可以指定要過濾的變量。但這些方法更容易出現人為錯誤,也更容易被程式員遺忘。這在使用架構或 ORM 的時候會出現一些問題。
既然我們關注性能,那就應該說說這個方面的好處。當在應用中多次使用同一個查詢的時候,它的好處特别明顯。既然向同一個預備好的語句中傳入不同的參數值,MySQL 對這個語句也隻會進行一次解析。
同時,最新版本的 MySQL 在傳輸預備好的語句時會采用二進制形式,這樣做的作用非常明顯,而且對減少網絡延遲很有幫助。
曾經有一段時間,許多程式員為了一個重要的原因則避免使用預處理語句。這個原因就是,它們不會被MySQL 緩存。不過在 5.1 版本的某個時候,查詢緩存也得到的支援。
想在 PHP 中使用預處理語句,你可以看看 mysqli 擴充 或使用資料抽象層,如 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. 無緩沖查詢
通常當你從腳本執行一個查詢,在它可以繼續後面的任務之前将需要等待查詢執行完成。你可以使用無緩沖的查詢來改變這一情況。
在PHP 文檔中對 mysql_unbuffered_query() f函數有一個很好的解釋:
“
"mysql_unbuffered_query() 發送SQL查詢語句到MySQL不會像 mysql_query()那樣自動地取并緩沖結果行。這讓産生大量結果集的查詢節省了大量的記憶體,在第一行已經被取回時你就可以立即在結果集上繼續工作,而不用等到SQL查詢被執行完成。"
然而,它有一定的局限性。你必須在執行另一個查詢之前讀取所有的行或調用mysql_free_result() 。另外你不能在結果集上使用mysql_num_rows() 或 mysql_data_seek() 。
14. 使用 UNSIGNED INT 存儲IP位址
很多程式員沒有意識到可以使用整數類型的字段來存儲 IP 位址,是以一直使用 VARCHAR(15) 類型的字段。使用 INT 隻需要 4 個位元組的空間,而且字段長度固定。
必須確定列是 UNSINGED INT 類型,因為 IP 位址可能會用到 32 位無符号整型資料的每一個位。
在查詢中可以使用 INET_ATON() 來把一個IP轉換為整數,用 INET_NTOA() 來進行相反的操作。在 PHP 也有類似的函數,ip2long() 和 long2ip()。
$r = "UPDATE users SET ip = INET_ATON('{$_SERVER['REMOTE_ADDR']}') WHERE user_id = $user_id";
15. 固定長度(靜态)的表會更快
(譯者注:這裡提到的表的長度,實際是指表頭的長度,即表中每條資料占用的空間大小,而不是指表的資料量)
如果表中所有列都是“固定長度”,那麼這個表被認為是“靜态”或“固定長度”的。不固定的列類型包括 VARCHAR、TEXT、BLOB等。即使表中隻包含一個這些類型的列,這個表就不再是固定長度的,MySQL 引擎會以不同的方式來處理它。
固定長度的表會提高性能,因為 MySQL 引擎在記錄中檢索的時候速度會更快。如果想讀取表中的某一地,它可以直接計算出這一行的位置。如果行的大小不固定,那就需要在主鍵中進行檢索。
它們也易于緩存,崩潰後容易重建。不過它們也會占用更多空間。例如,如果你把一個 VARCHAR(20) 的字元改為 CHAR(20) 類型,它會總是占用 20 個位元組,不管裡面存的是什麼内容。
你可以使用“垂直分區”技術,将長度變化的列拆分到另一張表中。來看看:
16. 垂直分區
垂直分區是為了優化表結構而對其進行縱向拆分的行為。
示例 1: 你可能會有一張使用者表,包含家庭住址,而這個不是一個常用資料。這時候你可以選擇把表拆分開,将住址資訊儲存到另一個表中。這樣你的主使用者表就會更小。如你所知,表越小越快。
示例 2: 表中有一個 “last_login” 字段,使用者每次登入網站都會更新這個字段,而每次更新都會導緻這個表緩存的查詢資料被清空。這種情況下你可以将那個字段放到另一張表裡,保持使用者表更新量最小。
不過你也需要確定不會經常聯合查詢分開後的兩張表,要不然你就得忍受由這帶來的性能下降。
17. 拆分大型DELETE或INSERT語句
如果你需要在網站上執行大型DELETE或INSERT查詢,則需要注意不要影響網絡流量。當執行大型語句時,它會鎖表并使你的Web應用程式停止。
Apache運作許多并行程序/線程。 是以它執行腳本效率很高。是以伺服器不期望打開過多的連接配接和程序,這很消耗資源,特别是記憶體。
如果你鎖表很長時間(如30秒或更長),在一個高流量的網站,會導緻程序和查詢堆積,處理這些程序和查詢可能需要很長時間,最終甚至使你的網站崩潰。
如果你的維護腳本需要删除大量的行,隻需使用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.越少的列越快
對于資料庫引擎,磁盤可能是最重要的瓶頸。更小更緊湊的資料、減少磁盤傳輸量,通常有助于性能提高。
MySQL文檔Storage Requirements 有所有資料類型清單。
如果已知表具有很少的行,則沒有理由是主鍵類型為INT,可以用MEDIUMINT、SMALLINT代替,甚至在某些情況下使用TINYINT。 如果不需要完整時間記錄,請使用DATE而不是DATETIME。
確定留下合理的擴充空間,不然你可能會像Slashdot這樣。
19. 選擇正确的存儲引擎
MySQL有兩個主要存儲引擎,MyISAM和InnoDB。 每個都有自己的優點和缺點。
MyISAM适用于讀取繁重的應用程式,但是當有很多寫入時它不能很好地擴充。 即使你正在更新一行的一個字段,整個表也被鎖定,并且在語句執行完成之前,其他程序甚至無法讀取該字段。 MyISAM在計算SELECT COUNT(*)的查詢時非常快。
InnoDB是一個更複雜的存儲引擎,對于大多數小的應用程式,它比MyISAM慢。 但它支援基于行的鎖定,使其更好地擴充。 它還支援一些更進階的功能,比如事務。
● MyISAM存儲引擎
● InnoDB存儲引擎
20. 使用對象關系映射器(ORM, Object Relational Mapper)
通過使用ORM(對象關系映射器),你可以獲得一定的性能提升。ORM可以完成的一切事情,手動編碼也可完成。但這可能意味着需要太多額外的工作,并且需要高水準的專業知識。
ORM以“延遲加載”著稱。這意味着它們僅在需要時擷取實際值。但是你需要小心處理他們,否則你可能最終建立了許多微型查詢,這會降低資料庫性能。
ORM還可以将多個查詢批處理到事務中,其操作速度比向資料庫發送單個查詢快得多。
目前我最喜歡的PHP-ORM是Doctrine。我寫了一篇關于如何安裝Doctrine與CodeIgniter的文章(install Doctrine with CodeIgniter)。
21. 小心使用持久連接配接
持久連接配接意味着減少重建連接配接到MySQL的成本。 當持久連接配接被建立時,它将保持打開狀态直到腳本完成運作。 因為Apache重用它的子程序,下一次程序運作一個新的腳本時,它将重用相同的MySQL連接配接。
● PHP:mysql_pconnect()
理論上看起來不錯。 但從我個人(和許多其他人)的經驗看來,這個功能可能會導緻更多麻煩。 你可能會出現連接配接數限制問題、記憶體問題等等。
Apache總是并行運作的,它建立許多子程序。 這是持久連接配接在這種環境中不能很好工作的主要原因。 在你考慮使用mysql_pconnect()之前,請咨詢你的系統管理者。