1. 程式人生 > 資料庫 >《高效能MySQL》學習筆記——第四章 Schema與資料型別優化

《高效能MySQL》學習筆記——第四章 Schema與資料型別優化

第四章 Schema與資料型別優化

4.1 選擇優化的資料型別

1、應該儘量使用可以正確儲存儲存資料的最小資料型別。如能使用tinyint就不要使用int,能使用varchar(20),就不要使用varchar(100)。

2、應該儘量使用盡可能簡單的資料型別,如日期型別儘量使用date、time、datetime,而不是使用varchar儲存日期資料,另外應該使用整形儲存IP地址,而不是使用varchar。

3、應該儘量避免null,可以保證不出錯的情況下儘量把列指定為not null。一方面在於在查詢時,如果有null,則需要使用ifnull,而這樣的話會使索引失效;另一方面,可為null的列會佔用更多的儲存空間。

4、int(11)通常是沒有意義的;decimal(m,n)中m是指整數部分和小數部分位數之和,n則僅僅指小數部分位數。

5、財務資料建議用decimal儲存,可以對小數部分進行精確計算,而在資料量比較大時,可以考慮使用BIGINT代替decimal,並根據需要儲存的小數位乘以相應的倍數就行,如需要保留到萬分之一,可以將原始資料乘以一百萬,再存到bigint裡面,因為bigint相比decimal的好處在於計算更精確和計算效率高。(但是這種方式要注意使用該金額時可能會出現忘記除掉相應的倍數的情況,個人建議還是使用decimal,更安全)

6、varchar和char:

(1)varchar和char型別消耗的儲存空間的位元組數由其字符集決定,如使用utf8mb4時,英文字母和數字佔用1個位元組,而大部分中文佔用3個位元組,少量特殊字元佔用4個位元組。使用char_length()和length()可以檢視字串長度和字串所佔用的位元組長度。

mysql> select test1, length(test1), char_length(test1) from test_varchar1;
+--------+---------------+--------------------+
| test1  | length(test1) | char_length(test1) |
+--------+---------------+--------------------+
| abc    |             3 |                  3 |
| 123    |             3 |                  3 |
| 你好啊 |             9 |                  3 |
+--------+---------------+--------------------+
3 rows in set (0.00 sec)

(2)varchar(n)是變長,char(n)是定長,即varchar消耗的儲存空間是隨字串長度而改變的,char消耗的儲存空間是既定的。此外varchar還會消耗1-2個位元組儲存字串的長度,而char不會。因此對於儲存定長資料,使用char更好,因此不需要額外儲存一個位元組來儲存字串長度,但在實際業務中這種需求較少,反倒使用enum的都比使用char的多。

(3)char型別會刪除末尾的空格再進行儲存(危!),而varchar不會刪除末尾空格(實際上在MySQL4.1或更老的版本中varchar也會刪末尾空格)。

mysql> select test2, test3, char_length(test2), char_length(test3), length(test2), length(test3), concat('(', test2, ')'), concat('(', test3, ')') from test_varchar2; -- test2為varchar型別,而test3為char型別。
+--------+-------+--------------------+--------------------+---------------+---------------+-------------------------+-------------------------+
| test2  | test3 | char_length(test2) | char_length(test3) | length(test2) | length(test3) | concat('(', test2, ')') | concat('(', test3, ')') |
+--------+-------+--------------------+--------------------+---------------+---------------+-------------------------+-------------------------+
|   ab   |   ab  |                  6 |                  4 |             6 |             4 | (  ab  )                | (  ab)                  |
|   ab   |   ab  |                  6 |                  4 |             6 |             4 | (  ab  )                | (  ab)                  |
+--------+-------+--------------------+--------------------+---------------+---------------+-------------------------+-------------------------+
2 rows in set (0.00 sec)

(4)當使用嚴格的SQL模式時,insert的資料超過varchar和char的最大長度時,都會報錯;當啟動非嚴格的SQL模式時,他們則是將超過長度後面的字元刪除儲存,並予以警告,而不是報錯。

(5)最好的策略是根據業務需求選擇最適合的型別,只分配真正需要的空間。

7、blob與text都是用於儲存長度特別長(超過65535個位元組)的資料型別,對他們的排序並不是對整個字串進行排序,都是對其前max_sort_length個位元組的字元進行排序,可以手動設定max_sort_length的值,或者使用order by sustring(column, length)。區別在於blob儲存的是字串的二進位制,而text儲存的是原始字串。

8、enum型別會將“數字-字串”對映關係的“查詢表”儲存於.frm檔案中,而資料中只儲存“數字”鍵,這種雙重性容易導致混亂,特別是排序的時候,enum排序是使用內部儲存的整數進行排序,而不是定義的字串進行排序。除非使用FIELD()函式自定義排序順序。列關聯時的效率:enum關聯enum > varchar關聯varchar > enum和varchar互相關聯

mysql> create table `enum1`(column1 enum('Y', 'M', 'N'));
Query OK, 0 rows affected (0.01 sec)
mysql> insert into enum1 values('Y'), ('Y'), ('N'), ('M');
Query OK, 4 rows affected (0.00 sec)

mysql> select column1 from enum1;
+---------+
| column1 |
+---------+
| Y       |
| Y       |
| N       |
| M       |
+---------+
4 rows in set (0.00 sec)

mysql> select column1 + 1 from enum1;
+-------------+
| column1 + 1 |
+-------------+
|           2 |
|           2 |
|           4 |
|           3 |
+-------------+
4 rows in set (0.00 sec)

mysql> select column1 from enum1 order by column1;
+---------+
| column1 |
+---------+
| Y       |
| Y       |
| M       |
| N       |
+---------+
4 rows in set (0.00 sec)

