1. 程式人生 > >MySQL基礎(三)表關係及資料的增刪改查

MySQL基礎(三)表關係及資料的增刪改查

這一部分主要使用SQL中的DML,資料庫操作語言(data manipulation language),對資料庫資料進行增、刪、改、查操作,作為前提,先介紹關係型資料庫中的表關係。

關係型資料庫(RDBMS)

MySQL作為關係型資料庫,資料是儲存在表中的,假如設計一個訂單系統,可能包含以下資訊:

  • 供應商(Vendors)
  • 產品目錄(Products)
  • 客戶列表(Customers)
  • 訂單(Orders)
  • 可能還會為產品打上一些標籤(比如,新品,進口商品,本地特產等)

每個表中都有個特殊的列(欄位),稱為主鍵(Primary Key),其值用來唯一標識表中的一行資料(記錄)。表中還可以有稱為外來鍵(Foreign Key)的列,引用同一個表或不同表中某行的主鍵。因此,通過外來鍵,表與表之間是有關聯關係的,這也是建立關係型資料庫模型的基礎。

下面我們試著設計一下這個訂單系統的表結構,就拿其中的供應商(Vendors)和產品目錄(Products)來說:

供應商表(Vendors):

說明 主鍵/外來鍵
vendor_id 供應商唯一ID, PK
vend_name 供應商名字
vend_address 供應商地址

產品目錄表(Products):

說明 主鍵/外來鍵
prod_id 產品唯一ID PK
prod_name 產品名字
prod_price 產品價格
vendor_id 供應商ID(關聯Vendors表的vendor_id列) FK

Products表存放產品記錄,每個產品都有prod_name, prod_price,並且有唯一的prod_id作為主鍵。除此之外,還有一個外來鍵vendor_id,引用Vendors表中的vend_id,通過這種方式,為每個產品指定了供應商。我們不需要在產品表中再新增更多的列來儲存其供應商的具體資訊,以後直接根據這個外來鍵,去Vendors表中查詢即可。

從中我們可以看出,關係型資料庫在儲存資料時是很高效的,避免了資料冗餘。而且,關係型資料庫的可伸縮性遠比非關係型資料庫要好。比如,對資料庫中供應商名字,地址等資訊的修改,可以只更新Vendors表中的記錄,相關表中的記錄不用改動。

但是另一方方面,將資料分別存放在不同的表中有時是很複雜的,我們需要查詢多個關聯的表,才能拿到我們想要的資料。因此,在設計表結構,以及編寫SQL語句時,需要很好地理這種關聯關係,知道什麼資訊存在什麼表中。

表關係種類

一對多

比如上面的供應商和產品,一個供應商可能提供多個產品,而一個產品只能屬於一個供應商,在多的一邊(產品表)通過Foreign Key 描述這種關係。

一對一

比如,一張訂單對應一張訂單詳情;一篇文章摘要,對應一篇具體的文章內容。一對一關係的Foreign Key建在關係的哪邊都行。

多對多

這個是相對複雜的一種,但是也很好理解。還是上面的例子,比如給產品打標籤,一個產品可能有多個標籤(”進口“, ”新品“),而一個標籤下可以對應多個產品(進口商品可能有很多)。

特別要注意的是,如果要描述多對多關係,需要藉助第三張表,這個表稱為關係表,這張表實際儲存時,看起來是這樣的:

id (PK) prod_id (FK) tag_id (FK)
1 1 1
2 1 2
3 2 1
4 3 1

除了自增的主鍵id,它還有兩個外來鍵,一個prod_id關聯產品表,一個tag_id關聯標籤表。

插入資料(INSERT INTO)

這裡將以教學管理為例,來建立資料庫,建立表,插入資料。最後一部分的綜合練習,將基於接下來要建立的資料。

建立表並定義約束關係

首先,建立一個數據庫,就命名為school

CREATE DATABASE school CHARSET utf8;

下面我們我們將建立5張表,分別是:

  • 班級表(Class)
  • 課程表(Course)
  • 成績表(Score)
  • 學生表(Student)
  • 教師表(Teacher)

其中班級表和課程表是一對多關係;班級表和學生表是一對多關係;教師表和課程表是一對多關係;學生表和課程表是多對多關係。

