Mysql優化建議

今天,數據庫的操作越來越成爲整個應用的性能瓶頸了,這點對於Web應用尤其明顯。關於數據庫的性能,這並不只是DBA才需要擔心的事,而這更是我們程序員需要去關注的事情。當我們去設計數據庫表結構,對操作數據庫時(尤其是查表時的SQL語句),我們都需要注意數據操作的性能。這裏,我們不會講過多的SQL語句的優化,而只是針對MySQL這一Web應用最多的數據庫。希望下面的這些優化技巧對你有用。
1. 爲查詢緩存優化你的查詢
大多數的MySQL服務器都開啓了查詢緩存。這是提高性最有效的方法之一,而且這是被MySQL的數據庫引擎處理的。當有很多相同的查詢被執行了多次的時候,這些查詢結果會被放到一個緩存中,這樣,後續的相同的查詢就不用操作表而直接訪問緩存結果了。
這裏最主要的問題是,對於程序員來說,這個事情是很容易被忽略的。因爲,我們某些查詢語句會讓MySQL不使用緩存。請看下面的示例:
// 查詢緩存不開啓
$r = mysql_query("SELECT username FROM user WHERE signup_date >= CURDATE()");
// 開啓查詢緩存
$today = date("Y-m-d");
$r = mysql_query("SELECT username FROM user WHERE signup_date >= '$today'");
上面兩條SQL語句的差別就是 CURDATE() ,MySQL的查詢緩存對這個函數不起作用。所以,像 NOW() 和 RAND()或是其它的諸如此類的SQL函數都不會開啓查詢緩存,因爲這些函數的返回是會不定的易變的。所以,你所需要的就是用一個變量來代替MySQL的函數,從而開啓緩存。
2. EXPLAIN 你的 SELECT 查詢
使用 EXPLAIN 關鍵字可以讓你知道MySQL是如何處理你的SQL語句的。這可以幫你分析你的查詢語句或是表結構的性能瓶頸。
EXPLAIN 的查詢結果還會告訴你你的索引主鍵被如何利用的,你的數據表是如何被搜索和排序的……等等,等等。
挑一個你的SELECT語句(推薦挑選那個最複雜的,有多表聯接的),把關鍵字EXPLAIN加到前面。你可以使用phpmyadmin來做這個事。然後,你會看到一張表格。下面的這個示例中,我們忘記加上了group_id索引,並且有表聯接:
當我們爲 group_id 字段加上索引後:
我們可以看到,前一個結果顯示搜索了 7883 行,而後一個只是搜索了兩個表的 9 和 16行。查看rows列可以讓我們找到潛在的性能問題。
3. 當只要一行數據時使用 LIMIT 1
當你查詢表的有些時候,你已經知道結果只會有一條結果,但因爲你可能需要去fetch遊標,或是你也許會去檢查返回的記錄數。
在這種情況下,加上 LIMIT 1可以增加性能。這樣一樣,MySQL數據庫引擎會在找到一條數據後停止搜索,而不是繼續往後查少下一條符合記錄的數據。
下面的示例,只是爲了找一下是否有“中國”的用戶,很明顯,後面的會比前面的更有效率。(請注意,第一條中是Select*,第二條是Select 1)
// 沒有效率的:
$r = mysql_query("SELECT * FROM user WHERE country = 'China'");
if (mysql_num_rows($r) > 0) {
// ...
}

