1. 程式人生 > 資料庫 >MySQL必備的常見知識點彙總整理

MySQL必備的常見知識點彙總整理

本文例項總結了MySQL必備的常見知識點。分享給大家供大家參考,具體如下:

最近在整理 sql 的時候發現一份優秀的筆記,是原作者學習 sql 所做的筆記,分享這份總結給大家,對大家對 sql 的可以來一次全方位的檢漏和排查,感謝原作者 hjzCy 的付出,原文連結放在文章最下方,如果出現錯誤,希望大家共同指出!

登入和退出 MySQL 伺服器

# 登入MySQL
$ mysql -u root -p12345612

# 退出MySQL資料庫伺服器
exit;

基本語法

-- 顯示所有資料庫
show databases;

-- 建立資料庫
CREATE DATABASE test;

-- 切換資料庫
use test;

-- 顯示資料庫中的所有表
show tables;

-- 建立資料表
CREATE TABLE pet (
  name VARCHAR(20),owner VARCHAR(20),species VARCHAR(20),sex CHAR(1),birth DATE,death DATE
);

-- 檢視資料表結構
-- describe pet;
desc pet;

-- 查詢表
SELECT * from pet;

-- 插入資料
INSERT INTO pet VALUES ('puffball','Diane','hamster','f','1990-03-30',NULL);

-- 修改資料
UPDATE pet SET name = 'squirrel' where owner = 'Diane';

-- 刪除資料
DELETE FROM pet where name = 'squirrel';

-- 刪除表
DROP TABLE myorder;

建表約束

主鍵約束

-- 主鍵約束
-- 使某個欄位不重複且不得為空,確保表內所有資料的唯一性。
CREATE TABLE user (
  id INT PRIMARY KEY,name VARCHAR(20)
);

-- 聯合主鍵
-- 聯合主鍵中的每個欄位都不能為空,並且加起來不能和已設定的聯合主鍵重複。
CREATE TABLE user (
  id INT,name VARCHAR(20),password VARCHAR(20),PRIMARY KEY(id,name)
);

-- 自增約束
-- 自增約束的主鍵由系統自動遞增分配。
CREATE TABLE user (
  id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(20)
);

-- 新增主鍵約束
-- 如果忘記設定主鍵,還可以通過SQL語句設定(兩種方式):
ALTER TABLE user ADD PRIMARY KEY(id);
ALTER TABLE user MODIFY id INT PRIMARY KEY;

-- 刪除主鍵
ALTER TABLE user drop PRIMARY KEY;

唯一主鍵

-- 建表時建立唯一主鍵
CREATE TABLE user (
  id INT,UNIQUE(name)
);

-- 新增唯一主鍵
-- 如果建表時沒有設定唯一建,還可以通過SQL語句設定(兩種方式):
ALTER TABLE user ADD UNIQUE(name);
ALTER TABLE user MODIFY name VARCHAR(20) UNIQUE;

-- 刪除唯一主鍵
ALTER TABLE user DROP INDEX name;

非空約束

-- 建表時新增非空約束
-- 約束某個欄位不能為空
CREATE TABLE user (
  id INT,name VARCHAR(20) NOT NULL
);

-- 移除非空約束
ALTER TABLE user MODIFY name VARCHAR(20);

預設約束

-- 建表時新增預設約束
-- 約束某個欄位的預設值
CREATE TABLE user2 (
  id INT,age INT DEFAULT 10
);

-- 移除非空約束
ALTER TABLE user MODIFY age INT;

外來鍵約束

-- 班級
CREATE TABLE classes (
  id INT PRIMARY KEY,name VARCHAR(20)
);

-- 學生表
CREATE TABLE students (
  id INT PRIMARY KEY,-- 這裡的 class_id 要和 classes 中的 id 欄位相關聯
  class_id INT,-- 表示 class_id 的值必須來自於 classes 中的 id 欄位值
  FOREIGN KEY(class_id) REFERENCES classes(id)
);

-- 1. 主表(父表)classes 中沒有的資料值,在副表(子表)students 中,是不可以使用的;
-- 2. 主表中的記錄被副表引用時,主表不可以被刪除。

資料庫的三大設計正規化

1NF

只要欄位值還可以繼續拆分,就不滿足第一正規化。

正規化設計得越詳細,對某些實際操作可能會更好,但並非都有好處,需要對專案的實際情況進行設定。

2NF

在滿足第一正規化的前提下,其他列都必須完全依賴於主鍵列。如果出現不完全依賴,只可能發生在聯合主鍵的情況下:

-- 訂單表
CREATE TABLE myorder (
  product_id INT,customer_id INT,product_name VARCHAR(20),customer_name VARCHAR(20),PRIMARY KEY (product_id,customer_id)
);

實際上,在這張訂單表中,product_name 只依賴於 product_idcustomer_name 只依賴於 customer_id 。也就是說,product_namecustomer_id 是沒用關係的,customer_nameproduct_id 也是沒有關係的。

這就不滿足第二正規化:其他列都必須完全依賴於主鍵列!

CREATE TABLE myorder (
  order_id INT PRIMARY KEY,product_id INT,customer_id INT
);

CREATE TABLE product (
  id INT PRIMARY KEY,name VARCHAR(20)
);

CREATE TABLE customer (
  id INT PRIMARY KEY,name VARCHAR(20)
);

拆分之後,myorder 表中的 product_idcustomer_id 完全依賴於 order_id 主鍵,而 productcustomer 表中的其他欄位又完全依賴於主鍵。滿足了第二正規化的設計!

3NF

在滿足第二正規化的前提下,除了主鍵列之外,其他列之間不能有傳遞依賴關係。

CREATE TABLE myorder (
  order_id INT PRIMARY KEY,customer_phone VARCHAR(15)
);

表中的 customer_phone 有可能依賴於 order_idcustomer_id 兩列,也就不滿足了第三正規化的設計:其他列之間不能有傳遞依賴關係。

CREATE TABLE myorder (
  order_id INT PRIMARY KEY,customer_id INT
);

CREATE TABLE customer (
  id INT PRIMARY KEY,phone VARCHAR(15)
);

修改後就不存在其他列之間的傳遞依賴關係,其他列都只依賴於主鍵列,滿足了第三正規化的設計!

查詢練習

準備資料

-- 建立資料庫
CREATE DATABASE select_test;
-- 切換資料庫
USE select_test;

-- 建立學生表
CREATE TABLE student (
  no VARCHAR(20) PRIMARY KEY,name VARCHAR(20) NOT NULL,sex VARCHAR(10) NOT NULL,birthday DATE,-- 生日
  class VARCHAR(20) -- 所在班級
);

-- 建立教師表
CREATE TABLE teacher (
  no VARCHAR(20) PRIMARY KEY,profession VARCHAR(20) NOT NULL,-- 職稱
  department VARCHAR(20) NOT NULL -- 部門
);

-- 建立課程表
CREATE TABLE course (
  no VARCHAR(20) PRIMARY KEY,t_no VARCHAR(20) NOT NULL,-- 教師編號
  -- 表示該 tno 來自於 teacher 表中的 no 欄位值
  FOREIGN KEY(t_no) REFERENCES teacher(no)
);