-- -------------------------
-- 建立班級表
-- -------------------------
CREATE TABLE Class
(
  cls_id        int          NOT NULL PRIMARY KEY AUTO_INCREMENT , -- PK
  caption       char(30)     NOT NULL 
);

-- -------------------------
-- 建立課程表
-- -------------------------
CREATE TABLE Course
(
  crs_id        int          NOT NULL PRIMARY KEY AUTO_INCREMENT , -- PK
  crs_name      char(30)     NOT NULL ,
  teacher_id    int          NOT NULL  -- FK 
);

-- -------------------------
-- 建立成績表
-- -------------------------
CREATE TABLE Score
(
  scr_id        int          NOT NULL PRIMARY KEY AUTO_INCREMENT ,  -- PK
  num           tinyint      NOT NULL ,
  student_id    int          NOT NULL ,  -- FK
  course_id     int          NOT NULL    -- FK
);

-- -------------------------
-- 建立學生表
-- -------------------------
CREATE TABLE Student
(
  std_id        int          NOT NULL PRIMARY KEY AUTO_INCREMENT , -- PK
  gender        enum('女', '男')   NOT NULL ,
  std_name      char(30)     NOT NULL ,
  class_id      int          NOT NULL  -- FK
);

-- -------------------------
-- 建立教師表
-- -------------------------
CREATE TABLE Teacher
(
  tea_id        int          NOT NULL PRIMARY KEY AUTO_INCREMENT , -- PK
  tea_name      char(30)     NOT NULL 
);

-- -------------------------
-- 定義外來鍵
-- -------------------------
ALTER TABLE Course
ADD CONSTRAINT FK_Course_Teacher
FOREIGN KEY (teacher_id) REFERENCES Teacher (tea_id);

ALTER TABLE Score
ADD CONSTRAINT FK_Score_Student
FOREIGN KEY (student_id) REFERENCES Student (std_id);
ALTER TABLE Score
ADD CONSTRAINT FK_Score_Course
FOREIGN KEY (course_id) REFERENCES Course (crs_id);

ALTER TABLE Student
ADD CONSTRAINT FK_Student_Class
FOREIGN KEY (class_id) REFERENCES Class (cls_id);

定義外來鍵的方式

注意:定義外來鍵的資料型別一定要和關聯表主鍵的資料型別一致!

建表時定義
CREATE TABLE Course
(
  crs_id        int          PRIMARY KEY AUTO_INCREMENT , 
  crs_name      char(30)     NOT NULL ,
  teacher_id    int          NOT NULL ,
  CONSTRAINT FK_Course_Teacher  -- 外來鍵名
  FOREIGN KEY (teacher_id)  -- 外來鍵欄位
  REFERENCES Teacher (tea_id)  -- 關聯表主鍵欄位
);
建表後定義
CREATE TABLE Course
(
  crs_id        int          PRIMARY KEY AUTO_INCREMENT , 
  crs_name      char(30)     NOT NULL ,
  teacher_id    int          NOT NULL 
);

ALTER TABLE Course
ADD CONSTRAINT FK_Course_Teacher  -- 外來鍵名
Foreign KEY (teacher_id) REFERENCES Teacher (tea_id);  -- 外來鍵欄位和關聯表主鍵欄位
刪除外來鍵

格式

ALTER TABLE 表名
DROP FOREIGN KEY 外來鍵名;

示例

ALTER TABLE Course
DROP FOREIGN KEY FK_Course_Teacher;

插入資料

插入單條資料

格式

INSERT INTO 表名 [(fld1, fld2, ..., fldn)] 
VALUES (val1, val2, ..., valn);

注意:表名後的欄位名可以省略,但建議加上,VALUES的值將依次匹配欄位名,而不一定非要按照表中欄位的實際次序;有預設值的欄位或自增欄位可不提供值。另外,對於自增id,如果插入資料時指定了非連續的值,比如表記錄最後一行id是3,插入資料時指定id是9,那麼以後再插入資料時,id將從9開始自增。

插入多條資料

格式

INSERT INTO 表名 [(fld1, fld2, ..., fldn)] 
VALUES (val1, val2, ..., valn),
       (val1, val2, ..., valn),
        -- ...
       (val1, val2, ..., valn);

示例

