MySQL-視圖-觸發器-事務-存儲過程-函數-流程式控制制-索引與慢查詢優化-06
- 2019 年 10 月 7 日
- 筆記
目錄
小科普
核心業務邏輯程式碼一般都是放在服務端的 客戶端容易被懂行點的人修改源碼,造成損失
視圖***
什麼是視圖
一個查詢語句的結果是虛擬表,將(查詢出)這張虛擬表(的sql語句)保存下來,他就變成了一個視圖(mysql中還是以表的形式存在的)
為什麼要用視圖
當頻繁需要用到多張表的聯表結果,你就可以事先生成好視圖,之後直接調用即可,避免了反覆寫聯表操作的 sql 語句(實際效果相當於再次執行語句)
如何生成視圖
# 語法 create view 視圖名 as 生成虛擬表的查詢語句 create view teacher_course as select * from teacher inner join course on teacher.tid = course.teacher_id; show tables # 即可看到該視圖(當做表了) select * from teacher_course; # 看到的就是
修改視圖 –> 最好(千萬)不要

關聯表數據改動前

關聯表數據改動之後
視圖中的數據自動更新了–> 執行查詢視圖記錄語句 = 重新執行了創建視圖的那個sql 語句

注意: 1.視圖只有表結構,視圖中的數據還是來源於原來的表 2.不要改動視圖表中的數據(可能會報錯,也可能會改其他表的數據) —> 我在cmd終端改動影響到了其他表 3.不要太依賴視圖,盡量少用視圖來寫業務邏輯
- 視圖會影響性能,佔用硬碟資源、資料庫資源
- 工作中可能是別的部門的人管理資料庫,跨部門交流比較麻煩,而且如果別人改動了視圖關聯表,或刪了視圖那涉及到的業務邏輯就很危險了。
觸發器
什麼是觸發器
到達某個條件自動觸發
觸發條件
當你在對數據進行增刪改的情況下會自動觸發觸發器的運行
觸發器語法結構
修改mysql的默認結束符(;)
delimiter $$ # --> 默認 ; 改成了 $$
,只對當前窗口有效,重新登錄也會無效
delimiter $$ # 改變當前窗口sql 語句的結束符 create trigger 觸發器的名字 after/before insert/update/delete on 表名 for each row begin # 操作其他表的sql 語句 end $$ delimiter ; # 把sql結束符改回 ; # 觸發器常見命名格式:tri_before/after_insert/update/delete_表名
觸發器死循環
觸發器裡面的程式碼會觸發觸發器本身的執行,造成了死循環
在觸發器裡面千萬不要寫操作本表的語句,增刪改查都不要,會報錯
比如:user表的 新增前觸發器 程式碼塊內寫的是 user表的新增插值操作,就會造成觸發器死循環(直接報錯)
六個觸發器的執行時機
新增前、新增後、刪除前、刪除後、更新前、更新後
小案例(新增後)
create table user( id int primary key auto_increment, name varchar(32) not null, password varchar(255) not null ) create table log( id int primary key auto_increment, message varchar(255) ); drop trigger tri_after_insert_user; # 刪除已存在的觸發器 tri_after_insert_user delimiter $$ create trigger tri_after_insert_user after insert on user for each row begin # insert into user(name, password) values('老子翻車了', '123'); # 死循環了,像遞歸,不斷觸發這個觸發器 # select * from user; # 也會報錯,觸發器里不能返回值 Not allowed to return a result set from a trigger insert into log(message) values ('看到我就說明你沒翻車'); end $$ delimiter ; insert into user(name, password) values('會翻車嗎', '可能吧'); select * from user; select * from log; # 刪除觸發器 drop trigger tri_after_insert_user;
擴展: 視圖、存儲過程的查看
摘抄自:mysql查看存儲過程函數
# 查詢資料庫中的存儲過程和函數 select `name` from mysql.proc where db = 'xx' and `type` = 'PROCEDURE' # 存儲過程 select `name` from mysql.proc where db = 'xx' and `type` = 'FUNCTION' # 函數 show procedure statusG; # 存儲過程(G 豎式排版查看,橫的太長了 看不清) show function statusG; # 函數 # 查看存儲過程或函數的創建程式碼 show create procedure proc_nameG; show create function func_nameG; # 查看視圖 SELECT * from information_schema.VIEWS; # 視圖 SELECT * from information_schema.TABLES; # 表 # 查看觸發器 SHOW TRIGGERS [FROM db_name] [LIKE expr] SELECT * FROM triggers T WHERE trigger_name="mytrigger"G;
NEW對象指代的就是當前記錄(對象)
NEW 對象可以取到觸發這個觸發器的sql語句的記錄對象,通過 . 欄位名
的方式來獲取到欄位值
# 案例 (mysql大小寫不敏感) CREATE TABLE cmd ( id INT PRIMARY KEY auto_increment, USER CHAR (32), priv CHAR (10), cmd CHAR (64), sub_time datetime, #提交時間 success enum ('yes', 'no') #0代表執行失敗 ); CREATE TABLE errlog ( id INT PRIMARY KEY auto_increment, err_cmd CHAR (64), err_time datetime ); delimiter $$ # 將mysql默認的結束符由;換成$$ create trigger tri_after_insert_cmd after insert on cmd for each row begin if NEW.success = 'no' then # 新記錄都會被MySQL封裝成NEW對象 insert into errlog(err_cmd,err_time) values(NEW.cmd,NEW.sub_time); end if; end $$ delimiter ; # 結束之後記得再改回來,不然後面結束符就都是$$了 #往表cmd中插入記錄,觸發觸發器,根據IF的條件決定是否插入錯誤日誌 INSERT INTO cmd ( USER, priv, cmd, sub_time, success ) VALUES ('egon','0755','ls -l /etc',NOW(),'yes'), ('egon','0755','cat /etc/passwd',NOW(),'no'), ('egon','0755','useradd xxx',NOW(),'no'), ('egon','0755','ps aux',NOW(),'yes'); # 查詢errlog表記錄 select * from errlog; # 刪除觸發器 drop trigger tri_after_insert_cmd;