-- 成績表
CREATE TABLE score (
  s_no VARCHAR(20) NOT NULL,-- 學生編號
  c_no VARCHAR(20) NOT NULL,-- 課程號
  degree DECIMAL,-- 成績
  -- 表示該 s_no,c_no 分別來自於 student,course 表中的 no 欄位值
  FOREIGN KEY(s_no) REFERENCES student(no),FOREIGN KEY(c_no) REFERENCES course(no),-- 設定 s_no,c_no 為聯合主鍵
  PRIMARY KEY(s_no,c_no)
);

-- 檢視所有表
SHOW TABLES;

-- 新增學生表資料
INSERT INTO student VALUES('101','曾華','男','1977-09-01','95033');
INSERT INTO student VALUES('102','匡明','1975-10-02','95031');
INSERT INTO student VALUES('103','王麗','女','1976-01-23','95033');
INSERT INTO student VALUES('104','李軍','1976-02-20','95033');
INSERT INTO student VALUES('105','王芳','1975-02-10','95031');
INSERT INTO student VALUES('106','陸軍','1974-06-03','95031');
INSERT INTO student VALUES('107','王尼瑪','95033');
INSERT INTO student VALUES('108','張全蛋','95031');
INSERT INTO student VALUES('109','趙鐵柱','95031');

-- 新增教師表資料
INSERT INTO teacher VALUES('804','李誠','1958-12-02','副教授','計算機系');
INSERT INTO teacher VALUES('856','張旭','1969-03-12','講師','電子工程系');
INSERT INTO teacher VALUES('825','王萍','1972-05-05','助教','計算機系');
INSERT INTO teacher VALUES('831','劉冰','1977-08-14','電子工程系');

-- 新增課程表資料
INSERT INTO course VALUES('3-105','計算機導論','825');
INSERT INTO course VALUES('3-245','作業系統','804');
INSERT INTO course VALUES('6-166','數位電路','856');
INSERT INTO course VALUES('9-888','高等數學','831');

-- 新增新增成績表資料
INSERT INTO score VALUES('103','3-105','92');
INSERT INTO score VALUES('103','3-245','86');
INSERT INTO score VALUES('103','6-166','85');
INSERT INTO score VALUES('105','88');
INSERT INTO score VALUES('105','75');
INSERT INTO score VALUES('105','79');
INSERT INTO score VALUES('109','76');
INSERT INTO score VALUES('109','68');
INSERT INTO score VALUES('109','81');

-- 查看錶結構
SELECT * FROM course;
SELECT * FROM score;
SELECT * FROM student;
SELECT * FROM teacher;

1 到 10

-- 查詢 student 表的所有行
SELECT * FROM student;

-- 查詢 student 表中的 name、sex 和 class 欄位的所有行
SELECT name,sex,class FROM student;

-- 查詢 teacher 表中不重複的 department 列
-- department: 去重查詢
SELECT DISTINCT department FROM teacher;

-- 查詢 score 表中成績在60-80之間的所有行(區間查詢和運算子查詢)
-- BETWEEN xx AND xx: 查詢區間,AND 表示 "並且"
SELECT * FROM score WHERE degree BETWEEN 60 AND 80;
SELECT * FROM score WHERE degree > 60 AND degree < 80;

-- 查詢 score 表中成績為 85,86 或 88 的行
-- IN: 查詢規定中的多個值
SELECT * FROM score WHERE degree IN (85,86,88);

-- 查詢 student 表中 '95031' 班或性別為 '女' 的所有行
-- or: 表示或者關係
SELECT * FROM student WHERE class = '95031' or sex = '女';

-- 以 class 降序的方式查詢 student 表的所有行
-- DESC: 降序,從高到低
-- ASC(預設): 升序,從低到高
SELECT * FROM student ORDER BY class DESC;
SELECT * FROM student ORDER BY class ASC;

-- 以 c_no 升序、degree 降序查詢 score 表的所有行
SELECT * FROM score ORDER BY c_no ASC,degree DESC;

-- 查詢 "95031" 班的學生人數
-- COUNT: 統計
SELECT COUNT(*) FROM student WHERE class = '95031';

-- 查詢 score 表中的最高分的學生學號和課程編號(子查詢或排序查詢)。
-- (SELECT MAX(degree) FROM score): 子查詢,算出最高分
SELECT s_no,c_no FROM score WHERE degree = (SELECT MAX(degree) FROM score);

-- 排序查詢
-- LIMIT r,n: 表示從第r行開始,查詢n條資料
SELECT s_no,c_no,degree FROM score ORDER BY degree DESC LIMIT 0,1;

分組計算平均成績

查詢每門課的平均成績。

-- AVG: 平均值
SELECT AVG(degree) FROM score WHERE c_no = '3-105';
SELECT AVG(degree) FROM score WHERE c_no = '3-245';
SELECT AVG(degree) FROM score WHERE c_no = '6-166';

-- GROUP BY: 分組查詢
SELECT c_no,AVG(degree) FROM score GROUP BY c_no;

分組條件與模糊查詢

查詢 score 表中至少有 2 名學生選修,並以 3 開頭的課程的平均分數。

SELECT * FROM score;
-- c_no 課程編號
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 |   92 |
| 103 | 3-245 |   86 |
| 103 | 6-166 |   85 |
| 105 | 3-105 |   88 |
| 105 | 3-245 |   75 |
| 105 | 6-166 |   79 |
| 109 | 3-105 |   76 |
| 109 | 3-245 |   68 |
| 109 | 6-166 |   81 |
+------+-------+--------+

分析表發現,至少有 2 名學生選修的課程是 3-1053-2456-166 ,以 3 開頭的課程是 3-1053-245 。也就是說,我們要查詢所有 3-1053-245degree 平均分。

-- 首先把 c_no,AVG(degree) 通過分組查詢出來
SELECT c_no,AVG(degree) FROM score GROUP BY c_no
+-------+-------------+
| c_no | AVG(degree) |
+-------+-------------+
| 3-105 |   85.3333 |
| 3-245 |   76.3333 |
| 6-166 |   81.6667 |
+-------+-------------+

-- 再查詢出至少有 2 名學生選修的課程
-- HAVING: 表示持有
HAVING COUNT(c_no) >= 2

-- 並且是以 3 開頭的課程
-- LIKE 表示模糊查詢,"%" 是一個萬用字元,匹配 "3" 後面的任意字元。
AND c_no LIKE '3%';

-- 把前面的SQL語句拼接起來,
-- 後面加上一個 COUNT(*),表示將每個分組的個數也查詢出來。
SELECT c_no,AVG(degree),COUNT(*) FROM score GROUP BY c_no
HAVING COUNT(c_no) >= 2 AND c_no LIKE '3%';
+-------+-------------+----------+
| c_no | AVG(degree) | COUNT(*) |
+-------+-------------+----------+
| 3-105 |   85.3333 |    3 |
| 3-245 |   76.3333 |    3 |
+-------+-------------+----------+

多表查詢 - 1

查詢所有學生的 name,以及該學生在 score 表中對應的 c_nodegree

SELECT no,name FROM student;
+-----+-----------+
| no | name   |
+-----+-----------+
| 101 | 曾華   |
| 102 | 匡明   |
| 103 | 王麗   |
| 104 | 李軍   |
| 105 | 王芳   |
| 106 | 陸軍   |
| 107 | 王尼瑪  |
| 108 | 張全蛋  |
| 109 | 趙鐵柱  |
+-----+-----------+

SELECT s_no,degree FROM score;
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 |   92 |
| 103 | 3-245 |   86 |
| 103 | 6-166 |   85 |
| 105 | 3-105 |   88 |
| 105 | 3-245 |   75 |
| 105 | 6-166 |   79 |
| 109 | 3-105 |   76 |
| 109 | 3-245 |   68 |
| 109 | 6-166 |   81 |
+------+-------+--------+