下面,我們將為上面建立的那5張表填充資料:

-- --------------------------
-- 插入班級資料
-- --------------------------
INSERT INTO Class (caption)
VALUES ('三年二班'), ('三年三班'), ('一年二班'), ('二年九班');

-- --------------------------
-- 插入教師資料
-- --------------------------
INSERT INTO Teacher (tea_name)
VALUES ('張磊老師'), ('李平老師'), ('劉海燕老師'), ('朱雲海老師'), ('李傑老師');

-- -------------------------
-- 插入課程資料
-- -------------------------
INSERT INTO Course (crs_name, teacher_id)
VALUES ('生物', 1), ('物理', 2), ('體育', 3), ('美術', 2);

-- -------------------------
-- 插入學生資料
-- -------------------------
INSERT INTO Student (gender, std_name, class_id)
VALUES ('男', '理解', 1), ('女', '鋼蛋', 1), ('男', '張三', 1), ('男', '張一', 1),
       ('女', '張二', 1), ('男', '張四', 1), ('女', '鐵錘', 2), ('男', '李三', 2), 
       ('男', '李一', 2), ('女', '李二', 2), ('男', '李四', 2), ('女', '如花', 3),
       ('男', '劉三', 3), ('男', '劉一', 3), ('女', '劉二', 3), ('男', '劉四', 3);

-- --------------------------
-- 插入成績資料
-- --------------------------
INSERT INTO Score (num, student_id, course_id)
VALUES (10,1,1),(9,1,2),(66,1,4),(8,2,1),(68,2,3),(99,2,4),(77,3,1),(66,3,2),
       (87,3,3),(99,3,4),(79,4,1),(11,4,2),(67,4,3),(100,4,4),(79,5,1),(11,5,2),
       (67,5,3),(100,5,4),(9,6,1),(100,6,2),(67,6,3),(100,6,4),(9,7,1),(100,7,2),
       (67,7,3),(88,7,4),(9,8,1),(100,8,2),(67,8,3),(88,8,4),(91,9,1),(88,9,2),
       (67,9,3),(22,9,4),(90,10,1),(77,10,2),(43,10,3),(87,10,4),(90,11,1),(77,11,2),
       (43,11,3),(87,11,4),(90,12,1),(77,12,2),(43,12,3),(87,12,4),(87,13,3);

修改和刪除資料

修改資料(UPDATE)

格式:

UPDATE 表名
SET fld1=val1, fld2=val2, ... -- 賦值,多個欄位之間以逗號分隔
WHERE 條件;  -- 過濾條件

注意:如果沒有WHERE子句,將更新所有的行!

示例:

-- 將學號為2,課程id為1 的成績加10分
UPDATE score
SET num = num + 10
WHERE student_id=2 and course_id=1;

刪除資料

DELETE

DELETE FROM 表名
WHERE 條件;  -- 過濾條件

刪除記錄:先查出所有符合條件的記錄,然後一條條刪除,資料很多比較慢。如果有自增id,再插入新資料時,id會在原有的基礎上自增。

TRUNCATE

如果需要刪除整張表的記錄,那麼使用TRUNCATE速度更快,它是直接刪除表,然後新建一張帶原來欄位的表。

TRUNCATE TABLE 表名;

單表查詢(SELECT)

基本格式:

SELECT fld, ...    -- 選擇欄位
FROM tablename     -- 選擇表
[WHERE ...]        -- WHERE 條件
[GROUP BY ...]     -- 分組
[HAVING ...]       -- HAVING 條件
[ORDER BY ...]     -- 排序
LIMIT ... ;        -- 限制條數

WHERE子句

資料庫表一般包含大量的資料,很少需要檢索表中的所有行,通常會根據需要指定過濾條件。WHERE子句就是用來指定過濾條件的。

WHERE子句操作符

操作符 說明 操作符 說明
= 等於 LIKE ‘_…%’ 模糊搜尋
!= 不等於 _ 萬用字元:匹配一個字元
> 大於 % 萬用字元:匹配任意字元任意個數
>= 大於等於 AND 邏輯與
< 小於 NOT 邏輯非
<= 小於等於 OR 邏輯或
BETWEEN x AND y 在x與y之間(左右包含) REGEXP 正則匹配
IN (val1, val2, …) 指定條件範圍