事務 *****
什麼是事務
事務包含一堆sql語句,要麼全部成功,要麼都不成功
事務的四大特性 ACID
用自己的話背下來
A:原子性 atomicity
一個事務是一個不可分割的工作單位,事務中包括的諸操作要麼都做,要麼都不做。
C:一致性 consistency
事務必須是使資料庫從一個一致性狀態變到另一個一致性狀態。 一致性與原子性是密切相關的。
I:隔離性 isolation
一個事務的執行不能被其他事務干擾。 即一個事務內部的操作及使用的數據對並發的其他事務是隔離的,並發執行的各個事務之間不能互相干擾。
D:持久性 durability
持久性也稱永久性(permanence),指一個事務一旦提交,它對資料庫中數據的改變就應該是永久性的。 接下來的其他操作或故障不應該對其有任何影響。
如何開啟事務
start transaction
標誌下面的語句都是 事務
事務開始後,只會在記憶體中修改
只有commit 之後才會寫到硬碟上
事務回滾
rollback
事務開始的地方到這裡的語句都會回滾(失效)
永久性更改
commit
把數據刷在硬碟上,後面再 rollback
就回滾不回去了
start transaction、rollback、commit 有點像python異常捕獲的 try … except … else …
小案例
create table user( id int primary key auto_increment, name char(32), balance int ); insert into user(name,balance) values ('wsb',1000), ('egon',1000), ('ysb',1000); # 修改數據之前先開啟事務操作 start transaction; # 修改操作 update user set balance=900 where name='wsb'; #買支付100元 update user set balance=1010 where name='egon'; #中介拿走10元 update user set balance=1090 where name='ysb'; #賣家拿到90元 # 回滾到上一個狀態 rollback; # 開啟事務之後,只要沒有執行commit操作,數據其實都沒有真正刷新到硬碟 commit; """開啟事務檢測操作是否完整,不完整主動回滾到上一個狀態,如果完整就應該執行commit操作""" # 站在python程式碼的角度,應該實現的偽程式碼邏輯, try: update user set balance=900 where name='wsb'; #買支付100元 update user set balance=1010 where name='egon'; #中介拿走10元 update user set balance=1090 where name='ysb'; #賣家拿到90元 except 異常: rollback; else: commit; # 那如何檢測異常?
存儲過程
什麼是存儲過程
就類似於python中的自定義函數
內部封裝了 sql 語句,後續想要實現相應的操作,只需要調用存儲過程即可
如何創建存儲過程
語法結構
# 無參數版 delimiter $$ # 改mysql的結束符 create procedure 存儲結構名字() begin sql 語句; end delimiter ; # 改回來 call 存儲結構名字() # 調用寫好的存儲過程 # 有參數版 delimiter $$ # 改mysql的結束符 create procedure 存儲結構名字( in m int, # in 只能傳進來,不能返回 in n int, # 參數對應的意思---> 進還是出, 變數名, 數據類型 out res int, # out 只能返回,不能傳 inout xxx int, # inout 可以傳進來,也可以被返回 ) begin sql 語句; end delimiter ; # 改回來 call 存儲結構名字(m, n) # 參數怎麼傳不知道。。
案例
存儲過程在哪個庫里定義就只能在哪個庫裡面使用
定義存儲過程
delimiter $$ create procedure p1( in m int, # in 只能傳進來,不能被返回 in n int, # 參數對應的意思---> 進還是出, 變數名, 數據類型 out res int # out 只能被返回,不能傳入 ) begin select tname from teacher where tid > m and tid < n; set res=0; # 就類似於一個標誌位,用來標識存儲器是否執行成功 end $$ delimiter ; show procedure statusG; # 查看存儲過程(豎式展示排版)
在mysql中調用存儲過程
存變數(設置初始值)
set @res=10; # 設置全局變數 @res 等於10
,select @res; # 查看全局變數 @res 的值

調用
call p1(1,5,@res);
將變數 @res
傳入,之後可以通過 select @res
來查看存儲過程執行完成後的返回結果

在pymysql中調用存儲過程
# 使用的是上一步創建的存儲過程 import pymysql conn = pymysql.connect( host='127.0.0.1', port=3306, user='root', password='000000', database='day38', charset='utf8', autocommit=True, ) cursor = conn.cursor(pymysql.cursors.DictCursor) # -------------------------------------------- # cursor.callproc() 調用存儲過程 # 內部自動用變數名存儲對應值(看下面案例注釋) # -------------------------------------------- cursor.callproc('p1', (1, 5, 10)) # 這裡就不需要設置那個全局變數了(@res=10),內部自動用變數名存儲了對應的值 print(cursor.fetchall()) # [{'tname': '李平老師'}, {'tname': '劉海燕老師'}, {'tname': '朱雲海老師'}] ''' callproc 內部自動用變數名存儲了對應的值 @_p1_0=1 @_p1_1=5 @_p1_2=10 # 自動取名規律: @_存儲過程名_標號 ''' cursor.execute('select @_p1_0=1') print(cursor.fetchall()) # [{'@_p1_0=1': 1}] cursor.execute('select @_p1_1=5') print(cursor.fetchall()) # [{'@_p1_1=5': 1}] cursor.execute('select @_p1_2=10') print(cursor.fetchall()) # [{'@_p1_2=10': 0}]

案例– 使用存儲過程監測事務
監測一個事務是否成功,通常使用存儲過程包起來
# 大前提:存儲過程在哪個庫下面創建的只能在對應的庫下面才能使用!!! delimiter // create PROCEDURE p5( OUT p_return_code tinyint ) BEGIN DECLARE exit handler for sqlexception BEGIN -- ERROR set p_return_code = 1; rollback; END; DECLARE exit handler for sqlwarning BEGIN -- WARNING set p_return_code = 2; rollback; END; START TRANSACTION; update user set balance=900 where id =1; update user123 set balance=1010 where id = 2; update user set balance=1090 where id =3; COMMIT; -- SUCCESS set p_return_code = 0; #0代表執行成功 END // delimiter ;
函數
注意與存儲過程的區別,mysql內置的函數只能在sql語句中使用!
參考部落格:函數
MySQL內置函數
常見函數及練習
一、數學函數 ROUND(x,y) 返回參數x的四捨五入的有y位小數的值 RAND() 返回0到1內的隨機值,可以通過提供一個參數(種子)使RAND()隨機數生成器生成一個指定的值。 二、聚合函數(常用於GROUP BY從句的SELECT查詢中) AVG(col)返回指定列的平均值 COUNT(col)返回指定列中非NULL值的個數 MIN(col)返回指定列的最小值 MAX(col)返回指定列的最大值 SUM(col)返回指定列的所有值之和 GROUP_CONCAT(col) 返回由屬於一組的列值連接組合而成的結果 三、字元串函數 CHAR_LENGTH(str) 返回值為字元串str 的長度,長度的單位為字元。一個多位元組字元算作一個單字元。 CONCAT(str1,str2,...) 字元串拼接 如有任何一個參數為NULL ,則返回值為 NULL。 CONCAT_WS(separator,str1,str2,...) 字元串拼接(自定義連接符) CONCAT_WS()不會忽略任何空字元串。 (然而會忽略所有的 NULL)。 CONV(N,from_base,to_base) 進位轉換 例如: SELECT CONV('a',16,2); 表示將 a 由16進位轉換為2進位字元串表示 FORMAT(X,D) 將數字X 的格式寫為'#,###,###.##',以四捨五入的方式保留小數點後 D 位, 並將結果以字元串的形式返回。若 D 為 0, 則返回結果不帶有小數點,或不含小數部分。 例如: SELECT FORMAT(12332.1,4); 結果為: '12,332.1000' INSERT(str,pos,len,newstr) 在str的指定位置插入字元串 pos:要替換位置其實位置 len:替換的長度 newstr:新字元串 特別的: 如果pos超過原字元串長度,則返回原字元串 如果len超過原字元串長度,則由新字元串完全替換 INSTR(str,substr) 返回字元串 str 中子字元串的第一個出現位置。 LEFT(str,len) 返回字元串str 從開始的len位置的子序列字元。 LOWER(str) 變小寫 UPPER(str) 變大寫 REVERSE(str) 返回字元串 str ,順序和字元順序相反。 SUBSTRING(str,pos) , SUBSTRING(str FROM pos) SUBSTRING(str,pos,len) , SUBSTRING(str FROM pos FOR len) 不帶有len 參數的格式從字元串str返回一個子字元串,起始於位置 pos。帶有len參數的格式從字元串str返回一個長度同len字元相同的子字元串,起始於位置 pos。 使用 FROM的格式為標準 SQL 語法。也可能對pos使用一個負值。假若這樣,則子字元串的位置起始於字元串結尾的pos 字元,而不是字元串的開頭位置。在以下格式的函數中可以對pos 使用一個負值。 mysql> SELECT SUBSTRING('Quadratically',5); -> 'ratically' mysql> SELECT SUBSTRING('foobarbar' FROM 4); -> 'barbar' mysql> SELECT SUBSTRING('Quadratically',5,6); -> 'ratica' mysql> SELECT SUBSTRING('Sakila', -3); -> 'ila' mysql> SELECT SUBSTRING('Sakila', -5, 3); -> 'aki' mysql> SELECT SUBSTRING('Sakila' FROM -4 FOR 2); -> 'ki' 四、日期和時間函數 CURDATE()或CURRENT_DATE() 返回當前的日期 CURTIME()或CURRENT_TIME() 返回當前的時間 DAYOFWEEK(date) 返回date所代表的一星期中的第幾天(1~7) DAYOFMONTH(date) 返回date是一個月的第幾天(1~31) DAYOFYEAR(date) 返回date是一年的第幾天(1~366) DAYNAME(date) 返回date的星期名,如:SELECT DAYNAME(CURRENT_DATE); FROM_UNIXTIME(ts,fmt) 根據指定的fmt格式,格式化UNIX時間戳ts HOUR(time) 返回time的小時值(0~23) MINUTE(time) 返回time的分鐘值(0~59) MONTH(date) 返回date的月份值(1~12) MONTHNAME(date) 返回date的月份名,如:SELECT MONTHNAME(CURRENT_DATE); NOW() 返回當前的日期和時間 QUARTER(date) 返回date在一年中的季度(1~4),如SELECT QUARTER(CURRENT_DATE); WEEK(date) 返回日期date為一年中第幾周(0~53) YEAR(date) 返回日期date的年份(1000~9999) 重點: DATE_FORMAT(date,format) 根據format字元串格式化date值 mysql> SELECT DATE_FORMAT('2009-10-04 22:23:00', '%W %M %Y'); -> 'Sunday October 2009' mysql> SELECT DATE_FORMAT('2007-10-04 22:23:00', '%H:%i:%s'); -> '22:23:00' mysql> SELECT DATE_FORMAT('1900-10-04 22:23:00', -> '%D %y %a %d %m %b %j'); -> '4th 00 Thu 04 10 Oct 277' mysql> SELECT DATE_FORMAT('1997-10-04 22:23:00', -> '%H %k %I %r %T %S %w'); -> '22 22 10 10:23:00 PM 22:23:00 00 6' mysql> SELECT DATE_FORMAT('1999-01-01', '%X %V'); -> '1998 52' mysql> SELECT DATE_FORMAT('2006-06-00', '%d'); -> '00' 五、加密函數 MD5() 計算字元串str的MD5校驗和 PASSWORD(str) 返回字元串str的加密版本,這個加密過程是不可逆轉的,和UNIX密碼加密過程使用不同的演算法。 六、控制流函數 CASE WHEN[test1] THEN [result1]...ELSE [default] END 如果testN是真,則返回resultN,否則返回default CASE [test] WHEN[val1] THEN [result]...ELSE [default]END 如果test和valN相等,則返回resultN,否則返回default IF(test,t,f) 如果test是真,返回t;否則返回f IFNULL(arg1,arg2) 如果arg1不是空,返回arg1,否則返回arg2 NULLIF(arg1,arg2) 如果arg1=arg2返回NULL;否則返回arg1 七、控制流函數小練習 #7.1、準備表 /* Navicat MySQL Data Transfer Source Server : localhost_3306 Source Server Version : 50720 Source Host : localhost:3306 Source Database : student Target Server Type : MYSQL Target Server Version : 50720 File Encoding : 65001 Date: 2018-01-02 12:05:30 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for course -- ---------------------------- DROP TABLE IF EXISTS `course`; CREATE TABLE `course` ( `c_id` int(11) NOT NULL, `c_name` varchar(255) DEFAULT NULL, `t_id` int(11) DEFAULT NULL, PRIMARY KEY (`c_id`), KEY `t_id` (`t_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of course -- ---------------------------- INSERT INTO `course` VALUES ('1', 'python', '1'); INSERT INTO `course` VALUES ('2', 'java', '2'); INSERT INTO `course` VALUES ('3', 'linux', '3'); INSERT INTO `course` VALUES ('4', 'web', '2'); -- ---------------------------- -- Table structure for score -- ---------------------------- DROP TABLE IF EXISTS `score`; CREATE TABLE `score` ( `id` int(11) NOT NULL AUTO_INCREMENT, `s_id` int(10) DEFAULT NULL, `c_id` int(11) DEFAULT NULL, `num` double DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of score -- ---------------------------- INSERT INTO `score` VALUES ('1', '1', '1', '79'); INSERT INTO `score` VALUES ('2', '1', '2', '78'); INSERT INTO `score` VALUES ('3', '1', '3', '35'); INSERT INTO `score` VALUES ('4', '2', '2', '32'); INSERT INTO `score` VALUES ('5', '3', '1', '66'); INSERT INTO `score` VALUES ('6', '4', '2', '77'); INSERT INTO `score` VALUES ('7', '4', '1', '68'); INSERT INTO `score` VALUES ('8', '5', '1', '66'); INSERT INTO `score` VALUES ('9', '2', '1', '69'); INSERT INTO `score` VALUES ('10', '4', '4', '75'); INSERT INTO `score` VALUES ('11', '5', '4', '66.7'); -- ---------------------------- -- Table structure for student -- ---------------------------- DROP TABLE IF EXISTS `student`; CREATE TABLE `student` ( `s_id` varchar(20) NOT NULL, `s_name` varchar(255) DEFAULT NULL, `s_age` int(10) DEFAULT NULL, `s_sex` char(1) DEFAULT NULL, PRIMARY KEY (`s_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of student -- ---------------------------- INSERT INTO `student` VALUES ('1', '魯班', '12', '男'); INSERT INTO `student` VALUES ('2', '貂蟬', '20', '女'); INSERT INTO `student` VALUES ('3', '劉備', '35', '男'); INSERT INTO `student` VALUES ('4', '關羽', '34', '男'); INSERT INTO `student` VALUES ('5', '張飛', '33', '女'); -- ---------------------------- -- Table structure for teacher -- ---------------------------- DROP TABLE IF EXISTS `teacher`; CREATE TABLE `teacher` ( `t_id` int(10) NOT NULL, `t_name` varchar(50) DEFAULT NULL, PRIMARY KEY (`t_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of teacher -- ---------------------------- INSERT INTO `teacher` VALUES ('1', '大王'); INSERT INTO `teacher` VALUES ('2', 'alex'); INSERT INTO `teacher` VALUES ('3', 'egon'); INSERT INTO `teacher` VALUES ('4', 'peiqi'); #7.2、統計各科各分數段人數.顯示格式:課程ID,課程名稱,[100-85],[85-70],[70-60],[ <60] select score.c_id, course.c_name, sum(CASE WHEN num BETWEEN 85 and 100 THEN 1 ELSE 0 END) as '[100-85]', sum(CASE WHEN num BETWEEN 70 and 85 THEN 1 ELSE 0 END) as '[85-70]', sum(CASE WHEN num BETWEEN 60 and 70 THEN 1 ELSE 0 END) as '[70-60]', sum(CASE WHEN num < 60 THEN 1 ELSE 0 END) as '[ <60]' from score,course where score.c_id=course.c_id GROUP BY score.c_id;
date_format() 函數(需掌握)
CREATE TABLE blog ( id INT PRIMARY KEY auto_increment, NAME CHAR (32), sub_time datetime ); INSERT INTO blog (NAME, sub_time) VALUES ('第1篇','2015-03-01 11:31:21'), ('第2篇','2015-03-11 16:31:21'), ('第3篇','2016-07-01 10:21:31'), ('第4篇','2016-07-22 09:23:21'), ('第5篇','2016-07-23 10:11:11'), ('第6篇','2016-07-25 11:21:31'), ('第7篇','2017-03-01 15:33:21'), ('第8篇','2017-03-01 17:32:21'), ('第9篇','2017-03-01 18:31:21'); select date_format(sub_time,'%Y-%m'),count(id) from blog group by date_format(sub_time,'%Y-%m');
自定義函數
注意
- 函數中不要寫sql語句(否則會報錯),函數僅僅只是一個功能,是一個在sql中被應用的功能
- 若要想在begin…end…中寫sql,請用存儲過程
delimiter // create function f1( i1 int, i2 int) returns int BEGIN declare num int; set num = i1 + i2; return(num); END // delimiter ; # 在查詢中使用函數 select f1(11,nid) ,name from tb2; # 刪除函數 drop function func_name;
流程式控制制
if 條件語句
# if條件語句 delimiter // # 修改mysql 默認的語句結束符 CREATE PROCEDURE proc_if () BEGIN declare i int default 0; if i = 1 THEN SELECT 1; ELSEIF i = 2 THEN SELECT 2; ELSE SELECT 7; END IF; END // delimiter ; # 將sql語句默認結束符改回 ;
while 循環
# while循環 delimiter // CREATE PROCEDURE proc_while () BEGIN DECLARE num INT ; SET num = 0 ; WHILE num < 10 DO SELECT num ; SET num = num + 1 ; END WHILE ; END // delimiter ;
索引與慢查詢優化 **
mysql 默認有查詢優化機制,我們不需要再上面花多少精力,能優化的,mysql基本都直接給我們優化了(可適當增加幾個索引)
詳細內容參考egon 的部落格
第八篇:索引原理與慢查詢優化 ,寫的很詳細,案例也很充分,我這個寫的不好?
本篇僅作快速了解,第一遍最好是去看 egon 的部落格(能有個詳細的了解)感覺寫的著實不錯,直接複製過來了。。。
前言(摘抄)
索引是應用程式設計和開發的一個重要方面。
若索引太多,應用程式的性能可能會受到影響,而索引太少,對查詢性能又會產生影響,要找到一個平衡點,這對應用程式的性能至關重要。
一些開發人員總是在事後才想起添加索引—-我一直認為,這源於一種錯誤的開發模式,如果知道數據的使用,從一開始就應該在需要處添加索引。
開發人員往往對資料庫的使用停留在應用的層面,比如編寫SQL語句、存儲過程之類,他們甚至可能不知道索引的存在,或認為事後讓相關DBA加上即可,但DBA往往不夠了解業務的數據流,而添加索引需要通過監控大量的SQL語句進而從中找到問題,這個步驟所需的時間肯定是遠大於初始添加索引所需的時間,並且可能會遺漏一部分的索引。
當然索引也並不是越多越好,我曾經遇到過這樣一個問題: 某台MySQL伺服器io stat顯示磁碟使用率一直處於100%,經過分析後發現是由於開發人員添加了太多的索引,在刪除一些不必要的索引之後,磁碟使用率馬上下降為20%。 可見索引的添加也是非常有技術含量的。
這一塊了解個大概即可,能在寫sql語句時稍微考慮下性能就可以了,畢竟我是要搞開發的呀~
索引
索引的目的在於提高查詢效率,與我們查閱圖書所用的目錄是一個道理:先定位到章,然後定位到該章下的一個小節,然後找到頁數。
索引在MySQL中也叫「鍵 key」, 是存儲引擎用於快速找到記錄的一種數據結構
擴展閱讀
索引原理
資料庫比起查字典,飛機航班等顯然要複雜的多,因為不僅面臨著等值查詢,還有範圍查詢(>、<、between、in)、模糊查詢(like)、並集查詢(or)等等。 那資料庫應該選擇怎麼樣的方式來應對所有的問題呢?我們回想字典的例子,能不能把數據分成段,然後分段查詢呢? 最簡單的如果1000條數據,1到100分成第一段,101到200分成第二段,201到300分成第三段……這樣查第250條數據,只要找第三段就可以了,一下子去除了90%的無效數據。 但如果是1千萬的記錄呢,分成幾段比較好?稍有演算法基礎的同學會想到搜索樹,其平均複雜度是lgN,具有不錯的查詢性能。 但這裡我們忽略了一個關鍵的問題,複雜度模型是基於每次相同的操作成本來考慮的。 而資料庫實現比較複雜,一方面數據是保存在磁碟上的,另外一方面為了提高性能,每次又可以把部分數據讀入記憶體來計算,因為我們知道訪問磁碟的成本大概是訪問記憶體的十萬倍左右,所以簡單的搜索樹難以滿足複雜的應用場景。
磁碟IO與預讀
前面提到了訪問磁碟,那麼這裡先簡單介紹一下磁碟IO和預讀。 磁碟讀取數據靠的是機械運動,每次讀取數據花費的時間可以分為尋道時間、旋轉延遲、傳輸時間三個部分, 尋道時間指的是磁臂移動到指定磁軌所需要的時間,主流磁碟一般在5ms以下; 旋轉延遲就是我們經常聽說的磁碟轉速,比如一個磁碟7200轉,表示每分鐘能轉7200次,也就是說1秒鐘能轉120次,旋轉延遲就是1/120/2 = 4.17ms; 傳輸時間指的是從磁碟讀出或將數據寫入磁碟的時間,一般在零點幾毫秒,相對於前兩個時間可以忽略不計。 那麼訪問一次磁碟的時間,即一次磁碟IO的時間約等於5+4.17 = 9ms左右,聽起來還挺不錯的,但要知道一台500 -MIPS(Million Instructions Per Second)的機器每秒可以執行5億條指令,因為指令依靠的是電的性質,換句話說執行一次IO的時間可以執行約450萬條指令,資料庫動輒十萬百萬乃至千萬級數據,每次9毫秒的時間,顯然是個災難。 下圖是電腦硬體延遲的對比圖,供大家參考:

考慮到磁碟IO是非常高昂的操作,電腦作業系統做了一些優化,當一次IO時,不光把當前磁碟地址的數據,而是把相鄰的數據也都讀取到記憶體緩衝區內,因為局部預讀性原理告訴我們,當電腦訪問一個地址的數據的時候,與其相鄰的數據也會很快被訪問到。 每一次IO讀取的數據我們稱之為一頁(page)。具體一頁有多大數據跟作業系統有關,一般為4k或8k,也就是我們讀取一頁內的數據時候,實際上才發生了一次IO,這個理論對於索引的數據結構設計非常有幫助。
常見索引
- primary key 主鍵索引
- unique key 唯一性索引
- index key 普通索引
- 聯合索引(上面三個展開),下面的聯合索引有介紹
上述三個鍵都可以加快查詢,primary key 和 unique key 除索引外還有額外的約束 外鍵是用來創建表與表之間關聯關係的,不算索引
索引一定是自己建的(key),普通欄位沒有 索引(之前我理解錯了)
索引的本質
通過不斷地縮小想要獲取數據的範圍來篩選出最終想要的結果,同時把隨機的事件變成順序的事件,也就是說,有了這種索引機制,我們可以總是用同一種查找方式來鎖定數據。
索引的缺點
- 在表中有大量數據時,創建索引速度會很慢
- 在索引創建完畢後,對錶的查詢性能會大幅度提升
- 往創建好索引的表(有大量數據)里插入數據會變得非常慢(更新索引導致慢)
小結
雖然索引好用,但應該在合理範圍內去用,並不是越多越好
索引的數據結構 — B+ 樹
前面講了索引的基本原理,資料庫的複雜性,又講了作業系統的相關知識,目的就是讓大家了解,任何一種數據結構都不是憑空產生的,一定會有它的背景和使用場景,我們現在總結一下:
我們需要這種數據結構能夠做些什麼,其實很簡單,那就是:每次查找數據時把磁碟IO次數控制在一個很小的數量級,最好是常數數量級。
那麼我們就想到如果一個高度可控的多路搜索樹是否能滿足需求呢?
就這樣,b+樹應運而生(B+樹是通過二叉查找樹,再由平衡二叉樹,B樹演化而來)。

如上圖,是一顆b+樹,關於b+樹的定義可以參見B+樹,這裡只說一些重點.
淺藍色的塊我們稱之為一個磁碟塊,可以看到每個磁碟塊包含幾個數據項(深藍色所示)和指針(黃色所示),
如磁碟塊1包含數據項17和35,包含指針P1、P2、P3,P1表示小於17的磁碟塊,P2表示在17和35之間的磁碟塊,P3表示大於35的磁碟塊。
真實的數據存在於葉子節點即3、5、9、10、13、15、28、29、36、60、75、79、90、99。
非葉子節點只不存儲真實的數據,只存儲指引搜索方向的數據項,如17、35並不真實存在於數據表中。
b+樹的查找過程
如圖所示,如果要查找數據項29,那麼首先會把磁碟塊1由磁碟載入到記憶體,此時發生一次IO,在記憶體中用二分查找確定29在17和35之間,鎖定磁碟塊1的P2指針,記憶體時間因為非常短(相比磁碟的IO)可以忽略不計,通過磁碟塊1的P2指針的磁碟地址把磁碟塊3由磁碟載入到記憶體,發生第二次IO,29在26和30之間,鎖定磁碟塊3的P2指針,通過指針載入磁碟塊8到記憶體,發生第三次IO,同時記憶體中做二分查找找到29,結束查詢,總計三次IO。
真實的情況是,3層的b+樹可以表示上百萬的數據,如果上百萬的數據查找只需要三次IO,性能提高將是巨大的,如果沒有索引,每個數據項都要發生一次IO,那麼總共需要百萬次的IO,顯然成本非常非常高。
案例:查 71

b+樹性質
索引欄位要盡量的小
通過上面的分析,我們知道IO次數取決於b+數的高度h,假設當前數據表的數據為N,每個磁碟塊的數據項的數量是m,則有h=㏒(m+1)N,當數據量N一定的情況下,m越大,h越小;而m = 磁碟塊的大小 / 數據項的大小,磁碟塊的大小也就是一個數據頁的大小,是固定的,如果數據項占的空間越小,數據項的數量越多,樹的高度越低。
這就是為什麼每個數據項,即索引欄位都要盡量的小,比如int佔4位元組,要比bigint8位元組少一半。這也是為什麼b+樹要求把真實的數據放到葉子節點而不是內層節點,一旦放到內層節點,磁碟塊的數據項會大幅度下降,導致樹增高。當數據項等於1時將會退化成線性表。
索引的最左匹配特性
當b+樹的數據項是複合的數據結構,比如(name,age,sex)的時候,b+數是按照從左到右的順序來建立搜索樹的,比如當(張三,20,F)這樣的數據來檢索的時候,b+樹會優先比較name來確定下一步的所搜方向,如果name相同再依次比較age和sex,最後得到檢索的數據;但當(20,F)這樣的沒有name的數據來的時候,b+樹就不知道下一步該查哪個節點,因為建立搜索樹的時候name就是第一個比較因子,必須要先根據name來搜索才能知道下一步去哪裡查詢。比如當(張三,F)這樣的數據來檢索時,b+樹可以用name來指定搜索方向,但下一個欄位age的缺失,所以只能把名字等於張三的數據都找到,然後再匹配性別是F的數據了, 這個是非常重要的性質,即索引的最左匹配特性。
只有葉子結點存放真實數據,根和樹枝節點存的僅僅是虛擬數據
查詢次數由樹的層級決定,層級越低次數越少查詢速度越快(磁碟塊存的數據越多層級越少越容易拿到數據)
————> 這也是把 id 作為主鍵的原因
一個磁碟塊兒的大小是一定的,那也就意味著能存的數據量是一定的。
如何保證樹的層級最低呢?一個磁碟塊兒存放佔用空間比較小的數據項
聚集索引非聚集索引
索引也有不同的種類,按不同的要求去分(不單單是一個欄位作為索引)
聚集索引(primary key)
聚集索引其實指的就是表的主鍵(一般都是 id欄位)
InnoDB 引擎規定一張表中必須要有主鍵 InnoDB 在建表的時候對應到硬碟上是兩個文件,
.frm 表結構文件
只存放表結構,不可能放索引,也就意味著 InnoDB 的索引跟數據都放在.ibd 表數據文件
中
聚集索引特點:葉子結點放的是一條條完整的記錄
輔助索引(unique,index)
查詢數據的時候不可能都是用 id 作為篩選條件,也可能會用 id 之外的 name,password 等欄位資訊,那麼這個時候就無法利用到聚集索引的加速查詢優勢。
此時就需要給其他欄位建立(的)索引,這些索引就叫輔助索引
輔助索引特點:葉子結點存放的是輔助索引欄位對應的那條記錄的主鍵的值(比如:按照name欄位創建索引,那麼葉子節點存放的是:{name對應的值:name所在的那條記錄的主鍵值})找到後再拿著id 去聚集索引裡面去查

聚集索引和非聚集索引的不同
葉子結點存放的是否是一整行的資訊
覆蓋索引
InnoDB存儲引擎支援覆蓋索引(covering index,或稱索引覆蓋) 使用覆蓋索引的一個好處是:輔助索引不包含整行記錄的所有資訊,故其大小要遠小於聚集索引,因此可以減少大量的IO操作
select name from user where name='jason';
覆蓋索引:只在輔助索引的葉子節點中就已經找到了所有我們想要的數據(條件和要查的欄位相同的時候)
where 條件後面的欄位作為輔助索引,select 後面的欄位正好是(沒有多餘的欄位)要找的數據,如果輔助索引和要找的數據一樣,那就找到了,就是覆蓋索引 –> 個人解釋,沒有權威性
非覆蓋索引
select age from user where name='jason';
雖然查詢的時候查到了輔助索引name,但是要查的是age欄位,所以查到name欄位後還需要利用查到的聚集索引id 才去查找那條記錄中 age 欄位的值
聯合索引
- primary key(host, port) 聯合主鍵索引(一般不用聯合主鍵,主鍵一般都是專門的 id 欄位)
- unique(host, port) 聯合唯一索引
- index(host, port) 聯合普通索引
select count(id) from s1 where name='jason' and gender = 'male' and id > 3 and email = 'xxx'; # 如果上述四個欄位區分度都很高,那給誰建都能加速查詢 # 給email加然而不用email欄位 select count(id) from s1 where name='jason' and gender = 'male' and id > 3; # 給name加然而不用name欄位 select count(id) from s1 where gender = 'male' and id > 3; # 給gender加然而不用gender欄位 select count(id) from s1 where id > 3; # 帶來的問題是所有的欄位都建了索引然而都沒有用到,還需要花費四次建立的時間 create index idx_all on s1(email,name,gender,id); # 最左匹配原則,區分度高的往左放 select count(id) from s1 where name='jason' and gender = 'male' and id > 3 and email = 'xxx'; # 速度變快
慢查詢優化
查詢優化神器-explain 通過分析sql語句來提升效率
設定一個時間檢測所有超出改時間的sql語句,然後針對性的進行優化!
根據使用場景管理並設置合適的索引
注意這塊的小點能提高sql語句性能
摘自egon部落格:,放自己部落格方便看
測試索引
數據準備
#1. 準備表 create table s1( id int, name varchar(20), gender char(6), email varchar(50) ); #2. 創建存儲過程,實現批量插入記錄 delimiter $$ #聲明存儲過程的結束符號為$$ create procedure auto_insert1() BEGIN declare i int default 1; while(i<3000000)do insert into s1 values(i,'egon','male',concat('egon',i,'@oldboy')); set i=i+1; end while; END$$ #$$結束 delimiter ; #重新聲明分號為結束符號 #3. 查看存儲過程 show create procedure auto_insert1G #4. 調用存儲過程 call auto_insert1();
在沒有索引的前提下測試查詢速度
#無索引:mysql根本就不知道到底是否存在id等於333333333的記錄,只能把數據表從頭到尾掃描一遍,此時有多少個磁碟塊就需要進行多少IO操作,所以查詢速度很慢 mysql> select * from s1 where id=333333333; Empty set (0.33 sec)
在表中已經存在大量數據的前提下,為某個欄位段建立索引,建立速度會很慢

在索引建立完畢後,以該欄位為查詢條件時,查詢速度提升明顯

1. mysql先去索引表裡根據b+樹的搜索原理很快搜索到id等於333333333的記錄不存在,IO大大降低,因而速度明顯提升 2. 我們可以去mysql的data目錄下找到該表,可以看到佔用的硬碟空間多了 3.需要注意,如下圖

總結
#1. 一定是為搜索條件的欄位創建索引,比如select * from s1 where id = 333;就需要為id加上索引 #2. 在表中已經有大量數據的情況下,建索引會很慢,且佔用硬碟空間,建完後查詢速度加快 比如create index idx on s1(id);會掃描表中所有的數據,然後以id為數據項,創建索引結構,存放於硬碟的表中。 建完以後,再查詢就會很快了。 #3. 需要注意的是:innodb表的索引會存放於s1.ibd文件中,而myisam表的索引則會有單獨的索引文件table1.MYI MySAM索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址。而在innodb中,表數據文件本身就是按照B+Tree(BTree即Balance True)組織的一個索引結構,這棵樹的葉節點data域保存了完整的數據記錄。這個索引的key是數據表的主鍵,因此innodb表數據文件本身就是主索引。 因為inndob的數據文件要按照主鍵聚集,所以innodb要求表必須要有主鍵(Myisam可以沒有),如果沒有顯式定義,則mysql系統會自動選擇一個可以唯一標識數據記錄的列作為主鍵,如果不存在這種列,則mysql會自動為innodb表生成一個隱含欄位作為主鍵,這欄位的長度為6個位元組,類型為長整型.
正確使用索引
索引未命中
並不是說我們創建了索引就一定會加快查詢速度, 若想利用索引達到預想的提高查詢速度的效果,我們在添加索引時,必須遵循以下問題
範圍問題
範圍問題,或者說條件不明確,條件中出現這些符號或關鍵字:>、>=、<、<=、!= 、between…and…、like、
大於號、小於號

不等於!=

between …and…

like

建立索引的欄位選擇
盡量選擇區分度高的列作為索引,區分度的公式是count(distinct col)/count(*),表示欄位不重複的比例,比例越大我們掃描的記錄數越少,唯一鍵的區分度是1,而一些狀態、性別欄位可能在大數據面前區分度就是0,那可能有人會問,這個比例有什麼經驗值嗎?使用場景不同,這個值也很難確定,一般需要join的欄位我們都要求是0.1以上,即平均1條掃描10條記錄
#先把表中的索引都刪除,讓我們專心研究區分度的問題 mysql> desc s1; +--------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------+-------------+------+-----+---------+-------+ | id | int(11) | YES | MUL | NULL | | | name | varchar(20) | YES | | NULL | | | gender | char(5) | YES | | NULL | | | email | varchar(50) | YES | MUL | NULL | | +--------+-------------+------+-----+---------+-------+ rows in set (0.00 sec) mysql> drop index a on s1; Query OK, 0 rows affected (0.20 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> drop index d on s1; Query OK, 0 rows affected (0.18 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> desc s1; +--------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------+-------------+------+-----+---------+-------+ | id | int(11) | YES | | NULL | | | name | varchar(20) | YES | | NULL | | | gender | char(5) | YES | | NULL | | | email | varchar(50) | YES | | NULL | | +--------+-------------+------+-----+---------+-------+ rows in set (0.00 sec) #先把表中的索引都刪除,讓我們專心研究區分度的問題

我們編寫存儲過程為表s1批量添加記錄,name欄位的值均為egon,也就是說name這個欄位的區分度很低(gender欄位也是一樣的,我們稍後再搭理它) 回憶b+樹的結構,查詢的速度與樹的高度成反比,要想將樹的高低控制的很低,需要保證:在某一層內數據項均是按照從左到右,從小到大的順序依次排開,即左1<左2<左3<... 而對於區分度低的欄位,無法找到大小關係,因為值都是相等的,毫無疑問,還想要用b+樹存放這些等值的數據,只能增加樹的高度,欄位的區分度越低,則樹的高度越高。極端的情況,索引欄位的值都一樣,那麼b+樹幾乎成了一根棍。本例中就是這種極端的情況,name欄位所有的值均為'egon' #現在我們得出一個結論:為區分度低的欄位建立索引,索引樹的高度會很高,然而這具體會帶來什麼影響呢??? #1:如果條件是name='xxxx',那麼肯定是可以第一時間判斷出'xxxx'是不在索引樹中的(因為樹中所有的值均為'egon』),所以查詢速度很快 #2:如果條件正好是name='egon',查詢時,我們永遠無法從樹的某個位置得到一個明確的範圍,只能往下找,往下找,往下找。。。這與全表掃描的IO次數沒有多大區別,所以速度很慢
條件中的 = 和 in 可以亂序(mysql查詢優化器自動優化)
=和in可以亂序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意順序,mysql的查詢優化器會幫你優化成索引可以識別的形式
索引列不能參加運算
索引列不能參與計算,保持列「乾淨」,比如from_unixtime(create_time) = 』2014-05-29』就不能使用到索引,原因很簡單,b+樹中存的都是數據表中的欄位值,但進行檢索時,需要把所有元素都應用函數才能比較,顯然成本太大。所以語句應該寫成create_time = unix_timestamp(』2014-05-29』)

and/or
#1、and與or的邏輯 條件1 and 條件2:所有條件都成立才算成立,但凡要有一個條件不成立則最終結果不成立 條件1 or 條件2:只要有一個條件成立則最終結果就成立 #2、and的工作原理 條件: a = 10 and b = 'xxx' and c > 3 and d =4 索引: 製作聯合索引(d,a,b,c) 工作原理: 對於連續多個and:mysql會按照聯合索引,從左到右的順序找一個區分度高的索引欄位(這樣便可以快速鎖定很小的範圍),加速查詢,即按照d—>a->b->c的順序 #3、or的工作原理 條件: a = 10 or b = 'xxx' or c > 3 or d =4 索引: 製作聯合索引(d,a,b,c) 工作原理: 對於連續多個or:mysql會按照條件的順序,從左到右依次判斷,即a->b->c->d

在左邊條件成立但是索引欄位的區分度低的情況下(name與gender均屬於這種情況),會依次往右找到一個區分度高的索引欄位,加速查詢


經過分析,在條件為name='egon' and gender='male' and id>333 and email='xxx'的情況下,我們完全沒必要為前三個條件的欄位加索引,因為只能用上email欄位的索引,前三個欄位的索引反而會降低我們的查詢效率

最左匹配原則
最左前綴匹配原則,是非常重要的原則,對於組合索引mysql會一直向右匹配直到遇到範圍查詢(>、<、between、like)就停止匹配(指的是範圍大了,有索引速度也慢),比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)順序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引則都可以用到,a,b,d的順序可以任意調整。