通過分析可以發現,只要把 score 表中的 s_no 欄位值替換成 student 表中對應的 name 欄位值就可以了,如何做呢?

-- FROM...: 表示從 student,score 表中查詢
-- WHERE 的條件表示為,只有在 student.no 和 score.s_no 相等時才顯示出來。
SELECT name,degree FROM student,score
WHERE student.no = score.s_no;
+-----------+-------+--------+
| name   | c_no | degree |
+-----------+-------+--------+
| 王麗   | 3-105 |   92 |
| 王麗   | 3-245 |   86 |
| 王麗   | 6-166 |   85 |
| 王芳   | 3-105 |   88 |
| 王芳   | 3-245 |   75 |
| 王芳   | 6-166 |   79 |
| 趙鐵柱  | 3-105 |   76 |
| 趙鐵柱  | 3-245 |   68 |
| 趙鐵柱  | 6-166 |   81 |
+-----------+-------+--------+

多表查詢 - 2

查詢所有學生的 no 、課程名稱 ( course 表中的 name ) 和成績 ( score 表中的 degree ) 列。

只有 score 關聯學生的 no ,因此只要查詢 score 表,就能找出所有和學生相關的 nodegree

SELECT s_no,degree FROM score;
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 |   92 |
| 103 | 3-245 |   86 |
| 103 | 6-166 |   85 |
| 105 | 3-105 |   88 |
| 105 | 3-245 |   75 |
| 105 | 6-166 |   79 |
| 109 | 3-105 |   76 |
| 109 | 3-245 |   68 |
| 109 | 6-166 |   81 |
+------+-------+--------+

然後查詢 course 表:

+-------+-----------------+
| no  | name      |
+-------+-----------------+
| 3-105 | 計算機導論   |
| 3-245 | 作業系統    |
| 6-166 | 數位電路    |
| 9-888 | 高等數學    |
+-------+-----------------+

只要把 score 表中的 c_no 替換成 course 表中對應的 name 欄位值就可以了。

-- 增加一個查詢欄位 name,分別從 score、course 這兩個表中查詢。
-- as 表示取一個該欄位的別名。
SELECT s_no,name as c_name,degree FROM score,course
WHERE score.c_no = course.no;
+------+-----------------+--------+
| s_no | c_name     | degree |
+------+-----------------+--------+
| 103 | 計算機導論   |   92 |
| 105 | 計算機導論   |   88 |
| 109 | 計算機導論   |   76 |
| 103 | 作業系統    |   86 |
| 105 | 作業系統    |   75 |
| 109 | 作業系統    |   68 |
| 103 | 數位電路    |   85 |
| 105 | 數位電路    |   79 |
| 109 | 數位電路    |   81 |
+------+-----------------+--------+

三表關聯查詢

查詢所有學生的 name 、課程名 ( course 表中的 name ) 和 degree

只有 score 表中關聯學生的學號和課堂號,我們只要圍繞著 score 這張表查詢就好了。

SELECT * FROM score;
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 |   92 |
| 103 | 3-245 |   86 |
| 103 | 6-166 |   85 |
| 105 | 3-105 |   88 |
| 105 | 3-245 |   75 |
| 105 | 6-166 |   79 |
| 109 | 3-105 |   76 |
| 109 | 3-245 |   68 |
| 109 | 6-166 |   81 |
+------+-------+--------+

只要把 s_noc_no 替換成 studentsrouse 表中對應的 name 欄位值就好了。

首先把 s_no 替換成 student 表中的 name 欄位:

SELECT name,score WHERE student.no = score.s_no;
+-----------+-------+--------+
| name   | c_no | degree |
+-----------+-------+--------+
| 王麗   | 3-105 |   92 |
| 王麗   | 3-245 |   86 |
| 王麗   | 6-166 |   85 |
| 王芳   | 3-105 |   88 |
| 王芳   | 3-245 |   75 |
| 王芳   | 6-166 |   79 |
| 趙鐵柱  | 3-105 |   76 |
| 趙鐵柱  | 3-245 |   68 |
| 趙鐵柱  | 6-166 |   81 |
+-----------+-------+--------+

再把 c_no 替換成 course 表中的 name 欄位:

-- 課程表
SELECT no,name FROM course;
+-------+-----------------+
| no  | name      |
+-------+-----------------+
| 3-105 | 計算機導論   |
| 3-245 | 作業系統    |
| 6-166 | 數位電路    |
| 9-888 | 高等數學    |
+-------+-----------------+

-- 由於欄位名存在重複,使用 "表名.欄位名 as 別名" 代替。
SELECT student.name as s_name,course.name as c_name,degree
FROM student,score,course
WHERE student.NO = score.s_no
AND score.c_no = course.no;

子查詢加分組求平均分

查詢 95031 班學生每門課程的平均成績。

score 表中根據 student 表的學生編號篩選出學生的課堂號和成績:

-- IN (..): 將篩選出的學生號當做 s_no 的條件查詢
SELECT s_no,degree FROM score
WHERE s_no IN (SELECT no FROM student WHERE class = '95031');
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 105 | 3-105 |   88 |
| 105 | 3-245 |   75 |
| 105 | 6-166 |   79 |
| 109 | 3-105 |   76 |
| 109 | 3-245 |   68 |
| 109 | 6-166 |   81 |
+------+-------+--------+

這時只要將 c_no 分組一下就能得出 95031 班學生每門課的平均成績:

SELECT c_no,AVG(degree) FROM score
WHERE s_no IN (SELECT no FROM student WHERE class = '95031')
GROUP BY c_no;
+-------+-------------+
| c_no | AVG(degree) |
+-------+-------------+
| 3-105 |   82.0000 |
| 3-245 |   71.5000 |
| 6-166 |   80.0000 |
+-------+-------------+

子查詢 - 1

查詢在 3-105 課程中,所有成績高於 109 號同學的記錄。

首先篩選出課堂號為 3-105 ,在找出所有成績高於 109 號同學的的行。

SELECT * FROM score
WHERE c_no = '3-105'
AND degree > (SELECT degree FROM score WHERE s_no = '109' AND c_no = '3-105');

子查詢 - 2

查詢所有成績高於 109 號同學的 3-105 課程成績記錄。

-- 不限制課程號,只要成績大於109號同學的3-105課程成績就可以。
SELECT * FROM score
WHERE degree > (SELECT degree FROM score WHERE s_no = '109' AND c_no = '3-105');

YEAR 函式與帶 IN 關鍵字查詢

查詢所有和 101108 號學生同年出生的 nonamebirthday 列。

-- YEAR(..): 取出日期中的年份
SELECT no,name,birthday FROM student
WHERE YEAR(birthday) IN (SELECT YEAR(birthday) FROM student WHERE no IN (101,108));

多層巢狀子查詢

查詢 '張旭' 教師任課的學生成績表。

首先找到教師編號:

SELECT NO FROM teacher WHERE NAME = '張旭'

通過 sourse 表找到該教師課程號:

SELECT NO FROM course WHERE t_no = ( SELECT NO FROM teacher WHERE NAME = '張旭' );

通過篩選出的課程號查詢成績表:

SELECT * FROM score WHERE c_no = (
  SELECT no FROM course WHERE t_no = (
    SELECT no FROM teacher WHERE NAME = '張旭'
  )
);

