難得一個(gè)周末,終于可以靜下心來(lái)整理一下筆記了,最近確實(shí)沒時(shí)間。但是我已經(jīng)預(yù)感到風(fēng)雨后的彩虹,所以一切都會(huì)變得很好......
今天我們來(lái)講一下mysql
數(shù)據(jù)庫(kù)中的schema類型優(yōu)化相關(guān)的知識(shí)
在進(jìn)行mysql
數(shù)據(jù)庫(kù)表設(shè)計(jì)的時(shí)候
需要遵守的幾點(diǎn)原則
更小的通常更好
使用更小的類型存儲(chǔ)數(shù)據(jù),通常更快,占用空間更小,CPU
運(yùn)行速度更快
簡(jiǎn)單就好
簡(jiǎn)單數(shù)據(jù)類型的操作通常需要更少的CPU
周期,整型比字符型操作代價(jià)更低,因?yàn)樽址托r?yàn)規(guī)則使字符型更復(fù)雜,使用mysql
內(nèi)建的類型而不是字符串來(lái)存儲(chǔ)日期和時(shí)間,使用整型來(lái)存儲(chǔ)IP
地址
mysql
支持很多別名,但通過別名創(chuàng)建表之后,通過show create table
顯示表創(chuàng)建語(yǔ)句時(shí),采用的是具體的類型
避免NULL
NULL
值列會(huì)影響到索引、索引統(tǒng)計(jì)和值比較,mysql
更難優(yōu)化,盡量避免在NULL
列上建立索引
可以存儲(chǔ)相同數(shù)據(jù)的不同數(shù)據(jù)庫(kù)類型很多,但是他們的長(zhǎng)度范圍空間占用都不同,比如使用datetime
和timestamp
來(lái)存儲(chǔ)日期時(shí)間,但是timestamp
只使用了datetime
一半的存儲(chǔ)空間
mysql數(shù)據(jù)類型
整型
數(shù)字分為整數(shù)和實(shí)數(shù),如果存儲(chǔ)整數(shù),又有TINYINT SMALLINT MEDIUMINT INT BIGINT
,他們的長(zhǎng)度分別為8 16 24 32 64
字節(jié)
整數(shù)類型有可選的UNSIGNED
屬性,表示不能為負(fù)數(shù),有符號(hào)和無(wú)符號(hào)類型使用相同存儲(chǔ)空間,并具有相同的性能
數(shù)據(jù)庫(kù)中使用BIGINT
來(lái)進(jìn)行整數(shù)計(jì)算
int(1)
與int(10)
在存儲(chǔ)層面上來(lái)說其實(shí)都占用一樣的空間,只是從mysql
客戶端層面來(lái)進(jìn)行限制顯示的字符長(zhǎng)度
int(1)
與int(10)
如果不加zerofill
,在展示上并沒有什么明顯的變化,如果添加上zerofill
就可以看到他們之間的區(qū)別
create table int_test(num(1),num2(10));
insert into int_test(num,num2) values(8,8);
select * from int_test;
+---+
| 8 |
+---+
| 8 |
+---+
如果添加了zerofill
屬性
create table int_test(num(1),num2(10) zerofill);
insert into int_test(num,num2) values(8,8);
select * from int_test;
+------+------------+
| num | num2 |
+------+------------+
| 1 | 0000000001 |
+------+------------+
實(shí)數(shù)
分兩種:近似計(jì)算
FLOAT DOUBLE
分別包含8,16
個(gè)字節(jié),對(duì)于浮點(diǎn)運(yùn)算,MYSQL
內(nèi)部使用DOUBLE
作為浮點(diǎn)運(yùn)算類型
精確計(jì)算 DECIMAL
該類型允許最多65個(gè)數(shù)字
浮點(diǎn)類型在存儲(chǔ)相同范圍的值時(shí),通常比 DECIMAL
使用更少的空間
應(yīng)該避免在非小數(shù)進(jìn)行精確計(jì)算的時(shí)候使用DECIMAL
,如果在數(shù)據(jù)量大的時(shí)候,可以使用BIGINT
代理DECIMAL
,可以使用BIGINT
* 相應(yīng)倍數(shù),主要是考慮到性能代價(jià)問題
字符串類型
varchar與char
varchar
varchar
類型用于存儲(chǔ)可變長(zhǎng)度字符串,是最常見的字符串?dāng)?shù)據(jù)類型,它比char
更節(jié)省空間,它只使用必要的空間,越短的字符串,使用越少的空間
但是如果是用ROW_FORMAT=FIXED
創(chuàng)建的話,這根char
就沒有什么區(qū)別了
ROW_FORMAT
的具體修改方式:
alter table tablename ROW_FORMAT=[DEFAULT,FIXED,DYNAMIC,COMPRESS,REDUNDANT,COMPACT]
當(dāng)ROW_FORMAT
從FIXED->DYNAMIC
時(shí),CHAR->VARCHAR
,
反之,
從DYNAMIC->FIXED
時(shí),VARCHAR->CHAR
varchar
需要1
到兩個(gè)字節(jié)來(lái)記錄字符串長(zhǎng)度,如果長(zhǎng)度小于255
就用一個(gè)字節(jié),否則用兩個(gè)字節(jié)
適用場(chǎng)景:字符串列的最大長(zhǎng)度比平均長(zhǎng)度大很多,這樣列的更新少了,主要是考慮到更新的時(shí)候如果長(zhǎng)度超過了指定的限制,那么就會(huì)導(dǎo)致分段(myisam
)或者分頁(yè)(innodb
)存儲(chǔ)
char
char
是定長(zhǎng)的,存儲(chǔ)char
時(shí),MySQL
會(huì)刪除末尾空格,char
會(huì)根據(jù)需要采用空格進(jìn)行填充以方便比較,需要注意的是char(1)
表示存儲(chǔ)的是一個(gè)字符,而不是一個(gè)字節(jié)
BINARY和VARBINARY
與CHAR
和VARCHAR
對(duì)應(yīng)的一組就是BINARY
與VARBINARY
,這兩者是用于存儲(chǔ)二進(jìn)制字符串,二進(jìn)制字符串與常規(guī)字符串相似,但是二進(jìn)制字符串存儲(chǔ)的是二進(jìn)制字節(jié)而不是字符,填充也不一樣,BINARY
采用的是\0
(零字節(jié))而不是空格進(jìn)行填充,檢索時(shí)也不會(huì)去掉填充值
BLOB與TEXT
兩者都是用來(lái)存儲(chǔ)很大的數(shù)據(jù),分別用于存儲(chǔ)二進(jìn)制和字符方式的數(shù)據(jù)
與其他類型不一樣,mysql
把他們當(dāng)做獨(dú)立的對(duì)象處理,存儲(chǔ)引擎在存儲(chǔ)時(shí)通常會(huì)做特殊處理,當(dāng)BLOB
和TEXT
值太大時(shí),INNODB
會(huì)使用外部的存儲(chǔ)區(qū)域進(jìn)行存儲(chǔ),此時(shí)每個(gè)值在行內(nèi)需要1~4
字節(jié)存儲(chǔ)一個(gè)指針,具體值存儲(chǔ)在外部區(qū)域中
兩者之間的不同是,采用BLOB
類型存儲(chǔ)的是二進(jìn)制數(shù)據(jù),沒有排序規(guī)則或字符集
mysql
不允許對(duì)TEXT/BLOB
全列進(jìn)行索引,只能根據(jù)max_sort_length
設(shè)置的最大長(zhǎng)度進(jìn)行索引,默認(rèn)是1024
,可以通過order by substring(column, length)
來(lái)對(duì)前面length
長(zhǎng)度字符串進(jìn)行排序
如果使用了BLOB
,TEXT
,在進(jìn)行結(jié)果排序時(shí),會(huì)使用到磁盤臨時(shí)表,盡量不要使用TEXT/BLOB
,如果實(shí)在沒有辦法,可以通過substring(column, length)
來(lái)代替整列值進(jìn)行排序,這樣就可以在內(nèi)存中使用內(nèi)存臨時(shí)表了
如果通過explain
分析sql
語(yǔ)句,extra
列出現(xiàn)了using temporary
,則說明這個(gè)查詢使用了隱式臨時(shí)表
枚舉
枚舉可以把不重復(fù)的字符串存儲(chǔ)成一個(gè)預(yù)定義的集合,mysql
會(huì)以整數(shù)保存各個(gè)字符串的位置,對(duì)枚舉類型字段進(jìn)行排序,默認(rèn)是按照整數(shù)值來(lái)進(jìn)行排序的,如果非要使用字符串順序排序,那么有兩種解決方案:
- 按照字符串順序插入枚舉
- 使用
field()
函數(shù),但是這樣會(huì)導(dǎo)致mysql
無(wú)法利用索引消除排序
select e from enumtest order by field(e, 'apple', 'fish','dog')
field(column, order serials)
,根據(jù)給定的order serials
順序?qū)Y(jié)果字符串進(jìn)行排序,但是這樣會(huì)導(dǎo)致無(wú)法使用索引消除排序
在使用char/varchar
與枚舉列進(jìn)行關(guān)聯(lián)時(shí),可能會(huì)比直接關(guān)聯(lián)char/varchar
列更慢
如果直接查詢枚舉字段,則是顯示的字符形式,可以通過數(shù)字上下文查看當(dāng)前枚舉值的位置
create table enum_test(animal enum(‘fish’,’dog’,’cat’);
insert into enum_test(animal) values(‘fish’), (‘dog’);
select * from enum_test;
+--------+
| animal |
+--------+
| fish |
| dog |
+--------+
select animal+0 from enum_test
+----------+
| animal+0 |
+----------+
| 1 |
| 2 |
+----------+
日期和時(shí)間類型
mysql
中日期時(shí)間類型最小單位是秒,但是可以用微妙級(jí)粒度進(jìn)行臨時(shí)計(jì)算
DATETIME
保存大范圍的值,從1001
年到9999
年,精度為秒,封裝到格式為YYYYMMDDHHMMSS
的整數(shù)中進(jìn)行存儲(chǔ),與時(shí)區(qū)無(wú)關(guān),采用8
個(gè)字節(jié)表示
TIMESTAMP
保存了1970
年1
月1
日到現(xiàn)在的秒數(shù),與UNIX
時(shí)間戳相同,只使用4
個(gè)字節(jié)表示,只能表示1970
年到2038
年,提供了FROM_UNIXTIME()
把unix
時(shí)間戳轉(zhuǎn)換為日期,并提供了UNIX_TIMESTAMP()
將日期轉(zhuǎn)化為時(shí)間戳
timestamp
顯示的值跟時(shí)區(qū)有關(guān)系,不同時(shí)區(qū)顯示的值可能會(huì)有差異,使用timestamp
在進(jìn)行sql
更新插入時(shí),如果沒有指定,則會(huì)將當(dāng)前時(shí)間插入進(jìn)去
BIT/SET
所有位類型,從技術(shù)上來(lái)說都是字符串類型
BIT
可以使用bit
在一個(gè)或者多個(gè)列上使用0/1
,BIT(1)
代表1
位,BIT(2)
代表2
位,最大長(zhǎng)度為64
BIT
因存儲(chǔ)引擎而差異,MYISAM
會(huì)打包存儲(chǔ)所有的BIT
列,所以17
個(gè)單獨(dú)的bit
只需要17
位,myisam
將會(huì)打包存儲(chǔ)所有的bit
列,只使用3
個(gè)字節(jié)就可以存儲(chǔ)
對(duì)于memory
與innodb
,則使用足夠存儲(chǔ)的最小整數(shù)類型來(lái)存放,所以在存儲(chǔ)空間上無(wú)法減少消耗
mysql
把BIT
當(dāng)做字符串類型,比如存放b’00111001’
,二進(jìn)制=57
到BIT(8)
的列并檢索,得到的內(nèi)容是ASCII
碼位57
的字符“9
”,在數(shù)字上下文中,是57
createtable bittest(a bit(8));
insert into bittest values(57);
select a, a+0 from bittest
9, 57
應(yīng)該謹(jǐn)慎使用這種類型,對(duì)于大部分應(yīng)用,最好避免使用這種類型
SET
用于保存并合并這些BIT
,但是一般不建議使用這樣的方式,而是采用一個(gè)整數(shù)包裝一系列位,通過位運(yùn)算來(lái)得到整數(shù)
ACL
權(quán)限控制
can_read 1
can_write 2
can_delete 4
set @can_read := 1 << 0,
@can_write := 1 << 1,
@can_delete := 1 << 2;
create table acl(persm tinyint unsigned not null default 0));
insert into acl(perms) values(@can_read + @can_write);
select persm from acl where perms&@can_read;//查詢擁有讀權(quán)限
選擇標(biāo)識(shí)符
標(biāo)識(shí)符選擇合適的類型非常重要,一般來(lái)說它可能會(huì)被用于與其他值比較、外鍵關(guān)聯(lián)、查找,在用于外鍵關(guān)聯(lián)時(shí),需要嚴(yán)格要求外鍵類型一致,避免關(guān)聯(lián)的性能問題和類型隱式轉(zhuǎn)換問題
整數(shù)類型是標(biāo)識(shí)列最好的選擇,因?yàn)樗麄兛梢允褂?code>auto_increment,應(yīng)該避免使用字符串類型作為標(biāo)識(shí)列,因?yàn)樗麄兒芟目臻g,通常,字符串比數(shù)字類型慢,在myisam
,對(duì)字符串默認(rèn)使用的是壓縮索引,對(duì)于隨機(jī)的字符串比如MD5
(),SHA1
(),UUID
()產(chǎn)生的字符串,任意分布在很大的空間內(nèi),這會(huì)導(dǎo)致查詢語(yǔ)句insert/select
變得很慢:
插入新值會(huì)隨機(jī)的寫到索引的不同位置,導(dǎo)致分頁(yè)、磁盤隨機(jī)訪問,聚簇索引產(chǎn)生碎片化
select
語(yǔ)句慢,因?yàn)檫壿嬌舷噜彽男袝?huì)分布到磁盤和內(nèi)存的任意地方,導(dǎo)致緩存對(duì)所有類型的查詢語(yǔ)句效果都很差,訪問局部性原理失效
存儲(chǔ)UUID
值應(yīng)該去掉-
,更好的做法是使用HEX
()函數(shù)轉(zhuǎn)化成16
字節(jié)的數(shù)字,并采用binary(16)
存儲(chǔ),如果要將16
字節(jié)數(shù)字轉(zhuǎn)化回去,應(yīng)該使用unhex()
select hex(uuid()) from dual;
+--------------------------------------------------------------------------+
| hex(uuid()) |
+--------------------------------------------------------------------------+
| 30333164396564612D396261662D313165372D383736352D646330656131363064353363 |
+--------------------------------------------------------------------------+
select unhex('30333164396564612D396261662D313165372D383736352D646330656131363064353363');
+-----------------------------------------------------------------------------------+
| unhex('30333164396564612D396261662D313165372D383736352D646330656131363064353363') |
+-----------------------------------------------------------------------------------+
| 031d9eda-9baf-11e7-8765-dc0ea160d53c |
+-----------------------------------------------------------------------------------+
特殊類型
給定的數(shù)據(jù)并不直接與數(shù)據(jù)庫(kù)內(nèi)置類型一致,比如時(shí)間<
秒級(jí),數(shù)據(jù)庫(kù)最低單位為秒,那么可以通過BIGINT
存儲(chǔ)微妙級(jí)別的時(shí)間戳,或者使用double
存儲(chǔ)秒之后的小數(shù)部分
另一個(gè)例子是IPV4
地址,其實(shí)IPV4
地址實(shí)際上是一個(gè)32
位的無(wú)符號(hào)整數(shù),不是字符串,用小數(shù)點(diǎn)將地址分成4
段的表示方法是為了讓人們閱讀容易,所以應(yīng)該用無(wú)符號(hào)整數(shù)存儲(chǔ)IP
地址
解釋:
ip
地址一共4
段,每段取值為0~255
,也就是說每段可以用1
個(gè)字節(jié)表示,4 * 1byte * 8bit = 32bit
如何將ip
地址轉(zhuǎn)換成數(shù)字
使用數(shù)據(jù)庫(kù)提供的方法
select inet_aton (ip -> number)
select inet_ntoa (number -> ip)
同理`ipv6`采用`128`位,通過`varbinary`存儲(chǔ)(`bigint`最大支持`64`位)
`inet6_aton/inet6_ntoa `
使用程序
ip
轉(zhuǎn)long
/**
* 把字符串IP轉(zhuǎn)換成long
*
* @param ipStr 字符串IP
* @return IP對(duì)應(yīng)的long值
*/
public static long ip2Long(String ipStr) {
String[] ip = ipStr.split("\\.");
return (Long.valueOf(ip[0]) << 24) + (Long.valueOf(ip[1]) << 16)
+ (Long.valueOf(ip[2]) << 8) + Long.valueOf(ip[3]);
}
/**
* 把IP的long值轉(zhuǎn)換成字符串
*
* @param ipLong IP的long值
* @return long值對(duì)應(yīng)的字符串
*/
public static String long2Ip(long ipLong) {
StringBuilder ip = new StringBuilder();
ip.append(ipLong >>> 24).append(".");
ip.append((ipLong >>> 16) & 0xFF).append(".");
ip.append((ipLong >>> 8) & 0xFF).append(".");
ip.append(ipLong & 0xFF);
return ip.toString();
}
范式和反范式
常用的數(shù)據(jù)庫(kù)范式有3
大范式
1NF
: 數(shù)據(jù)庫(kù)中的每一列都是最小的單元,不可拆分
2NF
: 數(shù)據(jù)庫(kù)表中的每一條記錄都能唯一標(biāo)識(shí)(主鍵唯一性約束)
3NF
:數(shù)據(jù)庫(kù)表中不存在其他表中的非主鍵列
反范式化的schema
,因?yàn)樗袛?shù)據(jù)都在一張表上,所以就不用關(guān)聯(lián)其他表了,當(dāng)數(shù)據(jù)量超大時(shí),這樣就避免了隨機(jī)IO
產(chǎn)生
緩存表和匯總表
緩存表:用于存儲(chǔ)可以比較簡(jiǎn)單從schema
其他表獲取數(shù)據(jù)的表,(但是獲取數(shù)據(jù)的速度比較慢)
匯總表:保存的是使用group by
語(yǔ)句聚合數(shù)據(jù)的表,也就是統(tǒng)計(jì)過后的數(shù)據(jù)
以獲取用戶24
小時(shí)之前內(nèi)發(fā)送的消息數(shù)來(lái)說,系統(tǒng)可以每小時(shí)生成一張匯總表,如果必須獲取24
小時(shí)之內(nèi)發(fā)送的消息數(shù),以每小時(shí)匯總表為基礎(chǔ),把前23
個(gè)小時(shí)的統(tǒng)計(jì)表中的計(jì)數(shù)全部加起來(lái),最后再加上開始階段和結(jié)束階段不完整的小時(shí)數(shù),假設(shè)統(tǒng)計(jì)表叫
msg_per_hr:
create table msg_per_hr {
hr datetime not null,
cnt int unsigned not null,
primary key (hr)
}
通過concat(left(now(), 14), ’00:00’)
來(lái)獲取最近的小時(shí)數(shù)
計(jì)算前面完整的23
個(gè)小時(shí)的消息總數(shù)
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’);
獲取前面第24
小時(shí)不完整的時(shí)間片段
select sum(cnt) from msg_per_hr where hr>= now() – interval 24 hour < concat(left(now(),14), ’00:00’) – interval 23 hour;
獲取最近1
小時(shí)內(nèi)的統(tǒng)計(jì)信息
select sum(cnt)from msg_per_hr where hr > concat(left(now(), 14), ’00:00’);
將這三個(gè)統(tǒng)計(jì)數(shù)加起來(lái)就得到之前24
小時(shí)內(nèi)的統(tǒng)計(jì)信息
在添加緩存表或者匯總表后,必須決定是實(shí)時(shí)維護(hù)還是定期重建數(shù)據(jù),但是采用定期重建并不只是節(jié)省資源,也可以保持表不會(huì)有很多碎片,通常在重建匯總表和緩存表時(shí),也要求數(shù)據(jù)在操作時(shí)可用,這時(shí)需要通過影子表來(lái)實(shí)現(xiàn),當(dāng)完成影子表創(chuàng)建后通過原子性的重命名操作切換影子表和原表
加快alter table速度
alter table
操作的性能對(duì)于大表來(lái)說是個(gè)大問題,mysql
執(zhí)行大部分修改表結(jié)構(gòu)操作的方法使用新的結(jié)構(gòu)創(chuàng)建一個(gè)空表,從舊表中查詢出所有的數(shù)據(jù)插入到新表中,然后刪除舊表
對(duì)于常見的場(chǎng)景:
- 先在一臺(tái)不提供服務(wù)的機(jī)器上執(zhí)行
alter table
,然后提供服務(wù)的主庫(kù)進(jìn)行切換 - 另外一種是創(chuàng)建"影子表"拷貝,影子拷貝的技巧用要求的表結(jié)構(gòu)創(chuàng)建一張與源表無(wú)關(guān)的新表,然后通過重命名和刪除操作交換兩張表
alter table 不引起表重建
這里以rental_duration tinyint(5)
改為tinyint(3)
來(lái)說
通過alter table modify column
會(huì)導(dǎo)致表重建,所有的modify column
都將導(dǎo)致表重建
alter table film modify column rental_duration tinyint(3) not null default 5;
使用alter table … alter column
來(lái)操作表的列
alter table film alter column rental_duration set default 5;
他會(huì)直接修改.frm
文件而不涉及表數(shù)據(jù)
駭客做法,請(qǐng)先備份您的數(shù)據(jù),不推薦
直接修改.frm
文件 - 創(chuàng)建一張有相同結(jié)構(gòu)的空表,并進(jìn)行相應(yīng)的修改
- 執(zhí)行
flush table with read lock
,這會(huì)關(guān)閉所有正在使用中的表,并禁止任何表被打開 - 交換
.frm
文件 - 執(zhí)行
unlock tables
來(lái)釋放第2
步的讀鎖
快速創(chuàng)建myisam
索引
要將數(shù)據(jù)高效的導(dǎo)入myisam
表中,常用的一個(gè)技巧是,先禁用索引,導(dǎo)入數(shù)據(jù)、啟用索引
alter table tablename disable keys;
loading data
alter table tablename enable keys;
但是上面的這種方式對(duì)唯一索引無(wú)效,因?yàn)?code>DISABLE KEY只對(duì)非唯一索引有效