其他情況
- 使用函數 select * from tb1 where reverse(email) = 'egon'; - 類型不一致 如果列是字元串類型,傳入條件是必須用引號引起來,不然... select * from tb1 where email = 999; #排序條件為索引,則select欄位必須也是索引欄位,否則無法命中 - order by select name from s1 order by email desc; 當根據索引排序時候,select查詢的欄位如果不是索引,則速度仍然很慢 select email from s1 order by email desc; 特別的:如果對主鍵排序,則還是速度很快: select * from tb1 order by nid desc; - 組合索引最左前綴 如果組合索引為:(name,email) name and email -- 命中索引 name -- 命中索引 email -- 未命中索引 - count(1)或count(列)代替count(*)在mysql中沒有差別了 - create index xxxx on tb(title(19)) #text類型,必須制定長度
其他注意事項
- 避免使用select * - count(1)或count(列) 代替 count(*) (默認是用id 聚集索引去查,效率會高很多) - 創建表時盡量時 char 代替 varchar (自行選擇執行效率還是硬碟資源) - 表的欄位順序固定長度的欄位優先 - 組合索引代替多個單列索引(經常使用多個條件查詢時) - 盡量使用短索引 (單個磁碟片上的數據多,層級少,查的快) - 使用連接(JOIN)來代替子查詢(Sub-Queries) - 連表時注意條件類型需一致 - 索引散列值(重複少)不適合建索引,例:性別不適合(大量重複的,分層不好找)