多表查詢

查詢某選修課程多於 5 個同學的教師姓名。

首先在 teacher 表中,根據 no 欄位來判斷該教師的同一門課程是否有至少 5 名學員選修:

-- 查詢 teacher 表
SELECT no,name FROM teacher;
+-----+--------+
| no | name  |
+-----+--------+
| 804 | 李誠  |
| 825 | 王萍  |
| 831 | 劉冰  |
| 856 | 張旭  |
+-----+--------+

SELECT name FROM teacher WHERE no IN (
  -- 在這裡找到對應的條件
);

檢視和教師編號有有關的表的資訊:

SELECT * FROM course;
-- t_no: 教師編號
+-------+-----------------+------+
| no  | name      | t_no |
+-------+-----------------+------+
| 3-105 | 計算機導論   | 825 |
| 3-245 | 作業系統    | 804 |
| 6-166 | 數位電路    | 856 |
| 9-888 | 高等數學    | 831 |
+-------+-----------------+------+

我們已經找到和教師編號有關的欄位就在 course 表中,但是還無法知道哪門課程至少有 5 名學生選修,所以還需要根據 score 表來查詢:

-- 在此之前向 score 插入一些資料,以便豐富查詢條件。
INSERT INTO score VALUES ('101','90');
INSERT INTO score VALUES ('102','91');
INSERT INTO score VALUES ('104','89');

-- 查詢 score 表
SELECT * FROM score;
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 101 | 3-105 |   90 |
| 102 | 3-105 |   91 |
| 103 | 3-105 |   92 |
| 103 | 3-245 |   86 |
| 103 | 6-166 |   85 |
| 104 | 3-105 |   89 |
| 105 | 3-105 |   88 |
| 105 | 3-245 |   75 |
| 105 | 6-166 |   79 |
| 109 | 3-105 |   76 |
| 109 | 3-245 |   68 |
| 109 | 6-166 |   81 |
+------+-------+--------+

-- 在 score 表中將 c_no 作為分組,並且限制 c_no 持有至少 5 條資料。
SELECT c_no FROM score GROUP BY c_no HAVING COUNT(*) > 5;
+-------+
| c_no |
+-------+
| 3-105 |
+-------+

根據篩選出來的課程號,找出在某課程中,擁有至少 5 名學員的教師編號:

SELECT t_no FROM course WHERE no IN (
  SELECT c_no FROM score GROUP BY c_no HAVING COUNT(*) > 5
);
+------+
| t_no |
+------+
| 825 |
+------+

teacher 表中,根據篩選出來的教師編號找到教師姓名:

SELECT name FROM teacher WHERE no IN (
  -- 最終條件
  SELECT t_no FROM course WHERE no IN (
    SELECT c_no FROM score GROUP BY c_no HAVING COUNT(*) > 5
  )
);

子查詢 - 3

查詢 “計算機系” 課程的成績表。

思路是,先找出 course 表中所有 計算機系 課程的編號,然後根據這個編號查詢 score 表。

-- 通過 teacher 表查詢所有 `計算機系` 的教師編號
SELECT no,department FROM teacher WHERE department = '計算機系'
+-----+--------+--------------+
| no | name  | department  |
+-----+--------+--------------+
| 804 | 李誠  | 計算機系   |
| 825 | 王萍  | 計算機系   |
+-----+--------+--------------+

-- 通過 course 表查詢該教師的課程編號
SELECT no FROM course WHERE t_no IN (
  SELECT no FROM teacher WHERE department = '計算機系'
);
+-------+
| no  |
+-------+
| 3-245 |
| 3-105 |
+-------+

-- 根據篩選出來的課程號查詢成績表
SELECT * FROM score WHERE c_no IN (
  SELECT no FROM course WHERE t_no IN (
    SELECT no FROM teacher WHERE department = '計算機系'
  )
);
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-245 |   86 |
| 105 | 3-245 |   75 |
| 109 | 3-245 |   68 |
| 101 | 3-105 |   90 |
| 102 | 3-105 |   91 |
| 103 | 3-105 |   92 |
| 104 | 3-105 |   89 |
| 105 | 3-105 |   88 |
| 109 | 3-105 |   76 |
+------+-------+--------+

UNION 和 NOTIN 的使用

查詢 計算機系電子工程系 中的不同職稱的教師。

-- NOT: 代表邏輯非
SELECT * FROM teacher WHERE department = '計算機系' AND profession NOT IN (
  SELECT profession FROM teacher WHERE department = '電子工程系'
)
-- 合併兩個集
UNION
SELECT * FROM teacher WHERE department = '電子工程系' AND profession NOT IN (
  SELECT profession FROM teacher WHERE department = '計算機系'
);

ANY 表示至少一個 - DESC ( 降序 )

查詢課程 3-105 且成績 <u>至少</u> 高於 3-245score 表。

SELECT * FROM score WHERE c_no = '3-105';
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 101 | 3-105 |   90 |
| 102 | 3-105 |   91 |
| 103 | 3-105 |   92 |
| 104 | 3-105 |   89 |
| 105 | 3-105 |   88 |
| 109 | 3-105 |   76 |
+------+-------+--------+

SELECT * FROM score WHERE c_no = '3-245';
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-245 |   86 |
| 105 | 3-245 |   75 |
| 109 | 3-245 |   68 |
+------+-------+--------+

-- ANY: 符合SQL語句中的任意條件。
-- 也就是說,在 3-105 成績中,只要有一個大於從 3-245 篩選出來的任意行就符合條件,
-- 最後根據降序查詢結果。
SELECT * FROM score WHERE c_no = '3-105' AND degree > ANY(
  SELECT degree FROM score WHERE c_no = '3-245'
) ORDER BY degree DESC;
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 |   92 |
| 102 | 3-105 |   91 |
| 101 | 3-105 |   90 |
| 104 | 3-105 |   89 |
| 105 | 3-105 |   88 |
| 109 | 3-105 |   76 |
+------+-------+--------+

表示所有的 ALL

查詢課程 3-105 且成績高於 3-245score 表。

-- 只需對上一道題稍作修改。
-- ALL: 符合SQL語句中的所有條件。
-- 也就是說,在 3-105 每一行成績中,都要大於從 3-245 篩選出來全部行才算符合條件。
SELECT * FROM score WHERE c_no = '3-105' AND degree > ALL(
  SELECT degree FROM score WHERE c_no = '3-245'
);
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 101 | 3-105 |   90 |
| 102 | 3-105 |   91 |
| 103 | 3-105 |   92 |
| 104 | 3-105 |   89 |
| 105 | 3-105 |   88 |
+------+-------+--------+

複製表的資料作為條件查詢

查詢某課程成績比該課程平均成績低的 score 表。

-- 查詢平均分
SELECT c_no,AVG(degree) FROM score GROUP BY c_no;
+-------+-------------+
| c_no | AVG(degree) |
+-------+-------------+
| 3-105 |   87.6667 |
| 3-245 |   76.3333 |
| 6-166 |   81.6667 |
+-------+-------------+

-- 查詢 score 表
SELECT degree FROM score;
+--------+
| degree |
+--------+
|   90 |
|   91 |
|   92 |
|   86 |
|   85 |
|   89 |
|   88 |
|   75 |
|   79 |
|   76 |
|   68 |
|   81 |
+--------+