// 有效率的:
$r = mysql_query("SELECT 1 FROM user WHERE country = 'China' LIMIT 1");
if (mysql_num_rows($r) > 0) {
// ...
}
4. 爲搜索字段建索引
索引並不一定就是給主鍵或是唯一的字段。如果在你的表中,有某個字段你總要會經常用來做搜索,那麼,請爲其建立索引吧。
從上圖你可以看到那個搜索字串 “last_name LIKE ‘a%'”,一個是建了索引,一個是沒有索引,性能差了4倍左右。
另外,你應該也需要知道什麼樣的搜索是不能使用正常的索引的。例如,當你需要在一篇大的文章中搜索一個詞時,如: “WHEREpost_content LIKE ‘%apple%'”,索引可能是沒有意義的。你可能需要使用MySQL全文索引或是自己做一個索引(比如說:搜索關鍵詞或是Tag什麼的)
5. 在Join表的時候使用相當類型的例,並將其索引
如果你的應用程序有很多 JOIN查詢,你應該確認兩個表中Join的字段是被建過索引的。這樣,MySQL內部會啓動爲你優化Join的SQL語句的機制。
而且,這些被用來Join的字段,應該是相同的類型的。例如:如果你要把 DECIMAL 字段和一個 INT字段Join在一起,MySQL就無法使用它們的索引。對於那些STRING類型,還需要有相同的字符集才行。(兩個表的字符集有可能不一樣)
// 在state中查找company
$r = mysql_query("SELECT company_name FROM users
LEFT JOIN companies ON (users.state = companies.state)
WHERE users.id = $user_id");

// 兩個 state 字段應該是被建過索引的,而且應該是相當的類型,相同的字符集。
6. 千萬不要 ORDER BY RAND()
想打亂返回的數據行?隨機挑一個數據?真不知道誰發明了這種用法,但很多新手很喜歡這樣用。但你確不瞭解這樣做有多麼可怕的性能問題。
如果你真的想把返回的數據行打亂了,你有N種方法可以達到這個目的。這樣使用只讓你的數據庫的性能呈指數級的下降。這裏的問題是:MySQL會不得不去執行RAND()函數(很耗CPU時間),而且這是爲了每一行記錄去記行,然後再對其排序。就算是你用了Limit1也無濟於事(因爲要排序)
下面的示例是隨機挑一條記錄


// 千萬不要這樣做:
$r = mysql_query("SELECT username FROM user ORDER BY RAND() LIMIT 1");

// 這要會更好:
$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");
7. 避免 SELECT *
從數據庫裏讀出越多的數據,那麼查詢就會變得越慢。並且,如果你的數據庫服務器和WEB服務器是兩臺獨立的服務器的話,這還會增加網絡傳輸的負載。
所以,你應該養成一個需要什麼就取什麼的好的習慣。


// 不推薦
$r = mysql_query("SELECT * FROM user WHERE user_id = 1");
$d = mysql_fetch_assoc($r);
echo "Welcome {$d['username']}";

// 推薦
$r = mysql_query("SELECT username FROM user WHERE user_id = 1");
$d = mysql_fetch_assoc($r);
echo "Welcome {$d['username']}";
8. 永遠爲每張表設置一個ID
我們應該爲數據庫裏的每張表都設置一個ID做爲其主鍵,而且最好的是一個INT型的(推薦使用UNSIGNED),並設置上自動增加的AUTO_INCREMENT標誌。
就算是你 users 表有一個主鍵叫 “email”的字段,你也別讓它成爲主鍵。使用 VARCHAR類型來當主鍵會使用得性能下降。另外,在你的程序中,你應該使用表的ID來構造你的數據結構。
而且,在MySQL數據引擎下,還有一些操作需要使用主鍵,在這些情況下,主鍵的性能和設置變得非常重要,比如,集羣,分區……
在這裏,只有一個情況是例外,那就是“關聯表”的“外鍵”,也就是說,這個表的主鍵,通過若干個別的表的主鍵構成。我們把這個情況叫做“外鍵”。比如:有一個“學生表”有學生的ID,有一個“課程表”有課程ID,那麼,“成績表”就是“關聯表”了,其關聯了學生表和課程表,在成績表中,學生ID和課程ID叫“外鍵”其共同組成主鍵。
9. 使用 ENUM 而不是 VARCHAR
ENUM 類型是非常快和緊湊的。在實際上,其保存的是TINYINT,但其外表上顯示爲字符串。這樣一來,用這個字段來做一些選項列表變得相當的完美。
如果你有一個字段,比如“性別”,“國家”,“民族”,“狀態”或“部門”,你知道這些字段的取值是有限而且固定的,那麼,你應該使用ENUM 而不是 VARCHAR。
MySQL也有一個“建議”(見第十條)告訴你怎麼去重新組織你的表結構。當你有一個 VARCHAR 字段時,這個建議會告訴你把其改成ENUM 類型。使用 PROCEDURE ANALYSE() 你可以得到相關的建議。
10. 從 PROCEDURE ANALYSE() 取得建議
PROCEDURE ANALYSE() 會讓 MySQL幫你去分析你的字段和其實際的數據,並會給你一些有用的建議。只有表中有實際的數據,這些建議纔會變得有用,因爲要做一些大的決定是需要有數據作爲基礎的。
例如,如果你創建了一個 INT 字段作爲你的主鍵,然而並沒有太多的數據,那麼,PROCEDUREANALYSE()會建議你把這個字段的類型改成 MEDIUMINT 。或是你使用了一個 VARCHAR字段,因爲數據不多,你可能會得到一個讓你把它改成 ENUM 的建議。這些建議,都是可能因爲數據不夠多,所以決策做得就不夠準。
在phpmyadmin裏,你可以在查看錶時,點擊 “Propose table structure” 來查看這些建議

一定要注意,這些只是建議,只有當你的表裏的數據越來越多時,這些建議纔會變得準確。一定要記住,你纔是最終做決定的人。


11. 儘可能的使用 NOT NULL
除非你有一個很特別的原因去使用 NULL 值,你應該總是讓你的字段保持 NOT NULL。這看起來好像有點爭議,請往下看。
首先,問問你自己“Empty”和“NULL”有多大的區別(如果是INT,那就是0和NULL)?如果你覺得它們之間沒有什麼區別,那麼你就不要使用NULL。(你知道嗎?在Oracle 裏,NULL 和 Empty 的字符串是一樣的!)
不要以爲 NULL 不需要空間,其需要額外的空間,並且,在你進行比較的時候,你的程序會更復雜。當然,這裏並不是說你就不能使用NULL了,現實情況是很複雜的,依然會有些情況下,你需要使用NULL值。
下面摘自MySQL自己的文檔:
1
“NULL columns require additional space in the row to record whether their values are NULL. For MyISAM tables, each NULL column takes one bit extra, rounded up to the nearest byte.”
12. Prepared Statements
Prepared Statements很像存儲過程,是一種運行在後臺的SQL語句集合,我們可以從使用 preparedstatements 獲得很多好處,無論是性能問題還是安全問題。
Prepared Statements可以檢查一些你綁定好的變量,這樣可以保護你的程序不會受到“SQL注入式”攻擊。當然,你也可以手動地檢查你的這些變量,然而,手動的檢查容易出問題,而且很經常會被程序員忘了。當我們使用一些framework或是ORM的時候,這樣的問題會好一些。
在性能方面,當一個相同的查詢被使用多次的時候,這會爲你帶來可觀的性能優勢。你可以給這些PreparedStatements定義一些參數,而MySQL只會解析一次。
雖然最新版本的MySQL在傳輸Prepared Statements是使用二進制形勢,所以這會使得網絡傳輸非常有效率。
當然,也有一些情況下,我們需要避免使用Prepared Statements,因爲其不支持查詢緩存。但據說版本5.1後支持了。
在PHP中要使用prepared statements,你可以查看其使用手冊:mysqli 擴展 或是使用數據庫抽象層,如:PDO.
// 創建 prepared statement
if ($stmt = $mysqli->prepare("SELECT username FROM user WHERE state=?")) {

// 綁定參數
$stmt->bind_param("s", $state);

// 執行
$stmt->execute();

// 綁定結果
$stmt->bind_result($username);

// 移動遊標
$stmt->fetch();

printf("%s is from %s\n", $username, $state);

$stmt->close();
}


13. 無緩衝的查詢
正常的情況下,當你在當你在你的腳本中執行一個SQL語句的時候,你的程序會停在那裏直到沒這個SQL語句返回,然後你的程序再往下繼續執行。你可以使用無緩衝查詢來改變這個行爲。
關於這個事情,在PHP的文檔中有一個非常不錯的說明: mysql_unbuffered_query() 函數:
1
“mysql_unbuffered_query() sends the SQL query query to MySQL without automatically fetching and buffering the result rows as mysql_query() does. This saves a considerable amount of memory with SQL queries that produce large result sets, and you can start working on the result set immediately after the first row has been retrieved as you don’t have to wait until the complete SQL query has been performed.”
上面那句話翻譯過來是說,mysql_unbuffered_query()發送一個SQL語句到MySQL而並不像mysql_query()一樣去自動fethch和緩存結果。這會相當節約很多可觀的內存,尤其是那些會產生大量結果的查詢語句,並且,你不需要等到所有的結果都返回,只需要第一行數據返回的時候,你就可以開始馬上開始工作於查詢結果了。
然而,這會有一些限制。因爲你要麼把所有行都讀走,或是你要在進行下一次的查詢前調用 mysql_free_result()清除結果。而且, mysql_num_rows() 或 mysql_data_seek()將無法使用。所以,是否使用無緩衝的查詢你需要仔細考慮。
14. 把IP地址存成 UNSIGNED INT
很多程序員都會創建一個 VARCHAR(15)字段來存放字符串形式的IP而不是整形的IP。如果你用整形來存放,只需要4個字節,並且你可以有定長的字段。而且,這會爲你帶來查詢上的優勢,尤其是當你需要使用這樣的WHERE條件:IPbetween ip1 and ip2。
我們必需要使用UNSIGNED INT,因爲 IP地址會使用整個32位的無符號整形。
而你的查詢,你可以使用 INET_ATON() 來把一個字符串IP轉成一個整形,並使用 INET_NTOA()把一個整形轉成一個字符串IP。在PHP中,也有這樣的函數 ip2long() 和 long2ip()。

發佈了33 篇原創文章 · 獲贊 15 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章