mysql> select column1 from enum1 order by field(column1, 'M', 'N', 'Y');
+---------+
| column1 |
+---------+
| M       |
| N       |
| Y       |
| Y       |
+---------+
4 rows in set (0.00 sec)

9、datetime和timestamp

型別 佔用位元組數 支援的時間範圍
datetime 8 1000-01-01 00:00:00至9999-12-31 23:59:59
timestamp 4 1970-01-01 00:00:00至2038-01-19 23:59:59

10、標識列(即能唯一標識一條資料的欄位)資料型別通常用unsigned int auto_increment或UUID()兩種:當該標識列索引使用BTREE(innodb預設使用BTREE)時,使用unsigned int auto_increment更好(io速度更快、儲存空間更小等),當標識列索引使用hash索引(innodb不顯式支援hash索引,但當支援自適應hash索引,等後面講)時,兩者差不多。

11、最好避免使用BIT和SET型別。

4.2 schema設計中的陷阱

1、避免過多的列和過多的關聯

2、避免NULL,或者使用其他值代替NULL

4.3 正規化和反正規化

設計方面的東西,詳見原文或者參考其他部落格,如

4.4 快取表和彙總表

快取表:表示儲存那些可以比較簡單的從schema其他表獲取(但每次獲取速度都比較慢)資料的表(例如,邏輯上冗雜的資料)。

彙總表:表示儲存那些使用group by語句聚合的資料。

使用快取表的情況是,比如展示一個很詳細的業務資料,要關聯很多張表並進行相關運算,每次查詢速度都比較慢,則可以將定期查詢該SQL並放到一張快取表中,等需要的時候直接取這張快取表中的資料即可,然後定時維護這張快取表以更新資料。這種情況資料雖然有延遲,但對於使用者來說能很快的看到資料。

使用匯總表的情況是,比如要看網站最近一個月每天的點選量,則要做group by操作,可以每天定時執行一個的SQL,將當天的點選量記錄到這張彙總表中,等需要的時候直接where between就行,不用做group by。

物化檢視:預先計算並存儲在磁碟上的表,並通過各種策略來自動更新該表(檢視)。MySQL可以用第三方工具:Justin Swanhart的Flexviews。

計數器表:再比如上面那個網站最近一個月每天的點選量的情況,可以通過定義一張下面所示的表,每次收到使用者訪問,就隨機選一個槽進行更新(避免鎖衝突)。再設定一個定時任務,每天將昨天的資料彙總到0號槽,並刪除其他槽,這樣就是一個統計每一天的訪問量的計數器表。

CREATE TABLE `daily_click` (
  `day` date NOT NULL,
  `slot` int unsigned NOT NULL,
  `cnt` int DEFAULT 0,
  PRIMARY KEY (`day`,`slot`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

mysql> insert into daily_click values(now(), round(rand(), 2) * 100, 1) on duplicate key update cnt = cnt + 1; -- 我這裡執行了六次
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql> select * from daily_click;
+------------+------+------+
| day        | slot | cnt  |
+------------+------+------+
| 2020-11-22 |   22 |    1 |
| 2020-11-22 |   29 |    1 |
| 2020-11-22 |   48 |    1 |
| 2020-11-22 |   53 |    2 |
| 2020-11-22 |   95 |    1 |
+------------+------+------+
5 rows in set (0.00 sec)

mysql> update daily_click as c,
      (select day, sum(cnt) as cnt, min(slot) as slot from daily_click group by day) as x
      set c.cnt = if(c.slot = x.slot, x.cnt, 0),
      c.slot = if(c.slot = x.slot, 0, c.slot)
      where c.day = x.day and c.day = '2020-11-22';
Query OK, 5 rows affected (0.00 sec)
Rows matched: 5  Changed: 5  Warnings: 0

mysql> delete from daily_click where day = '2020-11-22' and slot <> 0;
Query OK, 4 rows affected (0.00 sec)

mysql> select * from daily_click;
+------------+------+------+
| day        | slot | cnt  |
+------------+------+------+
| 2020-11-22 |    0 |    6 |
+------------+------+------+
1 row in set (0.00 sec)

4.5 加快ALTER TABLE的速度

1、修改列的三種方式:

(1)ALTER TABLE tbl_name CHANGE [COLUMN] old_col_name new_col_name column_definition [FIRST | AFTER col_name]:這種方法是整列換成一個新列的定義,包括列名也可以修改,會引起表的重建,即刪除舊列,構造新列;

(2)ALTER TABLE tbl_name MODIFY [COLUMN] col_name column_definition [FIRST | AFTER col_name]:這種方法也是整列換成一個新列,但是不能修改表名,只能修改屬性,也會引起表的重建;

(3)ALTER TABLE tbl_name ALTER [COLUMN] col_name {SET DEFAULT {literal | (expr)} | DROP DEFAULT}:這種方法侷限性很高,只能修改列的預設值屬性,這個語句會直接修改表的.frm檔案,不涉及表資料,不會引起表的重建,因此速度很快。

即如果是需要修改的東西實際存在於.frm檔案中,都可以通過直接修改.frm檔案來進行修改,而不用重建表。

2、修改表結構的技巧:

(1)先在一臺不提供服務的庫上執行alter table操作,然後和提供服務的主庫進行切換。過程(個人盲猜的):停止從主備庫同步,備用庫執行alter table,重新同步主備庫,待同步成功再切換主備庫。問題:切換主備庫會不會導致整個伺服器停頓?

(2)影子拷貝。建一個新的空表,表結構為原表修改後的表結構,在新表中建三個INSERT UPDATE DELETE的觸發器,將舊錶資料拷貝到新表,最新資料會通過更新過去,然後通過重命名錶和刪表的方式交換兩張表。