-- 將表 b 作用於表 a 中查詢資料
-- score a (b): 將表宣告為 a (b),
-- 如此就能用 a.c_no = b.c_no 作為條件執行查詢了。
SELECT * FROM score a WHERE degree < (
  (SELECT AVG(degree) FROM score b WHERE a.c_no = b.c_no)
);
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 105 | 3-245 |   75 |
| 105 | 6-166 |   79 |
| 109 | 3-105 |   76 |
| 109 | 3-245 |   68 |
| 109 | 6-166 |   81 |
+------+-------+--------+

子查詢 - 4

查詢所有任課 ( 在 course 表裡有課程 ) 教師的 namedepartment

SELECT name,department FROM teacher WHERE no IN (SELECT t_no FROM course);
+--------+-----------------+
| name  | department   |
+--------+-----------------+
| 李誠  | 計算機系    |
| 王萍  | 計算機系    |
| 劉冰  | 電子工程系   |
| 張旭  | 電子工程系   |
+--------+-----------------+

條件加組篩選

查詢 student 表中至少有 2 名男生的 class

-- 檢視學生表資訊
SELECT * FROM student;
+-----+-----------+-----+------------+-------+
| no | name   | sex | birthday  | class |
+-----+-----------+-----+------------+-------+
| 101 | 曾華   | 男 | 1977-09-01 | 95033 |
| 102 | 匡明   | 男 | 1975-10-02 | 95031 |
| 103 | 王麗   | 女 | 1976-01-23 | 95033 |
| 104 | 李軍   | 男 | 1976-02-20 | 95033 |
| 105 | 王芳   | 女 | 1975-02-10 | 95031 |
| 106 | 陸軍   | 男 | 1974-06-03 | 95031 |
| 107 | 王尼瑪  | 男 | 1976-02-20 | 95033 |
| 108 | 張全蛋  | 男 | 1975-02-10 | 95031 |
| 109 | 趙鐵柱  | 男 | 1974-06-03 | 95031 |
| 110 | 張飛   | 男 | 1974-06-03 | 95038 |
+-----+-----------+-----+------------+-------+

-- 只查詢性別為男,然後按 class 分組,並限制 class 行大於 1。
SELECT class FROM student WHERE sex = '男' GROUP BY class HAVING COUNT(*) > 1;
+-------+
| class |
+-------+
| 95033 |
| 95031 |
+-------+

NOTLIKE 模糊查詢取反

查詢 student 表中不姓 "王" 的同學記錄。

-- NOT: 取反
-- LIKE: 模糊查詢
mysql> SELECT * FROM student WHERE name NOT LIKE '王%';
+-----+-----------+-----+------------+-------+
| no | name   | sex | birthday  | class |
+-----+-----------+-----+------------+-------+
| 101 | 曾華   | 男 | 1977-09-01 | 95033 |
| 102 | 匡明   | 男 | 1975-10-02 | 95031 |
| 104 | 李軍   | 男 | 1976-02-20 | 95033 |
| 106 | 陸軍   | 男 | 1974-06-03 | 95031 |
| 108 | 張全蛋  | 男 | 1975-02-10 | 95031 |
| 109 | 趙鐵柱  | 男 | 1974-06-03 | 95031 |
| 110 | 張飛   | 男 | 1974-06-03 | 95038 |
+-----+-----------+-----+------------+-------+

YEAR 與 NOW 函式

查詢 student 表中每個學生的姓名和年齡。

-- 使用函式 YEAR(NOW()) 計算出當前年份,減去出生年份後得出年齡。
SELECT name,YEAR(NOW()) - YEAR(birthday) as age FROM student;
+-----------+------+
| name   | age |
+-----------+------+
| 曾華   |  42 |
| 匡明   |  44 |
| 王麗   |  43 |
| 李軍   |  43 |
| 王芳   |  44 |
| 陸軍   |  45 |
| 王尼瑪  |  43 |
| 張全蛋  |  44 |
| 趙鐵柱  |  45 |
| 張飛   |  45 |
+-----------+------+

MAX 與 MIN 函式

查詢 student 表中最大和最小的 birthday 值。

SELECT MAX(birthday),MIN(birthday) FROM student;
+---------------+---------------+
| MAX(birthday) | MIN(birthday) |
+---------------+---------------+
| 1977-09-01  | 1974-06-03  |
+---------------+---------------+

多段排序

classbirthday 從大到小的順序查詢 student 表。

SELECT * FROM student ORDER BY class DESC,birthday;
+-----+-----------+-----+------------+-------+
| no | name   | sex | birthday  | class |
+-----+-----------+-----+------------+-------+
| 110 | 張飛   | 男 | 1974-06-03 | 95038 |
| 103 | 王麗   | 女 | 1976-01-23 | 95033 |
| 104 | 李軍   | 男 | 1976-02-20 | 95033 |
| 107 | 王尼瑪  | 男 | 1976-02-20 | 95033 |
| 101 | 曾華   | 男 | 1977-09-01 | 95033 |
| 106 | 陸軍   | 男 | 1974-06-03 | 95031 |
| 109 | 趙鐵柱  | 男 | 1974-06-03 | 95031 |
| 105 | 王芳   | 女 | 1975-02-10 | 95031 |
| 108 | 張全蛋  | 男 | 1975-02-10 | 95031 |
| 102 | 匡明   | 男 | 1975-10-02 | 95031 |
+-----+-----------+-----+------------+-------+

子查詢 - 5

查詢 "男" 教師及其所上的課程。

SELECT * FROM course WHERE t_no in (SELECT no FROM teacher WHERE sex = '男');
+-------+--------------+------+
| no  | name     | t_no |
+-------+--------------+------+
| 3-245 | 作業系統   | 804 |
| 6-166 | 數位電路   | 856 |
+-------+--------------+------+

MAX 函式與子查詢

查詢最高分同學的 score 表。

-- 找出最高成績(該查詢只能有一個結果)
SELECT MAX(degree) FROM score;

-- 根據上面的條件篩選出所有最高成績表,
-- 該查詢可能有多個結果,假設 degree 值多次符合條件。
SELECT * FROM score WHERE degree = (SELECT MAX(degree) FROM score);
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 |   92 |
+------+-------+--------+

子查詢 - 6

查詢和 "李軍" 同性別的所有同學 name

-- 首先將李軍的性別作為條件取出來
SELECT sex FROM student WHERE name = '李軍';
+-----+
| sex |
+-----+
| 男 |
+-----+

-- 根據性別查詢 name 和 sex
SELECT name,sex FROM student WHERE sex = (
  SELECT sex FROM student WHERE name = '李軍'
);
+-----------+-----+
| name   | sex |
+-----------+-----+
| 曾華   | 男 |
| 匡明   | 男 |
| 李軍   | 男 |
| 陸軍   | 男 |
| 王尼瑪  | 男 |
| 張全蛋  | 男 |
| 趙鐵柱  | 男 |
| 張飛   | 男 |
+-----------+-----+

子查詢 - 7

查詢和 "李軍" 同性別且同班的同學 name

SELECT name,class FROM student WHERE sex = (
  SELECT sex FROM student WHERE name = '李軍'
) AND class = (
  SELECT class FROM student WHERE name = '李軍'
);
+-----------+-----+-------+
| name   | sex | class |
+-----------+-----+-------+
| 曾華   | 男 | 95033 |
| 李軍   | 男 | 95033 |
| 王尼瑪  | 男 | 95033 |
+-----------+-----+-------+

子查詢 - 8

查詢所有選修 "計算機導論" 課程的 "男" 同學成績表。