AS 顯示別名

更改列名顯示

格式:

SELECT fieldname AS fieldname_new FROM tablename;

示例:

SELECT tea_id as ID, tea_name as 教師姓名 FROM Teacher;

+----+------------+
| ID | 教師姓名   |
+----+------------+
|  1 | 張磊老師   |
|  2 | 李平老師   |
|  3 | 劉海燕老師 |
|  4 | 朱雲海老師 |
|  5 | 李傑老師   |
+----+------------+
5 rows in set (0.00 sec)

-- 也可以省略AS
SELECT tea_id ID, tea_name 教師姓名 FROM Teacher;

列值相加

假如有學生各科成績表,使用別名表示學生總分,使用+將列值相加

SELECT name as 姓名, math + chinese + english as 總成績 FROM ExamResult;  -- 表不存在,這裡只是示例

ORDER BY 排序

根據指定列排序,ASC升序,DESC降序

-- 查詢成績表中課程id為2的分數和學生,並以分數倒序顯示
SELECT num AS 分數, student_id AS 學生ID FROM Score
WHERE course_id = 2
ORDER BY num DESC;

+------+--------+
| 分數 | 學生ID |
+------+--------+
|  100 |      6 |
|  100 |      7 |
|  100 |      8 |
|   88 |      9 |
-- ......
+------+--------+
11 rows in set (0.14 sec)

LIMIT 限制查詢條數

-- 查詢學生表中3個姓張的學生
SELECT std_id, gender, std_name
FROM Student
WHERE std_name LIKE '張%'
LIMIT 3;

+--------+--------+----------+
| std_id | gender | std_name |
+--------+--------+----------+
|      3 | 男     | 張三     |
|      4 | 男     | 張一     |
|      5 | 女     | 張二     |
+--------+--------+----------+
3 rows in set (0.28 sec)

使用LIMIT 可以提高資料庫效能。對於上面的這個模糊搜尋,即使已經找到了,也將遍歷整個資料庫,如果資料庫中儲存了海量資料,這種全表掃描將極大影響效能。使用LIMIT後,找到指定條數的記錄後,就會停止查詢。

LIMIT [offset,] rows其實可以接受兩個引數,第一引數是偏移。預設不寫是偏移0。在最後綜合練習的第20題,你將看到它的應用。

聚合函式

聚合函式用來彙總資料:

函式 說明
AVG() 返回某列的平均值
COUNT() 返回某列的行數
MAX() 返回某列的最大值
MIN() 返回某列的最小值
SUM() 返回某列子和

AVG

-- 從成績表中,查詢學生id是3的學生的平均成績
SELECT AVG(num) AS avg_num
FROM Score
WHERE student_id=3;

+---------+
| avg_num |
+---------+
| 82.2500 |
+---------+

COUNT

該函式有兩種使用方式:

  • COUNT(*) 對錶中行的數目進行計數
  • COUNT(column) 對特定列中具有值的行進行計數(會忽略列值為NULL的行)
-- 查詢學生總數
SELECT COUNT(*) AS total_num FROM Student;

+-----------+
| total_num |
+-----------+
|        16 |
+-----------+

MAX MIN

  • MAX()一般用來找出最大的數值或日期,對於文字資料,將返回該列排序後的最後一行
  • 如果SELECT選擇多列,對其中一列使用MAX()統計最大值,比如下面這個例子,查詢出來的某列具有最大值的行和其它列所在的行,並不是同一行
-- 統計課程3的最高成績
SELECT student_id, MAX(num)
FROM Score
WHERE course_id=3;

+------------+----------+
| student_id | MAX(num) |
+------------+----------+
|          2 |       87 |
+------------+----------+


-- 事實上,課程3最高成績對應的學生id是3
SELECT student_id, num
FROM Score
WHERE course_id=3
ORDER BY num DESC
LIMIT 3;

+------------+-----+
| student_id | num |
+------------+-----+
|          3 |  87 |
|         13 |  87 |
|          2 |  68 |
+------------+-----+

-- 第一次查詢結果中的2,只是第一次出現課程3的那一行的student_id
SELECT student_id, num
FROM Score
WHERE course_id=3
LIMIT 3;

