mysql支持的數據類型很是多,正確選擇的數據類型對於得到高性能相當重要。無論存儲哪一種類型的數據,下面幾個簡單的原則都有助於作出更好的選擇:mysql
更小一般更好,儘可能使用能夠正確存儲數據的最小數據類型,更小的數據類型一般更快,由於它們佔用更少的磁盤、內存和CPU緩存,而且處理時須要的CPU週期也更少sql
簡單就好,簡單數據類型的操做一般須要更少的CPU週期。例如,整型比字符操做代價更低,由於字符集和校對規則(排序規則)使字符比較比整型比較更復雜。使用mysql內建的類型(data、time、datatime)而不是字符串來存儲日期和時間,用整型存儲IP地址數據庫
儘可能避免NULL,最好指定列爲NOT NULL,除非真的須要存儲NULL值。若是查詢中包含可爲NULL的列,對mysql來講更難優化,由於可爲NULL的列使得索引、索引統計和值比較都更復雜。可爲NULL的列會使用更多的存儲空間,在mysql中也須要特殊處理。當可爲NULL的列被索引時,每一個索引記錄須要一個額外的字節。InnoDB使用單獨的位(bit)存儲NULL值。緩存
有兩種類型的數字:整數和實數。若是存儲整數,可使用這幾種整數類型:tinyint、smallint、mediumint、int、bigint,分別使用:八、1六、2四、3二、64位存儲空間,它們能夠存儲值的範圍從-2^(N-1)到(2^(N-1))-1,其中N是存儲空間的位數。服務器
整數類型有可選的unsigned屬性,表示不容許負值,這大體可使正數的上限提升一倍,例如無符號型tinyint能夠存儲的範圍是0~255,而tinyint的存儲範圍是-128~127。有符號和無符號類型使用相同的存儲空間,並具備相同的性能,所以能夠根據實際狀況選擇合適的類型。數據結構
mysql能夠爲整數類型提供寬度,如int(11),對大多數應用這是沒有意義的:它不會限制值的合法範圍,只是規定了mysql的一些交互工具用來顯示字符的個數。對於存儲和計算來講,int(1)和int(20)相同。併發
實數是帶有小數部分的數字,然而,它們不僅是爲了存儲小數部分,也可使用decimal存儲比bigint還大的整數。mysql既支持精確類型,也支持不精確類型:ide
float和double類型支持使用標準的浮點運算進行近似計算 decimal類型用於存儲精確的小數 浮點和decimal類型均可以指定精度,對於decimal列,能夠指定小數點先後所容許的最大位數,這會影響列的空間消耗。mysql 5.0和更高版本將數字打包保存到一個二進制字符串中(每4個字節存9個數字)。例如,decimal(18,9)小數點兩邊將各存儲9個數字,一共使用9個字節:小數點前的數字用4個字節,小數點後的數字用4個字節,小數點自己佔1個字節。mysql 5.0和更高版本中的decimal類型容許最多65個數字。函數
浮點類型在存儲一樣範圍的值時,一般比decimal使用更少的空間。float使用4個字節存儲,double佔用8個字節,相比float有更高的精度和更大的範圍。和整數類型同樣,能選擇的只是存儲類型;mysql使用double做爲內部浮點計算的類型。工具
由於須要額外的空間和計算開銷,因此應該儘可能只在對小數進行精確計算時才使用decimal——例如存儲財務數據。但在數據量比較大的時候,能夠考慮使用bigint代替decimal,將須要存儲的貨幣單位根據小數的位數乘以相應的倍數便可,這樣能夠同時避免浮點存儲計算不精確和decimal精確計算代價高的問題。
mysql容許使用非標準語法:float(m,d)、real(m,d)或double(m,d),這裏(m,d)表示該值一共保存m位數字,其中d位數字在小數點後面。例如,定義爲float(7,4)的列保存值的範圍:-999.9999~999.9999,在實際保存值時會四捨五入,若是在float(7,4)列內插入999.00009,實際保存值999.0001。
decimal和numeric在mysql中視爲相同的類型,它們用於保存精確值,例如財務數據。當定義列爲該類型時,能夠指定精度和標度,例如,decimal(5,2)中5是精度,2是標度,精度表示能夠保存數字的總位數,標度表示小數點後能夠保存數字的位數。
mysql支持多種字符串類型,每種類型還有不少變種。
varchar和char varchar和char是兩種最主要的字符串類型。
varchar類型用於存儲可變長字符串,是最多見的字符串數據類型,它比定長類型更節省空間,由於它僅使用必要的空間(例如,越短的字符串使用越少的空間)。在mysql 5.0或更高版本,存儲和檢索varchar時會保留末尾空格。varchar須要使用1或2個額外字節記錄字符串的長度:若是列的最大長度小於或等於255字節,需額外使用1個字節,不然使用2個字節。 下面這些狀況下使用varchar是合適的:
字符串列的最大長度比平均長度大不少
列的更新不多,因此碎片不是問題
使用了像utf-8這樣複雜的字符集,每一個字符都使用不一樣的字節數進行存儲 char類型是定長的:mysql老是根據定義的字符串長度分配足夠的空間。當存儲char值時,mysql會刪除全部的末尾空格。char適合存儲很短的字符串,或者全部值都接近同一個長度:
char很是適合存儲密碼的md5值,由於這是一個定長的值
對於常常變動的數據,char也比varchar更好,由於定長的char類型不容易產生碎片
對於很是短的列,char也比varchar在存儲空間上也更有效率
VARCHAR(5)和VARCHAR(200)存儲'hello'的空間開銷是同樣的.更長的列會消耗更多的內存,應爲mysql一般會分配固定大小的內存塊來保存內部值.尤爲是使用內存臨時表進行排序或操做時會特別糟糕.再利用磁盤臨時表進行排序時也一樣糟糕.因此最好的策略是隻分配真正須要的空間.
varbinary和binary
varbinary和binary類型存儲的是二進制字符串。二進制字符串和常規字符串很是類似,可是二進制字符串存儲的是字節碼而不是字符,填充也不同:mysql填充binary採用的是\0(零字節)而不是空格,在檢索時也不會去掉填充值。
當須要存儲二進制數據,而且但願mysql使用字節碼而不是字符進行比較時,這些類型是很是有用的。二進制比較的優點並不只僅體如今大小寫敏感上。mysql比較binary字符串時,每次按一個字節,而且根據該字節的數值進行比較。所以,二進制比較比字符比較簡單不少,因此也就更快。
blob和text
blob和text都是爲存儲很大的數據而設計的字符串數據類型,分別採用二進制和字符方式存儲。實際上,它們分別屬於兩組不一樣的數據類型家族:字符類型是tinytext、text、mediumtext、longtext;對應的二進制類型是tinyblob、blob、mediumblob、longblob。
與其它類型不一樣,mysql把每一個blob和text值看成一個獨立的對象處理。存儲引擎在存儲時一般會作特殊處理。當blob和text值太大時,InnoDB會使用專門的「外部」存儲區域來進行存儲,此時每一個值在行內須要1~4個字節存儲一個指針,而後在外部存儲區域存儲實際的值。
blob和text家族之間僅有的不一樣是blob類型存儲的是二進制數據,沒有排序規則或字符集,而text類型有字符集和排序規則。
mysql對blob和text列進行排序與其餘類型是不一樣的:它只對每一個列的最前max_sort_length字節而不是整個字符串作排序。
mysql不能將blob和text列所有長度的字符串進行索引,也不能使用這些索引消除排序。
mysql能存儲的最小時間粒度爲秒,提供兩種類似的日期時間類型:datetime和timestamp,提供日期類型:date,提供時間類型:time。
datetime類型能保存大範圍的值,從1001年到9999年,精度爲妙,使用8個字節的存儲空間。
timestamp類型保存了從1970年1月1日午夜(格林尼治標準時間)以來的秒數,它和UNIX時間戳相同,使用4個字節的存儲空間,所以它的範圍比datetime小的多:只能表示從1970年到2038年。timestamp顯示的值也依賴於時區,mysql服務器、操做系統,以及客戶端鏈接都有時區設置。
默認狀況下,若是插入時沒有制定第一個TIMESTAMP列的值,mysql則設置這個列的值爲當前時間.再插入一行記錄時,mysql默認也會更新第一個TIMESTAMP列的值.TIMESTAMP列默認爲NOT NULL.
mysql沒有提供合適的數據類型存儲比秒更小粒度的日期和時間,可使用BIGINT類型存儲微秒級別的時間戳,或者用double存儲以後的小數部分.
BIT 在mysql5.0以前BIT和TINYINT是同義詞.5.0以及更新版本mysql把BIT當作字符串.最大長度爲64位
爲標識列(identifier column)選擇合適的數據類型很是重要。一旦選定了一種類型,要確保在全部關聯表中都使用一樣的類型,類型之間須要精確匹配。在能夠知足值的範圍的需求,而且預留將來增加空間的前提下,應該選擇最小的數據類型:
整數一般是標識列最好的選擇,由於它們很快而且可使用AUTO_INCREMENT 儘可能避免使用字符串類型做爲標識符,由於它們很消耗空間,而且一般比數字類型慢。對於徹底隨機的字符串也須要多加註意,隨機產生的值會任意分佈在很大的空間內,這會致使insert以及一些select語句變的很慢
插入值會隨機地寫到索引的不一樣位置
select語句會變得更慢,由於邏輯上相鄰的行爲會分佈在磁盤和內存的不一樣地方
隨機值致使緩存對全部類型的查詢語句效果都不好.
常用varchar(15)列來存儲IP地址,然而,它們其實是32位無符號整數,不是字符串,用小數點將地址分紅四段的表示方法只是爲了閱讀。因此應該用無符號整數存儲IP地址,mysql提供INET_ATON()、INET_NTOA()函數在這兩種表示方法之間轉換,示例以下。
`ip` int(10) unsigned DEFAULT '0'; UPDATE tb_test SET ip = INET_ATON('192.168.1.1'); SELECT INET_NTOA(ip) FROM tb_test;
在mysql特定實現下,設計schema時須要避免的錯誤:
太多的列
mysql的存儲引擎API工做時須要在服務器層和存儲引擎層之間經過行緩衝格式拷貝數據,而後在服務器層將緩衝內容解碼成各個列。從行緩衝中將編碼過的列轉換成行數據結構的操做代價是很是高的。InnoDB的行結構老是須要轉換,轉換的代價依賴於列的數量,若是計劃使用數千個字段,必須意識到服務器的性能運行特徵會有一些不一樣
太多的關聯
mysql限制了每一個關聯操做最多隻能有61張表。一個粗略的經驗法則,若是但願查詢執行的快速且併發性好,單個查詢最好在12個表之內作關聯
全能的枚舉
注意防止過分使用ENUM
變相的枚舉
非此發明的NULL
對於任何給定的數據一般都有不少種表示方法,從徹底的範式化到徹底的反範式化,以及二者的折中。在範式化的數據庫中,每一個事實數據會出現而且只出現一次。相反,在反範式化的數據庫中,信息是冗餘的,可能會存儲在多個地方。
由於性能問題而尋求幫助時,常常會被建議對schema進行範式化設計,尤爲是寫密集的場景:
範式化的更新操做一般比反範式化要快
當數據較好的範式化時,就只有不多或者沒有重複數據,因此只須要修改更少的數據
範式化的表一般更小,能夠更好的放在內存裏,因此執行操做會更快
不多有多餘的數據意味着檢索列表數據時更少須要DISTINCT或者GROUP BY語句
範式化設計的schema的缺點是一般須要關聯。稍微複雜一些的查詢語句在符合範式化的schema上均可能須要至少一次關聯,也許更多,這不但代價昂貴,也可能使一些索引策略無效。
反範式化的schema由於全部數據都在一張表中,能夠很好的避免關聯。若是不須要關聯表,則對大部分查詢最差的狀況——即便表沒有使用索引——是全表掃描。當數據比內存大時這可能比關聯要快的多,由於這樣避免了隨機I/O。
事實是,徹底的範式化和徹底的反範式化schema都是實驗室裏纔有的東西,在實際應用中常常須要混用,可能使用部分範式化的schema、緩存表,以及其餘技巧。
最多見的反範式化數據的方法是複製或者緩存,在不一樣的表中存儲相同的特定列。在mysql 5.0和更新版本中,可使用觸發器更新緩存值,這使得實現這樣的方案變的更簡單。
有時提高性能最好的方法是在同一張表中保存衍生的冗餘數據。然而,有時也須要建立一張徹底獨立的彙總表或緩存表(特別是爲知足檢索的需求時)。若是能允許少許的髒數據,這是很是好的方法,可是有時確實沒有選擇的餘地(例如,須要避免複雜、昂貴的實時更新操做)。
術語「緩存表」和「彙總表」沒有標準的含義。咱們用術語「緩存表」來表示存儲那些能夠比較簡單地從schema 其餘表獲取(可是每次獲取的速度比較慢)數據的表(例如,邏輯上冗餘的數據)。而術語「彙總表」時,則保存的是使用GROUP BY語句聚合數據的表(例如,數據不是邏輯上冗餘的)。也有人使用術語「累積表(Roll-Up Tables)」稱呼這些表。由於這些數據被「累積」了。
仍然以網站爲例,假設須要計算以前24 小時內發送的消息數。在一個很繁忙的網站不可能維護一個實時精確的計數器。做爲替代方案,能夠每小時生成一張彙總表。這樣也許一條簡單的查詢就能夠作到,而且比實時維護計數器要高效得多。缺點是計數器並非100% 精確。
若是必須得到過去24 小時準確的消息發送數量(沒有遺漏),有另一種選擇。以每小時彙總表爲基礎,把前23 個完整的小時的統計表中的計數所有加起來,最後再加上開始階段和結束階段不完整的小時內的計數。
假設統計表叫做msg_per_hr 而且這樣定義:
CREATE TABLE msg_per_hr ( hr DATETIME NOT NULL, cnt INT UNSIGNED NOT NULL, PRIMARY KEY(hr) );
能夠經過把下面的三個語句的結果加起來,獲得過去24 小時發送消息的總數。咱們使用LEFT(NOW(),14) 來得到當前的日期和時間最接近的小時:
mysql> SELECT SUM(cnt) FROM msg_per_hr -> WHERE hr BETWEEN -> CONCAT(LEFT(NOW(), 14), '00:00') - INTERVAL 23 HOUR -> AND CONCAT(LEFT(NOW(), 14), '00:00') - INTERVAL 1 HOUR; mysql> SELECT COUNT() FROM message -> WHERE posted >= NOW() - INTERVAL 24 HOUR -> AND posted < CONCAT(LEFT(NOW(), 14), '00:00') - INTERVAL 23 HOUR; mysql> SELECT COUNT() FROM message -> WHERE posted >= CONCAT(LEFT(NOW(), 14), '00:00');
無論是哪一種方法——不嚴格的計數或經過小範圍查詢填滿間隙的嚴格計數——都比計算message 表的全部行要有效得多。這是創建彙總表的最關鍵緣由。實時計算統計值是很昂貴的操做,由於要麼須要掃描表中的大部分數據,要麼查詢語句只能在某些特定的索引上纔能有效運行,而這類特定索引通常會對UPDATE 操做有影響,因此通常不但願建立這樣的索引。計算最活躍的用戶或者最多見的「標籤」是這種操做的典型例子。緩存表則相反,其對優化搜索和檢索查詢語句頗有效。這些查詢語句常常須要特殊的表和索引結構,跟普通OLTP 操做用的表有些區別。
例如,可能會須要不少不一樣的索引組合來加速各類類型的查詢。這些矛盾的需求有時須要建立一張只包含主表中部分列的緩存表。一個有用的技巧是對緩存表使用不一樣的存儲引擎。例如,若是主表使用InnoDB,用MyISAM 做爲緩存表的引擎將會獲得更小的索引佔用空間,而且能夠作全文搜索。有時甚至想把整個表導出MySQL,插入到專門的搜索系統中得到更高的搜索效率,例如Lucene 或者Sphinx 搜索引擎。
在使用緩存表和彙總表時,必須決定是實時維護數據仍是按期重建。哪一個更好依賴於應用程序,可是按期重建並不僅是節省資源,也能夠保持表不會有不少碎片,以及有徹底順序組織的索引(這會更加高效)。
當重建彙總表和緩存表時,一般須要保證數據在操做時依然可用。這就須要經過使用「影子表」來實現, 「 影子表」指的是一張在真實表「背後」建立的表。當完成了建表操做後,能夠經過一個原子的重命名操做切換影子表和原表。例如,若是須要重建 my_summary,則能夠先建立 my_summary_new,而後填充好數據,最後和真實表作切換:
mysql> DROP TABLE IF EXISTS my_summary_new, my_summary_old; mysql> CREATE TABLE my_summary_new LIKE my_summary; -- populate my_summary_new as desired mysql> RENAME TABLE my_summary TO my_summary_old, my_summary_new TO my_summary;
許多數據庫管理系統(例如Oracle 或者微軟SQL Server)都提供了一個被稱做物化視圖的功能。物化視圖其實是預先計算而且存儲在磁盤上的表,能夠經過各類各樣的策略刷新和更新。MySQL 並不原生支持物化視圖。
Flexviews 比徹底本身實現的解決方案要更精細,而且提供了不少不錯的功能使得能夠更簡單地建立和維護物化視圖。它由下面這些部分組成:
變動數據抓取(Change Data Capture,y CDC)功能,能夠讀取服務器的二進制日誌而且解析相關行的變動。
一系列能夠幫助建立和管理視圖的定義的存儲過程。
一些能夠應用變動到數據庫中的物化視圖的工具。
對比傳統的維護彙總表和緩存表的方法,Flexviews 經過提取對源表的更改,能夠增量地從新計算物化視圖的內容。這意味着不須要經過查詢原始數據來更新視圖。
先寫出一個SELECT 語句描述想從已經存在的數據庫中獲得的數據。這可能包含關聯和聚合(GROUP BY)。Flexviews 中有一個輔助工具能夠轉換SQL 語句到Flexviews 的API 調用。Flexviews 會作完全部的髒活、累活:監控數據庫的變動而且轉換後用於更新存儲物化視圖的表。如今應用能夠簡單地查詢物化視圖來替代查詢須要檢索的表。
應用在表中保存計數器,則在更新計數器時可能碰到併發問題。建立一張獨立的表存儲計數器一般是個好主意,這樣可以使計數器表小且快。使用獨立的表能夠幫助避免查詢緩存失效。
假設有一個計數器表,只有一行數據,記錄網站的點擊次數:
CREATE TABLE hit_counter( cnt int unsigned not null ) ENGINE=InnoDB;
網站的每次點擊都會致使對計數器進行更新:
UPDATE hit_counter set cnt = cnt + 1;
問題在於,對於任何想要更新這一行的事務來講,這條記錄上都有一個全局的互斥鎖(mutex)。這會使得這些事務只能串行執行。要得到更高的併發更新性能,也能夠將計數器保存在多行中,每次隨機選擇一行進行更新。這樣作須要對計數器表進行以下修改:
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 tinyint unsigned not null, cnt int unsigned not null, primary key(day,slot) ) ENGINE=InnoDB;
若是但願減小表的行數,以免表變得太大,能夠寫一個週期執行的任務,合併全部結果到0 號槽,而且刪除全部其餘的槽:
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;
總結:
儘可能避免過分設計
使用小而簡單的合適數據類型,除非真實數據模型中有確切的須要,不然應該儘量地避免使用NULL值。
儘可能使用相同的數據類型存儲類似或相關的值,尤爲是要在關聯條件中使用的列。
注意可變長字符串,其在臨時表和排序時可能致使悲觀的按最大長度分配內存。
儘可能使用整型定義標識列。
避免使用MySQL已經遺棄的特性。
當心使用ENUM和SET。