需要的 "計算機導論" 和性別為 "男" 的編號可以在 coursestudent 表中找到。

SELECT * FROM score WHERE c_no = (
  SELECT no FROM course WHERE name = '計算機導論'
) AND s_no IN (
  SELECT no FROM student WHERE sex = '男'
);
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 101 | 3-105 |   90 |
| 102 | 3-105 |   91 |
| 104 | 3-105 |   89 |
| 109 | 3-105 |   76 |
+------+-------+--------+

按等級查詢

建立一個 grade 表代表學生的成績等級,並插入資料:

CREATE TABLE grade (
  low INT(3),upp INT(3),grade char(1)
);

INSERT INTO grade VALUES (90,100,'A');
INSERT INTO grade VALUES (80,89,'B');
INSERT INTO grade VALUES (70,79,'C');
INSERT INTO grade VALUES (60,69,'D');
INSERT INTO grade VALUES (0,59,'E');

SELECT * FROM grade;
+------+------+-------+
| low | upp | grade |
+------+------+-------+
|  90 | 100 | A   |
|  80 |  89 | B   |
|  70 |  79 | C   |
|  60 |  69 | D   |
|  0 |  59 | E   |
+------+------+-------+

查詢所有學生的 s_noc_nograde 列。

思路是,使用區間 ( BETWEEN ) 查詢,判斷學生的成績 ( degree ) 在 grade 表的 lowupp 之間。

SELECT s_no,grade FROM score,grade
WHERE degree BETWEEN low AND upp;
+------+-------+-------+
| s_no | c_no | grade |
+------+-------+-------+
| 101 | 3-105 | A   |
| 102 | 3-105 | A   |
| 103 | 3-105 | A   |
| 103 | 3-245 | B   |
| 103 | 6-166 | B   |
| 104 | 3-105 | B   |
| 105 | 3-105 | B   |
| 105 | 3-245 | C   |
| 105 | 6-166 | C   |
| 109 | 3-105 | C   |
| 109 | 3-245 | D   |
| 109 | 6-166 | B   |
+------+-------+-------+

連線查詢

準備用於測試連線查詢的資料:

CREATE DATABASE testJoin;

CREATE TABLE person (
  id INT,cardId INT
);

CREATE TABLE card (
  id INT,name VARCHAR(20)
);

INSERT INTO card VALUES (1,'飯卡'),(2,'建行卡'),(3,'農行卡'),(4,'工商卡'),(5,'郵政卡');
SELECT * FROM card;
+------+-----------+
| id  | name   |
+------+-----------+
|  1 | 飯卡   |
|  2 | 建行卡  |
|  3 | 農行卡  |
|  4 | 工商卡  |
|  5 | 郵政卡  |
+------+-----------+

INSERT INTO person VALUES (1,'張三',1),'李四',3),'王五',6);
SELECT * FROM person;
+------+--------+--------+
| id  | name  | cardId |
+------+--------+--------+
|  1 | 張三  |   1 |
|  2 | 李四  |   3 |
|  3 | 王五  |   6 |
+------+--------+--------+

分析兩張表發現,person 表並沒有為 cardId 欄位設定一個在 card 表中對應的 id 外來鍵。如果設定了的話,personcardId 欄位值為 6 的行就插不進去,因為該 cardId 值在 card 表中並沒有。

內連線

要查詢這兩張表中有關係的資料,可以使用 INNER JOIN ( 內連線 ) 將它們連線在一起。

-- INNER JOIN: 表示為內連線,將兩張表拼接在一起。
-- on: 表示要執行某個條件。
SELECT * FROM person INNER JOIN card on person.cardId = card.id;
+------+--------+--------+------+-----------+
| id  | name  | cardId | id  | name   |
+------+--------+--------+------+-----------+
|  1 | 張三  |   1 |  1 | 飯卡   |
|  2 | 李四  |   3 |  3 | 農行卡  |
+------+--------+--------+------+-----------+

-- 將 INNER 關鍵字省略掉,結果也是一樣的。
-- SELECT * FROM person JOIN card on person.cardId = card.id;
注意:card 的整張表被連線到了右邊。

左外連線

完整顯示左邊的表 ( person ) ,右邊的表如果符合條件就顯示,不符合則補 NULL

-- LEFT JOIN 也叫做 LEFT OUTER JOIN,用這兩種方式的查詢結果是一樣的。
SELECT * FROM person LEFT JOIN card on person.cardId = card.id;
+------+--------+--------+------+-----------+
| id  | name  | cardId | id  | name   |
+------+--------+--------+------+-----------+
|  1 | 張三  |   1 |  1 | 飯卡   |
|  2 | 李四  |   3 |  3 | 農行卡  |
|  3 | 王五  |   6 | NULL | NULL   |
+------+--------+--------+------+-----------+

右外連結

完整顯示右邊的表 ( card ) ,左邊的表如果符合條件就顯示,不符合則補 NULL

SELECT * FROM person RIGHT JOIN card on person.cardId = card.id;
+------+--------+--------+------+-----------+
| id  | name  | cardId | id  | name   |
+------+--------+--------+------+-----------+
|  1 | 張三  |   1 |  1 | 飯卡   |
|  2 | 李四  |   3 |  3 | 農行卡  |
| NULL | NULL  |  NULL |  2 | 建行卡  |
| NULL | NULL  |  NULL |  4 | 工商卡  |
| NULL | NULL  |  NULL |  5 | 郵政卡  |
+------+--------+--------+------+-----------+

全外連結

完整顯示兩張表的全部資料。

-- MySQL 不支援這種語法的全外連線
-- SELECT * FROM person FULL JOIN card on person.cardId = card.id;
-- 出現錯誤:
-- ERROR 1054 (42S22): Unknown column 'person.cardId' in 'on clause'

-- MySQL全連線語法,使用 UNION 將兩張表合併在一起。
SELECT * FROM person LEFT JOIN card on person.cardId = card.id
UNION
SELECT * FROM person RIGHT JOIN card on person.cardId = card.id;
+------+--------+--------+------+-----------+
| id  | name  | cardId | id  | name   |
+------+--------+--------+------+-----------+
|  1 | 張三  |   1 |  1 | 飯卡   |
|  2 | 李四  |   3 |  3 | 農行卡  |
|  3 | 王五  |   6 | NULL | NULL   |
| NULL | NULL  |  NULL |  2 | 建行卡  |
| NULL | NULL  |  NULL |  4 | 工商卡  |
| NULL | NULL  |  NULL |  5 | 郵政卡  |
+------+--------+--------+------+-----------+

事務

在 MySQL 中,事務其實是一個最小的不可分割的工作單元。事務能夠保證一個業務的完整性

比如我們的銀行轉賬:

-- a -> -100
UPDATE user set money = money - 100 WHERE name = 'a';

-- b -> +100
UPDATE user set money = money + 100 WHERE name = 'b';

在實際專案中,假設只有一條 SQL 語句執行成功,而另外一條執行失敗了,就會出現資料前後不一致。

因此,在執行多條有關聯 SQL 語句時,事務可能會要求這些 SQL 語句要麼同時執行成功,要麼就都執行失敗。

如何控制事務 - COMMIT / ROLLBACK

在 MySQL 中,事務的自動提交狀態預設是開啟的。

-- 查詢事務的自動提交狀態
SELECT @@AUTOCOMMIT;
+--------------+
| @@AUTOCOMMIT |
+--------------+
|      1 |
+--------------+