+------------+-----+
| student_id | num |
+------------+-----+
|          2 |  68 |
|          3 |  87 |
|          4 |  67 |
+------------+-----+

MIN()函式同理,不贅述。

求最大值和最小值,有時候通過DESC ASC排序,配合LIMIT更方便,比如最後綜合練習中的19題。

SUM

-- 計算訂單號為20005的訂單總金額
SELECT SUM(item_price*quantity) AS total_price
FROM OrderItems
WHERE order_num = 20005;

注意:

如上所示,利用標準的算術操作符,所有的聚集函式都可以用來執行多個列上的計算

ALL & DISTINCT
  • ALL 對所有行執行計算,是預設行為,不需要指定
  • DISTINCT 只對包含不同列值的行進行計算(因此聚合函式必須指定列名),需要顯示地指定該引數

以查詢平均成績為例,二者的結果是不一樣的,DISTINCT引數會過濾掉重複的成績再計算

SELECT AVG(num) FROM Score;  -- 預設行為是 ALL

+----------+
| AVG(num) |
+----------+
|  67.2979 |
+----------+

SELECT AVG(DISTINCT num) FROM Score;  

+-------------------+
| AVG(DISTINCT num) |
+-------------------+
|           60.2941 |
+-------------------+

在最後綜合練習的第11題中,你也將看到相關應用。

組合聚合函式

SELECT語句可以根據需要包含多個聚集函式,比如:

SELECT COUNT(*) as num_items,
       MIN(num) as score_min,
       MAX(num) AS score_max,
       AVG(num) AS score_avg
FROM Score;

+-----------+-----------+-----------+-----------+
| num_items | score_min | score_max | score_avg |
+-----------+-----------+-----------+-----------+
|        47 |         9 |       100 |   67.2979 |
+-----------+-----------+-----------+-----------+

其它內建函式

文字處理函式

函式 說明
UPPER() 大寫
LOW() 小寫
LENGTH() 返回字串長度(Bytes)
SOUNDEX() 返回字串的SOUNDEX值
CONCAT() 拼接欄位/字串
LENGTH()

長度是位元組(在utf8編碼中,一箇中文字元佔3個位元組)

SOUNDEX()

將任何文字字串轉化為描述其語言表示的字母數字模式的演算法,SOUNDEX 考慮了類似的發音字元和音節,使得能夠對字串進行發音比較,而不是字母比較。(看這個介紹就知道,不支援中文,實測確實如此,很遺憾…)這裡給出一個例子:

假如有一張顧客表,其聯絡名為Michael Green,但如果記錯了,輸入了 Michelle Green 來查詢,肯定是找不到的。如果用SOUNDEX()函式進行搜尋,就可以解決這個問題,它匹配所有發音類似於Michael Green的聯絡名

SELECT cust_name, cust_contact
FROM Customers
WHERE SOUNDEX(cust_contact) = SOUNDEX('Michael Green');
CONCAT()

