注:本篇是《高性能Mysql》第三版的讀書筆記
分區表
根據某種情況進行分表操作,一般常用於按照時間分表,比如每個月的數據存儲到一張分區表中,分區表在查詢時匹配where條件,查詢總表時匹配where條件會減少查詢範圍,去查詢分區表,過濾很多其他分區表的數據來達到一個高效的方式。
在MySQL中使用關鍵字 partition
在postgresql中使用關鍵字 inherits / inhert
分區表的目的基本是數據量太大用來優化查詢速度的,或者熱點數據。
千萬級的數據就可以考慮使用分區表了,尤其對於某種屬性有特性的,比如按照時間會進行膨脹的表,可以按天或者按月分個表就美滋滋了。
分區需要注意的
- NULL值會使分區的過濾無效。(當然這個一般不會出現,分區鍵肯定會設置成not null 的)
- 分區列和索引列不匹配。(索引和列不匹配會導致不走索引,這不是廢話)
- 分區的數量要有數,一般100個分區就是極限了。
- 打開並鎖住所有底層表的成本可能很高。(當訪問分區表的時候,會打開並鎖住所有的底層表,這是另一個開銷。無法通過過濾來規避)
- 維護的程度。在插入的時候需要寫一些function
- 所有的分區需要使用相同的存儲引擎。
- 有些存儲引擎不支持分區,當然innodb肯定是支持的。
- 在 MySQL 中,創建分區表時候有一個限制條件:分區表中的每個唯一索引(包括主鍵),都必須包含分區表達式中的所有列。
分區的語句:
CREATE TABLE test (
id int(8) not null,
username VARCHAR(20) NOT NULL,
created DATETIME NOT NULL,
PRIMARY KEY(id, created)
)
PARTITION BY RANGE( YEAR(created) )(
PARTITION from_2013_or_less VALUES LESS THAN (2014),
PARTITION from_2014 VALUES LESS THAN (2015),
PARTITION from_2015 VALUES LESS THAN (2016),
PARTITION from_2016_and_up VALUES LESS THAN MAXVALUE);
INSERT into test(id,username,created) values(1,'test','2013-01-01');
INSERT into test(id,username,created) values(2,'test','2014-01-01');
INSERT into test(id,username,created) values(3,'test','2015-01-01');
INSERT into test(id,username,created) values(4,'test','2016-01-01');
INSERT into test(id,username,created) values(5,'test','2017-01-01');
explain select * from test where created<'2015-01-01' and created > '2014-01-01'; #使用範圍查詢
explain select * from test where created<'2015-01-01' ; # 返回2013和2014兩個分區表
DDL可以看到如下
CREATE TABLE `test` (
`id` int(8) NOT NULL,
`username` varchar(20) NOT NULL,
`created` datetime NOT NULL,
PRIMARY KEY (`id`,`created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE ( YEAR(created))
(PARTITION from_2013_or_less VALUES LESS THAN (2014) ENGINE = InnoDB,
PARTITION from_2014 VALUES LESS THAN (2015) ENGINE = InnoDB,
PARTITION from_2015 VALUES LESS THAN (2016) ENGINE = InnoDB,
PARTITION from_2016_and_up VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */;
存儲過程
mysql的存儲過程包括幾部分。可以參照下面一個授權取數據的小需求來參考。(暫時不關注sql的性能問題)
DELIMITER // #這個是定義執行語句的分隔符。 如果不改的話默認是 ; 分割。在mysql客戶端執行是有問題的。會導致識別存儲過程的函數體異常。
CREATE PROCEDURE get_module_sn_data_pro(in _name text)
begin #這個是聲明函數體的開始
declare total_name text;
declare var_name_index int(5); #聲明變量時給變量的類型存儲大小
declare var_name_value text;
declare module_name text;
declare sql_name text;
declare tmp_sql text;
declare tmp_var int(3) default 1; #聲明變量的時候賦一個默認值。
set total_name = concat(_name,',tmp_value'); #使用concat來進行字符串連接操作
while LOCATE(',',total_name) != 0 do
set var_name_value = (select SUBSTRING(total_name,1,INSTR(total_name,',')-1) );
set var_name_index = (select INSTR(total_name,','));
set module_name = var_name_value;
set tmp_sql = concat("(SELECT c_endtime,
c_functional -> '$.",module_name,".authorized_time' AS mod_time #一種mysql使用字符串存儲json的手段
" FROM test WHERE
c_functional !='NULL' and RIGHT(c_functional,1)='}' and c_endtime> '2019-12-01 00:00:00';
if tmp_var = 1 then
set sql_name = tmp_sql;
else
set sql_name = concat(sql_name," union all ", tmp_sql);
end if;
set tmp_var = 0;
set total_name = SUBSTRING(total_name,var_name_index+1);
end while;
set @get_module_sn_sql = concat(sql_name);
select @get_module_sn_sql; #第一個結果集輸出的是sql,第二個結果集是真正使用的sql
PREPARE stmt FROM @get_module_sn_sql ; #預編譯一個sql statement
EXECUTE stmt; #執行這個sql (可以這麼理解,因爲底層邏輯可能會深一些)
end #聲明函數體的結束
// # 最開始定義的 // 爲mysql客戶端執行命令的結束符號。
DELIMITER ; #跑完存儲過程的創建函數後把這個聲明給改回來。
刪掉存儲過程
drop PROCEDURE if exists get_module_sn_data_pro;
執行存儲過程
call get_module_sn_data_pro("qwe,rty"); #括號裏面是參數不需要管
有關mysql存儲過程的一些坑
1.declare test_name varchar(30) ; 必須有這個長度。 這個是使用 declare聲明變量的時候需要注意的事情。
2.給變量賦值要是左邊是sql 必須用() 擴起來。 這個是給變量賦值的時候需要注意的事情。
3.儘量使用declare這種作用域小的聲明變量方式。 這個沒遇到坑。但是感覺大量使用@這種會話變量會有一些坑
mysql存儲過程中,定義變量有兩種方式:
1.使用set或select直接賦值,變量名以 @ 開頭.
例如:set @var=1;
可以在一個會話的任何地方聲明,作用域是整個會話,稱爲會話變量。會話變量作用域會暴露、
2.以 DECLARE 關鍵字聲明的變量,只能在存儲過程中使用,稱爲存儲過程變量,例如:
DECLARE var1 INT DEFAULT 0;
主要用在存儲過程中,或者是給存儲傳參數中。
兩者的區別是:
在調用存儲過程時,以DECLARE聲明的變量都會被初始化爲 NULL。而會話變量(即@開頭的變量)則不會被再初始化,在一個會話內,只須初始化一次,之後在會話內都是對上一次計算的結果,就相當於在是這個會話內的全局變量。
在存儲過程中,使用動態語句,預處理時,動態內容必須賦給一個會話變量。
觸發器則是某個insert update delete執行了會觸發。
MySQL如何使用字符集
只有基於字符的存儲纔跟字符集有關係,其他類型跟字符集沒啥關係。
MySQL的設置可以分爲兩類:創建對象時的默認值、在服務器和客戶端通信時的設置。
MySQL服務器有默認的字符集和校驗規則,每個數據庫也有自己的默認值,每個表也有自己的默認值,每個列也有默認值。這是一個逐層繼承的默認設置。
- 創建數據庫時根據服務器上的character_set_server來設置該數據庫的默認字符集。
- 創建表的時候,將根據數據庫的字符集設置來指定這個表的字符集設置。
- 創建列的時候,根據表的設置指定列的字符集設置。
當列沒有設置字符集的時候纔會繼承表的字符集。
- 服務端總是假定客戶端是根據character_set_client設置的字符集來傳輸數據sql語句的。 後臺和MySQL的傳輸!
- 當服務器收到客戶端的sql語句時,它先將其轉換成character_set_connection 它還使用這個設置來決定如何將數據轉換成字符串。
- 當服務器端返回數據或者錯誤信息給客戶端是,他會將其轉換成character_set_result
有的需要去配置文件修改。
MySQL再比較兩個字符的時候,即使不相同的字符集會轉換成相同的字符集進行比較,如果轉換失敗就拋異常了。
可以通過convert函數轉碼字符集。
選擇字符集和校對規則
對於校對規則通常需要考慮的一個問題是,是否以大小寫敏感的方式比較字符串,或者是以字符串編碼的二進制值來比較大小。
對應得校對規則的前綴是_cs、ci、_bin
全文索引
mysql5.6之前是隻有myisam支持全文索引,5.6之後的innodb也支持全文索引了。
innodb存儲引擎創建全文索引如下所示,選擇fulltext全文索引不能選b-tree和hash等別的索引方式。
ALTER TABLE pressure ADD FULLTEXT INDEX fulltext_pre(hobby);
全文索引的性能是比較差的,我實際項目開發中還沒有使用過MySQL的全文索引
分佈式(XA)事務
存儲引擎的事務特性能保證在存儲引擎級別實現ACID,而分佈式事務可以讓存儲引擎級別的ACID擴展到數據庫層面。分兩個階段
- XA事務需要有一個事務協調器來保證所有的事務參與者都完成了準備工作。
- 如果協調器收到所有的參與者都準備好的消息,就會告訴所有的事務可以提交了。
分佈式事務分爲內部XA事務和外部XA事務。
內部XA事務
例如:存儲引擎和寫入日誌文件。這就是一個分佈式事務。
XA事務爲Mysql帶來了巨大的性能下降,從5.0開始,它破壞了MySQL內部的批量提交。使得MySQL不得不多次調用fsync
一個事務如果開啓了二進制日誌,則不僅對二進制日誌進行持久化操作,innodb事務日誌還需要兩次日誌持久化操作。
外部XA事務
MySQL能夠作爲一個參與者完成一個外部的分佈式事務。因爲通信的延遲,外部XA事務消耗更大。因爲一個參與者阻塞會導致其他參與者等待。
XA事務是一種在多個服務器之間同步數據的方法,如果由於某些原因不能使用MySQL本身的複製,或者性能並不是瓶頸的時候,可以嘗試使用。
查詢緩存
很多數據庫產品都能夠緩存查詢的執行計劃,對於相同類型的SQL就可以跳過SQL解析和執行計劃生成階段。
MySQL還有一種不同的緩存類型:緩存完整的select查詢結果,也就是查詢緩存。
MySQL查詢緩存保存查詢返回的完整結果,當查詢命中改緩存,MySQL會立刻返回結果,跳過了解析、優化和執行階段。
查詢緩存會跟蹤查詢中涉及的每個表,如果這些表發生變化,那麼和這個表相關的所有緩存數據都將失效。“這種簡單實現代價很小,對於一個繁重的系統來說非常重要”
隨着現在通用服務器越來越強大,查詢緩存被發現是一個影響服務器擴展性的因素。它可能成爲整個服務器資源競爭單點,在多核服務器上還可能導致服務器僵死。
大部分認爲應該默認關閉查詢緩存,如果很需求的話就配置一個很小的查詢緩存空間(幾十兆)
query_cache_type 字段是判斷是否打開查詢緩存的配置。
MySQL如何判斷緩存命中?
緩存存放在一個引用表中,通過一個哈希值引用,這個哈希值包括:查詢本身,當前要查詢的數據庫,客戶端版本協議等。
當判斷緩存是否命中時,MySQL不會解析、正規化或者參數化查詢語句,而是直接使用SQL語句和客戶端發送過來的其他原始信息。
- 任何字符上的不同例如:空格,註釋---任何的不同都會導致緩存的不命中。所以使用統一的編碼是很有必要的。
- 當查詢有一些不確定的值時不會被緩存例如:now()
- 當查詢中包含任何用戶自定義函數,存儲函數,用戶變量,臨時表,MySQL中的系統表或者任何包含列級別權限的表,都不會被緩存。
- 當查詢緩存可使用內存滿了,MySQL就會清除一些查詢緩存結果。
在檢查查詢緩存之前,MySQL只做一件事情,通過一個不區分大小寫的檢查看SQL語句是不是以sel開頭。select
tips: 如果查詢緩存使用了大量的內存,緩存失效操作就可能成爲一個非常嚴重的問題瓶頸。如果緩存存放了大量的結果,緩存失效操作時會導致整個系統僵死一會兒,因爲這個操作是靠一個全局鎖操作保護的,所有需要做該操作的查詢都要等待這個鎖,而且無論是檢查是否命中緩存、還是緩存失效檢測都需要等待這個全局鎖。
查詢緩存如何使用內存?
查詢緩存是完全存儲在內存中的,在服務器啓動的時候會初始化查詢緩存需要使用的內存空間。
當有查詢結果需要緩存的時候,MySQL從大空間塊申請一個數據塊用於存儲結果。這個數據塊需要大於參數query_cache_min_res_unit的配置。需要一步步從小到大分配給查詢緩存結果直到數據塊足夠大。小的存滿,新申請大的繼續存。這其中的分配是MySQL管理的。
tips: 對於多個查詢緩存來說,很容易產生內存碎片。碎片的大小小於query_cache_min_res_unit ,無法被新的查詢緩存來存儲結果。
需要內存回收機制來回收內存碎片。減少碎片可以通過對query_cache_min_res_unit參數的調小。
flush query cache來清碎片。不會清查詢緩存結果。會進行重排序。
MySQL接到一個select查詢的時候,要麼增加Qcache_hits的值,要麼增加Com_select的值。
查詢緩存命中率: Qcache_hits/(Qcache_hits+Com_select)
綜上:使用查詢緩存要保證佔用內存小,並且足夠大的收益。