自動提交的作用:當我們執行一條 SQL 語句的時候,其產生的效果就會立即體現出來,且不能回滾

什麼是回滾?舉個例子:

CREATE DATABASE bank;

USE bank;

CREATE TABLE user (
  id INT PRIMARY KEY,money INT
);

INSERT INTO user VALUES (1,'a',1000);

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  | 1000 |
+----+------+-------+

可以看到,在執行插入語句後資料立刻生效,原因是 MySQL 中的事務自動將它提交到了資料庫中。那麼所謂回滾的意思就是,撤銷執行過的所有 SQL 語句,使其回滾到最後一次提交資料時的狀態。

在 MySQL 中使用 ROLLBACK 執行回滾:

-- 回滾到最後一次提交
ROLLBACK;

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  | 1000 |
+----+------+-------+

由於所有執行過的 SQL 語句都已經被提交過了,所以資料並沒有發生回滾。那如何讓資料可以發生回滾?

-- 關閉自動提交
SET AUTOCOMMIT = 0;

-- 查詢自動提交狀態
SELECT @@AUTOCOMMIT;
+--------------+
| @@AUTOCOMMIT |
+--------------+
|      0 |
+--------------+

將自動提交關閉後,測試資料回滾:

INSERT INTO user VALUES (2,'b',1000);

-- 關閉 AUTOCOMMIT 後,資料的變化是在一張虛擬的臨時資料表中展示,
-- 發生變化的資料並沒有真正插入到資料表中。
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  | 1000 |
| 2 | b  | 1000 |
+----+------+-------+

-- 資料表中的真實資料其實還是:
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  | 1000 |
+----+------+-------+

-- 由於資料還沒有真正提交,可以使用回滾
ROLLBACK;

-- 再次查詢
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  | 1000 |
+----+------+-------+

那如何將虛擬的資料真正提交到資料庫中?使用 COMMIT :

INSERT INTO user VALUES (2,1000);
-- 手動提交資料(永續性),
-- 將資料真正提交到資料庫中,執行後不能再回滾提交過的資料。
COMMIT;

-- 提交後測試回滾
ROLLBACK;

-- 再次查詢(回滾無效了)
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  | 1000 |
| 2 | b  | 1000 |
+----+------+-------+

總結

  1. 自動提交

    • 檢視自動提交狀態:SELECT @@AUTOCOMMIT
    • 設定自動提交狀態:SET AUTOCOMMIT = 0
  2. 手動提交

    @@AUTOCOMMIT = 0 時,使用 COMMIT 命令提交事務。

  3. 事務回滾

    @@AUTOCOMMIT = 0 時,使用 ROLLBACK 命令回滾事務。

事務的實際應用,讓我們再回到銀行轉賬專案:

-- 轉賬
UPDATE user set money = money - 100 WHERE name = 'a';

-- 到賬
UPDATE user set money = money + 100 WHERE name = 'b';

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  |  900 |
| 2 | b  | 1100 |
+----+------+-------+

這時假設在轉賬時發生了意外,就可以使用 ROLLBACK 回滾到最後一次提交的狀態:

-- 假設轉賬發生了意外,需要回滾。
ROLLBACK;

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  | 1000 |
| 2 | b  | 1000 |
+----+------+-------+

這時我們又回到了發生意外之前的狀態,也就是說,事務給我們提供了一個可以反悔的機會。假設資料沒有發生意外,這時可以手動將資料真正提交到資料表中:COMMIT

手動開啟事務 - BEGIN / START TRANSACTION

事務的預設提交被開啟 ( @@AUTOCOMMIT = 1 ) 後,此時就不能使用事務回滾了。但是我們還可以手動開啟一個事務處理事件,使其可以發生回滾:

-- 使用 BEGIN 或者 START TRANSACTION 手動開啟一個事務
-- START TRANSACTION;
BEGIN;
UPDATE user set money = money - 100 WHERE name = 'a';
UPDATE user set money = money + 100 WHERE name = 'b';

-- 由於手動開啟的事務沒有開啟自動提交,
-- 此時發生變化的資料仍然是被儲存在一張臨時表中。
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  |  900 |
| 2 | b  | 1100 |
+----+------+-------+

-- 測試回滾
ROLLBACK;

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  | 1000 |
| 2 | b  | 1000 |
+----+------+-------+

仍然使用 COMMIT 提交資料,提交後無法再發生本次事務的回滾。

BEGIN;
UPDATE user set money = money - 100 WHERE name = 'a';
UPDATE user set money = money + 100 WHERE name = 'b';

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a  |  900 |
| 2 | b  | 1100 |
+----+------+-------+

-- 提交資料
COMMIT;

-- 測試回滾(無效,因為表的資料已經被提交)
ROLLBACK;

事務的 ACID 特徵與使用

事務的四大特徵:

  • A 原子性:事務是最小的單位,不可以再分割;
  • C 一致性:要求同一事務中的 SQL 語句,必須保證同時成功或者失敗;
  • I 隔離性:事務 1 和 事務 2 之間是具有隔離性的;
  • D 永續性:事務一旦結束 ( COMMIT ) ,就不可以再返回了 ( ROLLBACK ) 。

事務的隔離性

事務的隔離性可分為四種 ( 效能從低到高 )

  1. READ UNCOMMITTED ( 讀取未提交 )

    如果有多個事務,那麼任意事務都可以看見其他事務的未提交資料

  2. READ COMMITTED ( 讀取已提交 )

    只能讀取到其他事務已經提交的資料

  3. REPEATABLE READ ( 可被重複讀 )

    如果有多個連線都開啟了事務,那麼事務之間不能共享資料記錄,否則只能共享已提交的記錄。

  4. SERIALIZABLE ( 序列化 )

    所有的事務都會按照固定順序執行,執行完一個事務後再繼續執行下一個事務的寫入操作

檢視當前資料庫的預設隔離級別:

-- MySQL 8.x,GLOBAL 表示系統級別,不加表示會話級別。
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
SELECT @@TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| REPEATABLE-READ        | -- MySQL的預設隔離級別,可以重複讀。
+--------------------------------+

-- MySQL 5.x
SELECT @@GLOBAL.TX_ISOLATION;
SELECT @@TX_ISOLATION;

修改隔離級別:

-- 設定系統隔離級別,LEVEL 後面表示要設定的隔離級別 (READ UNCOMMITTED)。
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- 查詢系統隔離級別,發現已經被修改。
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| READ-UNCOMMITTED        |
+--------------------------------+

髒讀

測試 READ UNCOMMITTED ( 讀取未提交 ) 的隔離性:

INSERT INTO user VALUES (3,'小明',1000);
INSERT INTO user VALUES (4,'淘寶店',1000);

SELECT * FROM user;
+----+-----------+-------+
| id | name   | money |
+----+-----------+-------+
| 1 | a     |  900 |
| 2 | b     | 1100 |
| 3 | 小明   | 1000 |
| 4 | 淘寶店  | 1000 |
+----+-----------+-------+

-- 開啟一個事務操作資料
-- 假設小明在淘寶店買了一雙800塊錢的鞋子:
START TRANSACTION;
UPDATE user SET money = money - 800 WHERE name = '小明';
UPDATE user SET money = money + 800 WHERE name = '淘寶店';

-- 然後淘寶店在另一方查詢結果,發現錢已到賬。
SELECT * FROM user;
+----+-----------+-------+
| id | name   | money |
+----+-----------+-------+
| 1 | a     |  900 |
| 2 | b     | 1100 |
| 3 | 小明   |  200 |
| 4 | 淘寶店  | 1800 |
+----+-----------+-------+