MySQL使用CONCAT函式來拼接欄位和字串(其它DBMS中可能使用 +||

-- 將tea_id欄位與tea_name欄位聯合顯示
SELECT CONCAT('(', tea_id, ')', tea_name) AS '(ID)姓名'
FROM Teacher;

+---------------+
| (ID)姓名      |
+---------------+
| (1)張磊老師   |
| (2)李平老師   |
| (3)劉海燕老師 |
| (4)朱雲海老師 |
| (5)李傑老師   |
+---------------+

日期和時間處理函式

這一塊兒的函式比較多,要用的時候可以根據需求,去查詢文件

GROUP BY分組和 HAVING過濾

GROUP BY可以將資料分為多個邏輯組,再對每個組進行聚合計算

注意區別WHEREHAVING

  • WHERE過濾行,HAVING過濾分組
  • WHERE在資料分組前進行過濾, HAVING在資料分組後進行過濾,結合GROUP BY子句使用
-- 具有兩個以上的產品,且產品價格大於4的供應商
SELECT vend_id, COUNT(*) AS num_prods
FROM Products
WHERE prod_price >= 4   -- 產品價格大於4
GROUP BY vend_id        -- 根據供應商分組
HAVING COUNT(*) >= 2    -- 具有兩個以上的產品


-- 從成績表中,查詢每門課的平均成績
SELECT AVG(num) AS 'score_avg', course_id
FROM Score
GROUP BY course_id;

+-----------+-----------+
| score_avg | course_id |
+-----------+-----------+
|   54.2500 |         1 |
|   65.0909 |         2 |
|   64.4167 |         3 |
|   85.2500 |         4 |
+-----------+-----------+
4 rows in set (0.00 sec)

InnoDb 外來鍵約束支援的ON語句

這裡將建立外來鍵的表稱為子表,外來鍵引用的表稱為父表(主表)。

外來鍵約束是雙重的:

  • 對子表來說,如果引用的記錄在父表中找不到,那麼不允許在子表中插入或更新資料
  • 對父表來說,如果某記錄被子表引用,那麼該記錄不能被隨便刪除(具體取決於子表在定義外來鍵約束時的ON語句)

外來鍵約束的四種ON語句方式:

CASCADE

級聯刪除:如果父表的記錄被刪除,那麼子表中對應的記錄也被自動刪除

FOREIGN KEY (xx_id) REFERENCES parent_tablename (id)
ON DELETE CASCADE;

SET NULL

刪除父表中某條記錄時,將子表中引用了該記錄的外來鍵設為NULL

FOREIGN KEY (xx_id) REFERENCES parent_tablename (id)
ON DELETE SET NULL;

RESTRICT

如果子表中引用了父表記錄,則不允許對父表中的該記錄進行刪除。

NO ACTION

在MySQL中同RESTRICT。

組合查詢(UNION)

UNION

通過UNION操作符,可以執行多個SELECT查詢,並將結果作為一個查詢結果返回,這就是組合查詢(也稱並查詢,複合查詢 compound query)。

UNION使用場景:

  • 在一個查詢中從不同的表返回結果
  • 對一個表執行多個查詢,按一個查詢返回結果

使用形式:

SELECT column1, column2, ...
FROM tablename
WHERE ...
UNION           -- 使用UNION連線多個查詢
SELECT column1, column2, ...
FROM tablename
WHERE ...

比如,從學生表中查詢性別為“男”,或者 姓”李“的學生:

SELECT std_id, std_name, gender FROM Student WHERE gender = '男'
UNION
SELECT std_id, std_name, gender FROM Student WHERE std_name LIKE '李%';
+--------+----------+--------+
| std_id | std_name | gender |
+--------+----------+--------+
|      1 | 理解     | 男     |
|      3 | 張三     | 男     |
|      4 | 張一     | 男     |
|      6 | 張四     | 男     |
|      8 | 李三     | 男     |
|      9 | 李一     | 男     |
|     11 | 李四     | 男     |
|     13 | 劉三     | 男     |
|     14 | 劉一     | 男     |
|     16 | 劉四     | 男     |
|     10 | 李二     | 女     |
+--------+----------+--------+

當然以上要求也可以用WHERE子句來做,可能還更簡潔,用OR連線兩個條件即可。事實上,UNION和WHERE常常是可以互換的,但是對於較為複雜的過濾條件,或者從多個表中檢索資料時,使用UNION可能會更簡單。

可以看到UNION非常容易使用,但在進行組合時,要注意幾條規則:

  • UNION必須由兩條或以上的SELECT語句組成,中間用UNION連線
  • UNION中的每個查詢必須包含相同的列,表示式或聚合函式(次序可以不同)
  • 列資料型別必須相容

UNION ALL

預設UNION會從查詢結果中取出重複的行,如果不希望這麼做,可以使用UNION ALL

對組合查詢排序

在組合查詢中,如果要對查詢結果排序,只能使用一條ORDER BY子句,且必須位於最後一條SELECT語句之後。

多表查詢(SELECT)

內聯結(INNER JOIN)

多表查詢需要利用聯結,使多個表返回一組輸出。聯結是SQL中最重要,最強大的特性!

最基本的方式是,在查詢時,指定要聯結的所有表,以及關聯它們的方式即可。

基本格式:

SELECT column1, column2, ...    -- 選擇位於不同表中要檢索的列
FROM tablename1, tablename2     -- 要聯結的表
WHERE ...                       -- 指定聯結條件,非常重要!!!

我們看一個例子:

SELECT crs_name, tea_name
FROM Course, Teacher
WHERE Course.teacher_id = Teacher.tea_id;  -- 完全限定列名

+----------+------------+
| crs_name | tea_name   |
+----------+------------+
| 生物     | 張磊老師   |
| 物理     | 李平老師   |
| 體育     | 劉海燕老師 |
| 美術     | 李平老師   |
+----------+------------+

完全限定列名:用一個句點分隔表名和列名,以避免列名混淆。

笛卡兒積

如果沒有WHERE子句指定聯結條件,第一個表中的每一行將與第二個表中的每一行配對,而不管它們邏輯上是否能匹配在一起。這樣返回的結果稱為笛卡兒積,檢索出的行的數目等於兩個錶行數的乘積。

內聯接

上面這種聯結方式稱為等值聯結(equijoin),也叫內聯接(inner join),它基於兩個表之間的相等測試。

我們也可以使用另一種的語法,明確指定聯結的型別:

SELECT column1, column2, ...              -- 選擇位於不同表中要檢索的列
FROM tablename1 INNER JOIN tablename2     -- 要聯結的表
ON ...                                    -- ON指定聯結條件,非常重要!!!

下面的SELECT語句返回與上面例子完全相同的結果:

SELECT crs_name, tea_name
FROM Course INNER JOIN Teacher
ON Course.teacher_id = Teacher.tea_id;

在聯結中使用別名

之前的AS別名也可以用在聯結中,好處是:

  • 縮短SQL語句
  • 允許在一條SQL語句中多次使用相同的表(下面自聯結會用到)

我們將上面的例子用AS別名改寫下:

SELECT crs_name, tea_name
FROM Course AS c INNER JOIN Teacher AS t
ON c.teacher_id = t.tea_id;

這樣做還有一個好處就是,一旦聯結的多張表中出現了欄位名重複,可以通過別名.欄位名(完全限定列名)的方式加以區分:

SELECT a.name, b.name
FROM TableA AS a INNER JOIN TableB AS b
ON a.fk = b.pk;

自聯結(self-join)

自聯結(self-join),聯結的兩張表是相同的表,通過AS別名來區分。

在最後的綜合練習第8題,第21題,你將看到它的應用。

外聯結(outer-join)

假如有一個父表,和一個子表,子表通過外來鍵引用父表中的記錄。但並不是父表中的所有記錄都會被引用,比如顧客表(父表)和訂單表(子表),肯定會有沒有下單的顧客。如果在統計顧客的訂單數時,希望將沒有下單的顧客也包括進來,那麼用內聯結就不合適。這個時候就要用到外聯結。

左外聯結(LEFT JOIN)

在內聯結的基礎上,增加左邊有,右邊沒有的結果,沒有的部分顯示為Null。注意:左右指的是聯結條件的兩段。

使用左外連線來查詢所有顧客的訂單,包括那些沒下單的顧客時,可以這樣寫

SELECT Customers.cust_id, Orders.order_num
FROM Customers LEFT JOIN Orders
ON Customers.cust_id = Orders.cust_id;    -- 聯結條件

右外聯結(RIGHT JOIN)

在內聯結的基礎上,增加右邊有,左邊沒有的結果,沒有的部分顯示為Null。

如果將聯結條件順序調換一下,左右聯結的效果是一樣的。

SELECT Customers.cust_id, Orders.order_num
FROM Customers RIGHT JOIN Orders
ON Orders.cust_id = Customers.cust_id;    -- 聯結條件

全外聯結

在內連線的基礎上增加左邊有右邊沒有的,和右邊有左邊沒有的結果。MySQL是不支援的FULL JOIN的!但是可以通過UNIO組合左外聯結和右外聯結,達到同樣的效果。

子查詢(subquery)

子查詢就是巢狀在其它查詢中的查詢,注意:MySQL4.1之前的版本是不支援這一特性的。

使用場景:

  • 利用子查詢進行過濾
  • 建立計算欄位
  • 更多使用檢視後面綜合練習

利用子查詢進行過濾

將一條SELECT語句的返回結果作為另一條SELECT語句的WHERE子句

SELECT cln
FROM tablename
WHERE cln IN (SELECT cln
              FROM tablename
              WHERE cln IN (SELECT cln
                            FROM tablename
                            WHERE 查詢條件));

在這裡,子查詢由內向外處理,對於巢狀子查詢的數目也沒有限制,但在實際使用時,出於效能考慮,不要巢狀太多的子查詢。

作為計算欄位

假如有一個顧客表Customers和訂單表Orders,Orders表通過cust_id外來鍵引用Customers表中的cust_id主鍵。要求查詢Customers表中每個顧客的訂單總數。

要完成以上需求,需要以下兩步:

  • 從Customers表中檢索出顧客列表
  • 對於檢索出的每個顧客,統計其在Orders表中的訂單數目

下面利用子查詢完成這一需求

SELECT cust_name,
       (SELECT COUNT(*)
        FROM Orders
        WHERE Orders.cust_id = Customers.cust_id) AS orders
FROM Customers;

+---------------+--------+
| cust_name     | orders |
+---------------+--------+
| Village Toys  |      2 |
| Kids Place    |      0 |
| Fun4All       |      1 |
| Fun4All       |      1 |
| The Toy Store |      1 |
+---------------+--------+

說明:orders是一個計算欄位,它由子查詢建立,該子查詢對檢索出的每個顧客執行一次。這裡也使用了完全限定列名,以避免列名混淆。

通過聯結,也可以完成這一需求:

SELECT cust_name, COUNT(Orders.order_num) AS orders
FROM Customers LEFT JOIN Orders
ON Customers.cust_id = Orders.cust_id
GROUP BY Customers.cust_id;

要注意的一點是,這裡COUNT後不能用*,否則就是對行作統計,必須指定列名,這樣如果該列沒有值(顧客沒有訂單NULL),就不會被統計進去。

綜合練習

這裡將以插入資料部分建立的教學管理資料為基礎,進行練習。

1. 將所有的課程的名稱以及對應的任課老師姓名打印出來

SELECT crs_name, tea_name
FROM Course LEFT JOIN Teacher
ON Course.teacher_id = Teacher.tea_id;
+----------+------------+
| crs_name | tea_name   |
+----------+------------+
| 生物     | 張磊老師   |
| 物理     | 李平老師   |
| 體育     | 劉海燕老師 |
| 美術     | 李平老師   |
+----------+------------+
4 rows in set (0.00 sec)

2. 查詢學生表中男女生各有多少人?

SELECT gender, COUNT(*) AS total
FROM Student
GROUP BY gender;
+--------+-------+
| gender | total |
+--------+-------+
| 女     |     6 |
| 男     |    10 |
+--------+-------+
2 rows in set (0.00 sec)

3. 查詢物理成績等於100的學生的姓名

SELECT std_name, num
FROM Student INNER JOIN Score
ON Score.course_id = (SELECT crs_id FROM Course WHERE crs_name='物理')
AND Score.num = 100
AND Score.student_id = Student.std_id;
+----------+-----+
| std_name | num |
+----------+-----+
| 張四     | 100 |
| 鐵錘     | 100 |
| 李三     | 100 |
+----------+-----+
3 rows in set (0.00 sec)

4. 查詢平均成績大於八十分的同學的姓名和平均成績

-- 將成績根據學生id分組
SELECT std_name, AVG(Score.num)
FROM Student LEFT JOIN Score
ON Student.std_id = Score.student_id
GROUP BY Score.student_id
HAVING AVG(Score.num)>80;
+----------+----------------+
| std_name | AVG(Score.num) |
+----------+----------------+
| 張三     |        82.2500 |
| 劉三     |        87.0000 |
+----------+----------------+
2 rows in set (0.04 sec)

5. 查詢所有學生的學號,姓名,選課數,總成績

SELECT std_id, std_name, COUNT(Score.course_id), SUM(Score.num)
FROM Student LEFT JOIN Score
ON Student.std_id = Score.student_id
GROUP BY Student.std_id;
+--------+----------+------------------------+----------------+
| std_id | std_name | COUNT(Score.course_id) | SUM(Score.num) |
+--------+----------+------------------------+----------------+
|      1 | 理解     |                      3 |             85 |
|      2 | 鋼蛋     |                      3 |            185 |
|      3 | 張三     |                      4 |            329 |
|      4 | 張一     |                      4 |            257 |
|      5 | 張二     |                      4 |            257 |
|      6 | 張四