由於小明的轉賬是在新開啟的事務上進行操作的,而該操作的結果是可以被其他事務(另一方的淘寶店)看見的,因此淘寶店的查詢結果是正確的,淘寶店確認到賬。但就在這時,如果小明在它所處的事務上又執行了 ROLLBACK 命令,會發生什麼?

-- 小明所處的事務
ROLLBACK;

-- 此時無論對方是誰,如果再去查詢結果就會發現:
SELECT * FROM user;
+----+-----------+-------+
| id | name   | money |
+----+-----------+-------+
| 1 | a     |  900 |
| 2 | b     | 1100 |
| 3 | 小明   | 1000 |
| 4 | 淘寶店  | 1000 |
+----+-----------+-------+

這就是所謂的髒讀,一個事務讀取到另外一個事務還未提交的資料。這在實際開發中是不允許出現的。

讀取已提交

把隔離級別設定為 READ COMMITTED

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| READ-COMMITTED         |
+--------------------------------+

這樣,再有新的事務連線進來時,它們就只能查詢到已經提交過的事務資料了。但是對於當前事務來說,它們看到的還是未提交的資料,例如:

-- 正在操作資料事務(當前事務)
START TRANSACTION;
UPDATE user SET money = money - 800 WHERE name = '小明';
UPDATE user SET money = money + 800 WHERE name = '淘寶店';

-- 雖然隔離級別被設定為了 READ COMMITTED,但在當前事務中,
-- 它看到的仍然是資料表中臨時改變資料,而不是真正提交過的資料。
SELECT * FROM user;
+----+-----------+-------+
| id | name   | money |
+----+-----------+-------+
| 1 | a     |  900 |
| 2 | b     | 1100 |
| 3 | 小明   |  200 |
| 4 | 淘寶店  | 1800 |
+----+-----------+-------+


-- 假設此時在遠端開啟了一個新事務,連線到資料庫。
$ mysql -u root -p12345612

-- 此時遠端連線查詢到的資料只能是已經提交過的
SELECT * FROM user;
+----+-----------+-------+
| id | name   | money |
+----+-----------+-------+
| 1 | a     |  900 |
| 2 | b     | 1100 |
| 3 | 小明   | 1000 |
| 4 | 淘寶店  | 1000 |
+----+-----------+-------+

但是這樣還有問題,那就是假設一個事務在操作資料時,其他事務干擾了這個事務的資料。例如:

-- 小張在查詢資料的時候發現:
SELECT * FROM user;
+----+-----------+-------+
| id | name   | money |
+----+-----------+-------+
| 1 | a     |  900 |
| 2 | b     | 1100 |
| 3 | 小明   |  200 |
| 4 | 淘寶店  | 1800 |
+----+-----------+-------+

-- 在小張求表的 money 平均值之前,小王做了一個操作:
START TRANSACTION;
INSERT INTO user VALUES (5,'c',100);
COMMIT;

-- 此時表的真實資料是:
SELECT * FROM user;
+----+-----------+-------+
| id | name   | money |
+----+-----------+-------+
| 1 | a     |  900 |
| 2 | b     | 1100 |
| 3 | 小明   | 1000 |
| 4 | 淘寶店  | 1000 |
| 5 | c     |  100 |
+----+-----------+-------+

-- 這時小張再求平均值的時候,就會出現計算不相符合的情況:
SELECT AVG(money) FROM user;
+------------+
| AVG(money) |
+------------+
| 820.0000 |
+------------+

雖然 READ COMMITTED 讓我們只能讀取到其他事務已經提交的資料,但還是會出現問題,就是在讀取同一個表的資料時,可能會發生前後不一致的情況。這被稱為不可重複讀現象 ( READ COMMITTED )

幻讀

將隔離級別設定為 REPEATABLE READ ( 可被重複讀取 ) :

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| REPEATABLE-READ        |
+--------------------------------+

測試 REPEATABLE READ ,假設在兩個不同的連線上分別執行 START TRANSACTION :

-- 小張 - 成都
START TRANSACTION;
INSERT INTO user VALUES (6,'d',1000);

-- 小王 - 北京
START TRANSACTION;

-- 小張 - 成都
COMMIT;

當前事務開啟後,沒提交之前,查詢不到,提交後可以被查詢到。但是,在提交之前其他事務被開啟了,那麼在這條事務線上,就不會查詢到當前有操作事務的連線。相當於開闢出一條單獨的執行緒。

無論小張是否執行過 COMMIT ,在小王這邊,都不會查詢到小張的事務記錄,而是隻會查詢到自己所處事務的記錄:

SELECT * FROM user;
+----+-----------+-------+
| id | name   | money |
+----+-----------+-------+
| 1 | a     |  900 |
| 2 | b     | 1100 |
| 3 | 小明   | 1000 |
| 4 | 淘寶店  | 1000 |
| 5 | c     |  100 |
+----+-----------+-------+

這是因為小王在此之前開啟了一個新的事務 ( START TRANSACTION ) ,那麼在他的這條新事務的線上,跟其他事務是沒有聯絡的,也就是說,此時如果其他事務正在操作資料,它是不知道的。

然而事實是,在真實的資料表中,小張已經插入了一條資料。但是小王此時並不知道,也插入了同一條資料,會發生什麼呢?

INSERT INTO user VALUES (6,1000);
-- ERROR 1062 (23000): Duplicate entry '6' for key 'PRIMARY'

報錯了,操作被告知已存在主鍵為 6 的欄位。這種現象也被稱為幻讀,一個事務提交的資料,不能被其他事務讀取到

序列化

顧名思義,就是所有事務的寫入操作全都是序列化的。什麼意思?把隔離級別修改成 SERIALIZABLE :

SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| SERIALIZABLE          |
+--------------------------------+

還是拿小張和小王來舉例:

-- 小張 - 成都
START TRANSACTION;

-- 小王 - 北京
START TRANSACTION;

-- 開啟事務之前先查詢表,準備操作資料。
SELECT * FROM user;
+----+-----------+-------+
| id | name   | money |
+----+-----------+-------+
| 1 | a     |  900 |
| 2 | b     | 1100 |
| 3 | 小明   | 1000 |
| 4 | 淘寶店  | 1000 |
| 5 | c     |  100 |
| 6 | d     | 1000 |
+----+-----------+-------+

-- 發現沒有 7 號王小花,於是插入一條資料:
INSERT INTO user VALUES (7,'王小花',1000);

此時會發生什麼呢?由於現在的隔離級別是 SERIALIZABLE ( 序列化 ) ,序列化的意思就是:假設把所有的事務都放在一個序列的佇列中,那麼所有的事務都會按照固定順序執行,執行完一個事務後再繼續執行下一個事務的寫入操作 ( 這意味著佇列中同時只能執行一個事務的寫入操作 ) 。

根據這個解釋,小王在插入資料時,會出現等待狀態,直到小張執行 COMMIT 結束它所處的事務,或者出現等待超時。

更多關於MySQL相關內容感興趣的讀者可檢視本站專題:《MySQL查詢技巧大全》、《MySQL常用函式大彙總》、《MySQL日誌操作技巧大全》、《MySQL事務操作技巧彙總》、《MySQL儲存過程技巧大全》及《MySQL資料庫鎖相關技巧彙總》

希望本文所述對大家MySQL資料庫計有所幫助。