數據庫系統概論 - 實驗


數據庫系統概論 - 實驗

本文目的
按照《數據庫系統概論(第 5 版)習題解析與實驗指導》所列實驗內容進行實驗,記錄相關內容,熟悉數據庫常用操作。

參考資料
《數據庫系統概論(第 5 版)》,王珊,薩師煊,高等教育出版社;
《數據庫系統概論(第 5 版)習題解析與實驗指導》,王珊,張俊,高等教育出版社。

實驗環境
OS: ubuntu 16.04 server;
DBMS: MariaDB
mysql Ver 15.1 Distrib 10.0.38-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
VM: Virtual Box 6.1.16
3700X @ 3.6 GHz, 2/8 Cores, 4/32 GB Memory, 512GB SSD M.2/SATA WD
注:所有性能對比實驗只進行實驗內對比,不應進行實驗間對比(因為環境可能變化)。

示例來源
https://dev.mysql.com/doc/index-other.html
https://github.com/datacharmer/test_db (2020.2.12 copy);

目錄

0 准備

0.1 查詢

0.1.1 限制最大查詢數

MYSQL:

  • 從雇員表中查詢 10 條記錄
SELECT * FROM employees LIMIT 10;

0.2 SQL 語法

0.2.1 反引號、單引號

反引號用於區別保留字,比如將保留字用於屬性名。
單引號用於界定字符串,或者用於單引號的轉義字符。

0.2.2 語句分界符

可以使用 DELIMITER 修改分界符,實現在終端中輸入過程語句。如:

DELIMITER $$
BEGIN
  ...
END $$

0.2.3 SHOW 格式

可以加入 \G 以實現換行打印:

SHOW TRIGGERS \G;

0.3 MYSQL 配置

0.3.1 關閉緩存

MYSQL 默認緩存是打開的,緩存會影響性能試驗,因此需要手動關閉。

show variables like '%query_cache%';
+------------------------------+----------+
| Variable_name                | Value    |
+------------------------------+----------+
| have_query_cache             | YES      |
| query_cache_limit            | 1048576  |
| query_cache_min_res_unit     | 4096     |
| query_cache_size             | 16777216 |
| query_cache_strip_comments   | OFF      |
| query_cache_type             | ON       |
| query_cache_wlock_invalidate | OFF      |
+------------------------------+----------+

修改 MYSQL 配置文件,添加配置即可:

[mysqld]
query_cache_size=0
query_cache_type=0

0.3.2 遠程訪問配置

遠程訪問首先要關閉配置文件中綁定到本地的配置。即注釋掉配置文件中的 bind-addresss 所在行

/etc/mysql/mariadb.conf.d/50-server.cnf
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
# bind-address          = 127.0.0.1

然后重啟 MYSQL 服務,並檢查是否處於監聽狀態:

netstat -anpt
tcp6       0      0 :::3306                 :::*                    LISTEN      -

注:測試時,出現了重啟服務后仍然只監聽本地網絡,系統重啟后生效的情況。

最后進行權限配置,如:

GRANT ALL ON *.*  TO 'root'@'%' IDENTIFIED BY '123456';

1 數據庫定義與操作語言實驗

1.1 數據庫定義語言

1.1.1 實驗目的

理解和掌握數據庫 DDL 語言,能夠熟練地使用 SQL DDL 語句創建、修改和刪除數據庫、模式和基本表。

1.1.2 實驗內容和要求

理解和掌握 SQL DDL 語句的語法,特別是各種參數的具體含義和使用方法;使用 SQL 語句創建、修改和刪除數據庫、模式和基本表。掌握 SQL 語句常見語法錯誤的調試方法。

1.1.3 實驗重點和難點

實驗重點:創建數據庫、基本表。
實驗難點:創建基本表時,為不同的列選擇合適的數據類型,正確創建表級和列級完整性約束,如列值是否允許為空、主碼和完整性限制等。注意:數據完整性約束可以在創建基本表時定義,也可以先創建表然后定義完整性約束。由於完整性約束的限制,被引用的表要先創建。

1.1.4 實驗內容記錄

1.1.4.1 實驗內容簡述

本實驗建立對 employees 數據庫的認識,分析各個表的關系和約束,分析表中各個列的語意。

1.1.4.2 數據庫結構

數據庫名:employees

含義
departments 部門表
employees 雇員表
dept_emp 雇員部門表
dept_manager 部門經理表
titles 職位表
salaries 工資
CREATE TABLE employees (
    emp_no      INT             NOT NULL,
    birth_date  DATE            NOT NULL,
    first_name  VARCHAR(14)     NOT NULL,
    last_name   VARCHAR(16)     NOT NULL,
    gender      ENUM ('M','F')  NOT NULL,    
    hire_date   DATE            NOT NULL,
    PRIMARY KEY (emp_no)
);

CREATE TABLE departments (
    dept_no     CHAR(4)         NOT NULL,
    dept_name   VARCHAR(40)     NOT NULL,
    PRIMARY KEY (dept_no),
    UNIQUE  KEY (dept_name)
);

CREATE TABLE dept_manager (
   emp_no       INT             NOT NULL,
   dept_no      CHAR(4)         NOT NULL,
   from_date    DATE            NOT NULL,
   to_date      DATE            NOT NULL,
   FOREIGN KEY (emp_no)  REFERENCES employees (emp_no)    ON DELETE CASCADE,
   FOREIGN KEY (dept_no) REFERENCES departments (dept_no) ON DELETE CASCADE,
   PRIMARY KEY (emp_no,dept_no)
); 

CREATE TABLE dept_emp (
    emp_no      INT             NOT NULL,
    dept_no     CHAR(4)         NOT NULL,
    from_date   DATE            NOT NULL,
    to_date     DATE            NOT NULL,
    FOREIGN KEY (emp_no)  REFERENCES employees   (emp_no)  ON DELETE CASCADE,
    FOREIGN KEY (dept_no) REFERENCES departments (dept_no) ON DELETE CASCADE,
    PRIMARY KEY (emp_no,dept_no)
);

CREATE TABLE titles (
    emp_no      INT             NOT NULL,
    title       VARCHAR(50)     NOT NULL,
    from_date   DATE            NOT NULL,
    to_date     DATE,
    FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON DELETE CASCADE,
    PRIMARY KEY (emp_no,title, from_date)
); 

CREATE TABLE salaries (
    emp_no      INT             NOT NULL,
    salary      INT             NOT NULL,
    from_date   DATE            NOT NULL,
    to_date     DATE            NOT NULL,
    FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON DELETE CASCADE,
    PRIMARY KEY (emp_no, from_date)
); 

1.1.5 思考

  • SQL 語法規定,雙引號括定的符號串為對象名稱,單引號括定的符號串為常量字符串,那么什么情況下需要用雙引號來界定對象名呢?請實驗驗證。

  • 數據庫對象的完整引用是“服務器名.數據庫名.模式名.對象名”,但通常可以省略服務器名和數據庫名,甚至模式名,直接用對象名訪問對象即可。請設計相應的實驗驗證基本表及其列的訪問方法。

1.2 數據基本查詢實驗

1.2.1 實驗目的

掌握 SQL 程序設計基本規范,熟練運用 SQL 語言實現數據基本查詢,包括單表查詢、分組統計查詢和連接查詢。

1.2.2 實驗內容和要求

針對 TPC-H 數據庫設計各種表單查詢 SQL 語句、分組統計查詢語句;設計單個表針對自身的連接查詢,設計多個表的連接查詢。理解和掌握 SQL 查詢語句各個字句的特點和作用,按照 SQL 程序設計規范寫出具體的 SQL 查詢語句,並調試通過。
說明:簡單地說,SQL 程序設計規范包含 SQL 關鍵字大寫、表名、屬性名、存儲過程名等標識符大小寫混合、SQL 程序書寫縮緊排列等變成規范。

1.2.3 實驗重點和難點

實驗重點:分組統計查詢、單表自身連接查詢、多表連接查詢。
實驗難點:區分元組過濾條件和分組過濾條件;確定連接屬性,正確設計連接條件。

1.2.4 實驗內容記錄

1.2.4.1 單表查詢(查詢)
  • 查詢部門信息。
SELECT dept_no, dept_name 
FROM departments;
1.2.4.2 單表查詢(選擇)
  • 查詢雇佣日期為 1990-01-01 的所有男性員工信息。
SELECT * 
FROM employees
WHERE hire_date='1990-01-01' 
  AND gender='M';
1.2.4.3 不帶分組過濾條件的分組統計查詢
  • 查詢所有員工的工資總額。
SELECT emp_no, SUM(salary)
FROM salaries
GROUP BY emp_no;
1.2.4.4 帶分組過濾條件的分組統計查詢
  • 查詢工資總額不低於 100 萬的所有員工的工資總額。
SELECT emp_no, SUM(salary)
FROM salaries
GROUP BY emp_no
HAVING SUM(salary) >= 1000000;
1.2.4.5 兩表連接查詢(普通連接)
  • 查詢所有 1990 年入職的員工職位。
SELECT DISTINCT e.emp_no, first_name, last_name, hire_date, title
FROM employees e, titles t
WHERE e.emp_no=t.emp_no 
  AND hire_date
    BETWEEN '1990-01-01'
    AND '1990-12-31';

1.2.5 思考

  • 不在 GROUP BY 子句中出現的屬性,是否可以出現在 SELECT 子句中?請舉例並上機驗證。

析:
GROUP BY 用於按相同屬性分組,用來細化聚集函數的作用對象。使用 GROUP BY 子句后,查詢結果集中每個分組只有一個元組,因此對於沒有出現在 GROUP BY 子句中的屬性,在 SELECT 子句中出現會出現問題。舉例如:

查詢所有員工的總工資

SELECT emp_no, SUM(salary), salary
FROM salaries
GROUP BY emp_no;
+--------+-------------+--------+
| emp_no | SUM(salary) | salary |
+--------+-------------+--------+
|  10001 |     1281612 |  60117 |
|  10002 |      413127 |  65828 |
|  10003 |      301212 |  40006 |
|  10004 |      904196 |  40054 |
|  10005 |     1134585 |  78228 |
|  10006 |      606179 |  40000 |

對於沒有出現在 GROUP BY 中的屬性,MYSQL 選取了分組中的第一個元組值(盡管沒有實際意義)。

  • 請舉例說明分組統計查詢中 WHERE 和 HAVING 有何區別?

兩者區別在於作用對象不同,WHERE 作用於基本表或視圖,HAVING 作用於組。舉例如:

SELECT emp_no, SUM(salary)
FROM salaries
GROUP BY emp_no
HAVING SUM(salary)>1000000;
+--------+-------------+
| emp_no | SUM(salary) |
+--------+-------------+
|  10001 |     1281612 |
|  10005 |     1134585 |
|  10009 |     1409122 |
|  10018 |     1098241 |
|  10021 |     1029743 |
|  10050 |     1067848 |
SELECT emp_no, SUM(salary)
FROM salaries
WHERE SUM(salary)>1000000
GROUP BY emp_no;
ERROR 1111 (HY000): Invalid use of group function

WHERE 子句中不能使用聚集函數作為條件表達式

  • 連接查詢速度是影響關系數據庫性能的關鍵因素。請討論如何提高連接查詢速度,並進行實驗驗證。

連接操作中最慢的是嵌套循環算法,如果有序,或者有索引,則可以進行優化。
如果連接條件只有等值比較,則容易優化(有序雙指針比較);
如果連接條件能夠建立索引,且事先已建立索引,則容易優化;

舉例:

查詢在 1992-01-01 入職的員工及其所在部門信息。

SELECT COUNT(*) FROM (
  SELECT e.emp_no, d.dept_name, e.hire_date
  FROM employees e, dept_emp de, departments d
  WHERE e.emp_no=de.emp_no
  AND de.dept_no=d.dept_no
  AND e.hire_date='1992-01-01'
) AS C;
+----------+
| COUNT(*) |
+----------+
|      278 |
+----------+
1 row in set (0.26 sec)
SELECT COUNT(*) FROM (
  SELECT e.emp_no, d.dept_name, e.hire_date
  FROM employees e, dept_emp de, departments d
  WHERE e.emp_no=de.emp_no
  AND de.dept_no=d.dept_no
  AND e.first_name='Georgi'
) AS C;
+----------+
| COUNT(*) |
+----------+
|      278 |
+----------+
1 row in set (0.27 sec)
SELECT COUNT(*) FROM (
  SELECT e.emp_no, d.dept_name, e.hire_date
  FROM employees e, dept_emp de, departments d
  WHERE e.emp_no=de.emp_no
  AND de.dept_no=d.dept_no
  AND d.dept_name='Finance'
) AS C;
+----------+
| COUNT(*) |
+----------+
|    17346 |
+----------+
1 row in set (0.02 sec)

可以看出,對於存在 UNIQUE 完整性限制的屬性,連接速度提升非常大。

1.3 數據高級查詢實驗

1.3.1 實驗目的

掌握 SQL 嵌套查詢和集合查詢等各種高級查詢的設計方法等。

1.3.2 實驗內容和要求

針對 TPC-H 數據庫,正確分析用戶查詢要求,設計各種嵌套查詢和集合查詢。

1.3.3 實驗重點和難點

實驗重點:嵌套查詢。
實驗難點:相關子查詢、多層 EXIST 嵌套查詢。

1.3.4 實驗內容記錄

1.3.4.1 IN 嵌套查詢
  • 查詢財務部門經理的工資記錄。
SELECT s.emp_no, first_name, last_name, salary, 
  s.from_date, s.to_date
FROM salaries s, employees e
WHERE s.emp_no=e.emp_no
AND s.emp_no IN (
    SELECT dm.emp_no
    FROM dept_manager dm, departments d
    WHERE d.dept_name='Finance'
    AND d.dept_no=dm.dept_no
);
1.3.4.2 單層 EXISTS 嵌套查詢

注:帶 EXISTS 謂詞的子查詢不一定能被其它形式的子查詢等價替換。

  • 統計(曾經)在財務部門的員工數量。

使用 EXISTS 謂詞可以理解為找出這樣的員工,該員工存在部門為財務的元組。

SELECT COUNT(DISTINCT de.emp_no)
FROM dept_emp de
WHERE EXISTS (
  SELECT *
  FROM departments d
  WHERE d.dept_name='Finance'
  AND d.dept_no=de.dept_no
);

如果用兩表連接查詢,可以實現相似的語義(語義存在細微區別,前者是存在語義,后者是計數語義)。

SELECT COUNT(DISTINCT de.emp_no)
FROM dept_emp de, departments d
WHERE de.dept_no=d.dept_no
AND d.dept_name='Finance';
1.3.4.3 兩層 EXISTS 嵌套查詢

注意:SQL 查詢的結果是一個集合,實際上就是一層全稱謂詞。如果給出的條件中再出現全稱謂詞,則可以轉換為存在謂詞進行查詢。

注意:嵌套查詢如果進行了相關查詢,則實際上就是一個外層和內層進行笛卡爾積然后對結果集合進行篩選(嵌套內層進行篩選)的過程。

  • 查詢(曾經)在所有部門待過的員工。

轉換為,不存在這樣的部門,員工沒有待過。

SELECT emp_no
FROM employees e
WHERE NOT EXISTS (
  SELECT *
  FROM departments d
  WHERE NOT EXISTS (
    SELECT *
    FROM dept_emp de
    WHERE d.dept_no=de.dept_no
    AND e.emp_no=de.emp_no
  )
);
1.3.4.4 FROM 子句中的嵌套查詢

FROM 子句中進行嵌套是將嵌套塊生成臨時表進行查詢。

  • 統計所有員工的平均總工資。

首先需要一張員工的總工資表,然后根據總工資表進行平均數計算。

SELECT SUM(total_salary)/COUNT(DISTINCT total_emp_no) avg
FROM (
  SELECT emp_no total_emp_no, SUM(salary) total_salary
  FROM salaries
  GROUP BY emp_no
)
AS total;
+-------------+
| avg         |
+-------------+
| 604887.4671 |
+-------------+
1.3.4.5 集合查詢(交、並、差)

與集合運算的要求一致,集合查詢時進行集合運算的集合必須具有相同的列數以及對應有相同的數據類型。

  • 統計財務(Finance)部門和銷售(Sales)部門的歷史員工數。
SELECT COUNT(DISTINCT emp_no)
FROM dept_emp de, departments d
WHERE de.dept_no=d.dept_no
AND d.dept_name IN ('Finance')
UNION
SELECT COUNT(DISTINCT emp_no)
FROM dept_emp de, departments d
WHERE de.dept_no=d.dept_no
AND d.dept_name IN ('Sales');
+------------------------+
| COUNT(DISTINCT emp_no) |
+------------------------+
|                  17346 |
|                  52245 |
+------------------------+
  • 查詢在財務(Finance)部門和銷售(Sales)部門都工作過的員工。

注:MYSQL 目前不支持交運算

SELECT DISTINCT de.emp_no
FROM dept_emp de, departments d
WHERE de.dept_no=d.dept_no
AND d.dept_name='Finance'
INTERSECT
SELECT DISTINCT de.emp_no
FROM dept_emp de, departments d
WHERE de.dept_no=d.dept_no
AND d.dept_name='Sales';

1.3.5 思考

  • 試分析什么類型的查詢可以用連接查詢實現,什么類型的查詢只能用嵌套查詢實現?

連接查詢在算法實現上,一定可以通過嵌套循環實現,因此連接查詢一定能被嵌套查詢等價替換。由於嵌套查詢在嵌套條件上提供了一些語義,因此嵌套查詢不一定能被轉換為連接查詢。
從語義上看,連接查詢首先是對兩個表進行笛卡爾積運算(不帶條件的),然后對得到的元組集合進行條件篩選,因此連接查詢適用於需要將多個表的屬性關聯起來的查詢需求。嵌套查詢則是外層表和內層表進行嵌套羅列,嵌套時可以使用 IN、ANY、ALL、EXISTS 謂詞。這些謂詞的使用使得嵌套查詢語義不一定能被連接查詢實現。
連接查詢在算法實現上不一定需要通過嵌套循環實現,因此效率往往高於嵌套查詢。如:

統計在財務(Finance)部門的員工人數。

SELECT COUNT(e.emp_no)
FROM employees e, dept_emp de, departments d
WHERE e.emp_no=de.emp_no
AND de.dept_no=d.dept_no
AND d.dept_name='Finance';
+-----------------+
| COUNT(e.emp_no) |
+-----------------+
|           17346 |
+-----------------+
1 row in set (0.02 sec)
SELECT COUNT(e.emp_no)
FROM employees e
WHERE EXISTS (
  SELECT *
  FROM dept_emp de
  WHERE EXISTS (
    SELECT *
    FROM departments d
    WHERE e.emp_no=de.emp_no
    AND de.dept_no=d.dept_no
    AND d.dept_name='Finance' 
  )
);
+-----------------+
| COUNT(e.emp_no) |
+-----------------+
|           17346 |
+-----------------+
1 row in set (0.40 sec)

可以看出來,三個表的連接查詢相比三個表的嵌套查詢在速度上要快很多。

  • 試分析不相關子查詢和相關子查詢的區別。

兩者區別在於子查詢是否引用外層查詢的屬性。若不引用,則兩個查詢完全隔離,相當於是兩個獨立的查詢。
從實現上看,相關子查詢由於存在相關引用,因此子查詢被執行多次,而不相關子查詢由於是一個獨立的查詢,因此只執行一次。

1.4 數據更新實驗

1.4.1 實驗目的

熟悉數據庫的數據更新操作,能夠使用 SQL 語句對數據庫進行數據的插入、修改、刪除操作。

1.4.2 實驗內容和要求

針對 employees 數據庫設計單元組插入、批量修改插入、修改數據和刪除數據等 SQL 語句。理解和掌握 INSERT、UPDATE 和 DELETE 語法結構的各個組成成分,結合嵌套 SQL 子查詢,分別設計幾種不同形式的插入、修改和刪除的語句,並調試成功。

1.4.3 實驗重點和難點

實驗重點:插入、修改和刪除數據的 SQL。
實驗難點:與嵌套 SQL 子查詢相結合的插入、修改和刪除數據的 SQL 語句;利用一個表的數據來插入、修改和刪除另外一個表的數據。

1.4.4 實驗內容記錄

1.4.4.1 INSERT 基本語句

插入一條雇員記錄。

INSERT INTO employees
VALUES(500000,'1990-05-06','San','Zhang','M','2010-09-05');
INSERT INTO employees(emp_no,birth_date,first_name,last_name,gender,hire_date)
VALUES(500000,'1990-05-06','San','Zhang','M','2010-09-05');
1.4.4.2 批量數據 INSERT 語句
  • 創建一個男員工表和女員工表,並將所有男員工插入到男員工表,將所有女員工插入到女員工表。

先創建兩張表。

CREATE TABLE employees_female 
AS SELECT * 
FROM employees
WHERE 1=2;
CREATE TABLE employees_male
AS SELECT *
FROM employees
WHERE 1=2;

注:這里的條件永不滿足,用來復制表模式,而不復制任何數據。

然后將數據錄入兩張表。

INSERT INTO employees_female
SELECT e.*
FROM employees e
WHERE e.gender='F';
INSERT INTO employees_male
SELECT e.*
FROM employees e
WHERE e.gender='M';
  • 創建一個部門員工人數統計表,並統計所有部門的歷史員工人數。

先創建表。

CREATE TABLE dept_emp_count(
  dept_no CHAR(4),
  emp_count INT);

然后批量錄入數據。

INSERT INTO dept_emp_count
SELECT de.dept_no, COUNT(DISTINCT emp_no)
FROM dept_emp de
GROUP BY de.dept_no;
1.4.4.3 UPDATE 語句(修改部分記錄的部分列值)
  • 將所有 1985 年及以前入職,職位為 Assistant Engineer 的員工的職位修改為 Engineer 。
UPDATE titles t
SET t.title='Engineer'
WHERE t.emp_no IN (
  SELECT e.emp_no
  FROM employees e
  WHERE e.hire_date < '1986-01-01'
)
AND t.title='Assistant Engineer';
1.4.4.4 UPDATE 語句(利用一個表的數據更新另一個表的數據)
  • 更新部門員工人數統計表(假設表已經存在)。
UPDATE dept_emp_count decount
SET decount.emp_count = (
  SELECT COUNT(DISTINCT de.emp_no)
  FROM dept_emp de
  WHERE de.dept_no=decount.dept_no
);
1.4.4.5 DELETE 基本語句(刪除給定條件的所有記錄)
  • 刪除 2000 年以前的工資記錄。
DELETE FROM salaries
WHERE from_date < '2000-01-01'
AND to_date < '2000-01-01';

1.4.5 思考

  • 請分析數據庫模式更新和數據更新 SQL 語句的異同。

兩者的關鍵字不同,更新模式使用 ALTER TABLE ,更新數據使用 UPDATE 。

  • 請分析數據庫系統除了 INSERT、UPDATE 和 DELETE 等基本的數據更新語句之外,還有哪些可以用來更新數據庫基本表數據的 SQL 語句?

如 truncate 。

1.5 視圖實驗

1.5.1 實驗目的

熟悉 SQL 語言有關視圖的操作,能夠熟練使用 SQL 語句來創建需要的視圖,定義數據庫外模式,並能使用所創建的視圖實現數據管理。

1.5.2 實驗內容和要求

針對給定的數據庫模式,以及相應的應用需求,創建視圖和帶 WITH CHECK OPTION 的視圖,並驗證 WITH CHECK OPTION 選項的有效性。理解和掌握視圖消解執行原理,掌握可更新視圖和不可更新視圖的區別。

1.5.3 實驗重點和難點

實驗重點:創建視圖。
實驗難點:可更新的視圖和不可更新的視圖之區別,WITH CHECK OPTION 的驗證。

1.5.4 實驗內容記錄

1.5.4.1 創建視圖(省略視圖列名)
  • 創建男性員工視圖。
CREATE VIEW v_emploree_male AS
SELECT *
FROM employees e
WHERE e.gender='M';
1.5.4.2 創建視圖(不能省略列名的情況)

如果目標列不是單純的屬性名(如聚集函數或表達式),則應該給出列名。

  • 創建部門歷史員工總數的視圖。
CREATE VIEW v_dept_emp_num(dept_no, emp_count) AS
SELECT dept_no, COUNT(DISTINCT emp_no)
FROM dept_emp
GROUP BY dept_no;
1.5.4.3 創建視圖(WITH CHECK OPTION)

WITH CHECK OPTION 使得在對視圖進行操作時,會驗證操作是否符合視圖的條件。

例如,對男性員工視圖進行插入操作,插入一個女員工信息:

INSERT INTO
v_emploree_male
VALUES(500000,'1990-05-06','San','Zhang','F','2010-09-05');

由男性員工視圖沒有 WITH CHECK OPTION 語句,因此插入成功,這使得視圖的封裝性被破壞。
加入選項:

CREATE VIEW v_emploree_male_check AS
SELECT *
FROM employees e
WHERE e.gender='M'
WITH CHECK OPTION;

再執行插入:

INSERT INTO
v_emploree_male_check
VALUES(500000,'1990-05-06','San','Zhang','F','2010-09-05');

會得到錯誤:

ERROR 1369 (HY000): CHECK OPTION failed 'employees.v_emploree_male_check'
1.5.4.4 不可更新視圖

如果為行列子集視圖,則一定是可以更新的(如男性員工視圖)。
如果不是,則不一定可以更新,如屬性為聚集函數或表達式(如部門員工總數視圖)。

  • 嘗試修改財務(Finance)部門員工總數為 0 並觀察報錯。
UPDATE v_dept_emp_num vden
SET emp_count=0
WHERE vden.dept_no=(
  SELECT d.dept_no
  FROM departments
  WHERE d.dept_name='Finance'
);

得到報錯:

ERROR 1288 (HY000): The target table vden of the UPDATE is not updatable
1.5.4.5 刪除視圖

刪除未被引用的視圖,直接刪除即可。

  • 刪除部門員工總數視圖和男性員工視圖。
DROP VIEW v_dept_emp_num;
DROP VIEW v_emploree_male;

刪除視圖時 CASCADE 語句的作用:

先建立兩個視圖:

CREATE VIEW v_e_1 AS
SELECT emp_no,birth_date,gender
FROM employees;
CREATE VIEW v_e_2 AS
SELECT emp_no,gender
FROM v_e_1;

直接刪除 v_e_1,

DROP VIEW v_e_1;

對於 MYSQL ,刪除成功,但是會引起 v_e_2 查詢報錯。

ERROR 1356 (HY000): View 'employees.v_e_2' references invalid table(s) or column(s) or function(s) or definer/invoker of view lack rights to use them

不論使用 RESTRICT 短語還是 CASCADE 短語結果都是一樣。

1.5.5 思考

  • 請分析視圖和基本表在使用方面有哪些異同,並設計相應的例子加以驗證。

視圖的數據來源於最原始的基本表,也即視圖本身不存放數據。
視圖不一定都是可以修改的。

  • 請具體分析修改基本表的結構對相應的視圖會產生何種影響?

修改基本表后,可能導致視圖不能正常工作。

先創建一個測試表:

CREATE TABLE d1
SELECT * FROM departments;

並創建基於測試表的一個視圖:

CREATE VIEW v_d1 AS
SELECT * FROM d1;

然后調整測試表的結構,這里先嘗試調整列的數據類型:

ALTER TABLE d1
MODIFY COLUMN dept_no INT;

調整會觸發警告,數據變為 0 。相應的視圖也會同步改變。

如果刪除一列:

ALTER TABLE d1
DROP COLUMN dept_name;

此時查詢視圖會報告錯誤。

1.6 索引實驗

1.6.1 實驗目的

掌握索引設計原則和技巧,能夠創建合適的索引以提高數據庫查詢、統計分析效率。

1.6.2 實驗內容和要求

針對給定的數據庫模式和具體應用需求,創建唯一索引、函數索引、復合索引等;修改索引;刪除索引。設計相應的 SQL 查詢驗證索引有效性。學習利用 EXPLAIN 命令分析 SQL 查詢是否使用了所創建的索引,並能夠分析其原因,執行 SQL 查詢並估算索引提高查詢效率的百分比。要求實驗數據集達到 10 萬條記錄以上的數據量,以便驗證索引效果。

1.6.3 實驗重點和難點

實驗重點:創建索引。
實驗難點:設計 SQL 查詢驗證索引有效性。

1.6.4 實驗內容記錄

1.6.4.1 創建唯一索引
  • 對員工表的員工號碼建立唯一索引
CREATE UNIQUE INDEX idx_emp_no
ON employees (emp_no);

注:主碼列在創建表時會自動創建相應的索引。
注:在存在數據時,建立索引需要一定的時間。

1.6.4.2 創建復合索引
  • 對員工表的名字建立復合索引
CREATE INDEX idx_name
ON employees (first_name, last_name);
1.6.4.3 創建長度函數索引

MYSQL 函數索引暫不錄入。

1.6.4.4 創建聚簇索引

MYSQL Innodb 不支持單獨建立聚簇索引。在此不列出。

1.6.4.5 創建 HASH 索引
  • 對員工的雇佣日期創建 HASH 索引
CREATE INDEX idx_hash_hiredate
USING HASH
ON employees (hire_date);

注:MYSQL 創建 HASH 索引的語法和書中給出的語法稍有不同。

1.6.4.6 修改索引名稱

注:修改索引名稱通過先刪除再添加實現。

1.6.4.7 查詢表上的已有索引
  • 查詢員工表上的已有索引
SHOW INDEX FROM employees;
MariaDB [employees]> SHOW INDEX FROM employees;
+-----------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table     | Non_unique | Key_name          | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-----------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| employees |          0 | PRIMARY           |            1 | emp_no      | A         |      299290 |     NULL | NULL   |      | BTREE      |         |               |
| employees |          0 | idx_emp_no        |            1 | emp_no      | A         |      299290 |     NULL | NULL   |      | BTREE      |         |               |
| employees |          1 | idx_name          |            1 | first_name  | A         |        2672 |     NULL | NULL   |      | BTREE      |         |               |
| employees |          1 | idx_name          |            2 | last_name   | A         |      299290 |     NULL | NULL   |      | BTREE      |         |               |
| employees |          1 | idx_hash_hiredate |            1 | hire_date   | A         |       10688 |     NULL | NULL   |      | BTREE      |         |               |
+-----------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
1.6.4.8 分析某個 SQL 語句執行時是否使用了索引
  • 分析從員工表上查詢某個日期雇佣的員工是否使用了索引。
EXPLAIN SELECT * 
FROM employees 
WHERE hire_date='1990-01-01';

得到結果如下:

MariaDB [employees]> EXPLAIN SELECT *  FROM employees  WHERE hire_date='1990-01-01';
+------+-------------+-----------+------+-------------------+-------------------+---------+-------+------+-------+
| id   | select_type | table     | type | possible_keys     | key               | key_len | ref   | rows | Extra |
+------+-------------+-----------+------+-------------------+-------------------+---------+-------+------+-------+
|    1 | SIMPLE      | employees | ref  | idx_hash_hiredate | idx_hash_hiredate | 3       | const |   65 |       |
+------+-------------+-----------+------+-------------------+-------------------+---------+-------+------+-------+
1 row in set (0.00 sec)
1.6.4.9 驗證索引效率

首先驗證復合索引,在有索引的情況下,統計指定名字的員工數目。

SELECT COUNT(*)
FROM employees
WHERE first_name='Georgi'
AND last_name='Facello';

得到結果為:

+----------+
| COUNT(*) |
+----------+
|        2 |
+----------+
1 row in set (0.00 sec)

然后去掉復合索引,再次執行,結果為:

+----------+
| COUNT(*) |
+----------+
|        2 |
+----------+
1 row in set (0.06 sec)

結果顯示復合索引提高了查詢效率。

然后驗證哈希索引,在有索引的情況下,統計指定雇佣日期的員工數目。

SELECT COUNT(*)
FROM employees
WHERE hire_date='1990-01-01';

結果為:

+----------+
| COUNT(*) |
+----------+
|       65 |
+----------+
1 row in set (0.00 sec)

去掉索引,然后在查詢,結果為:

+----------+
| COUNT(*) |
+----------+
|       65 |
+----------+
1 row in set (0.06 sec)

結果顯示哈希索引提高了查詢效率。

1.6.5 思考

  • 在一個表的多個字段上創建的復合索引,與在相應的每個字段上創建的多個簡單索引有何異同?請設計相應的例子加以驗證。

可以直接創建兩個在名字上的簡單索引然后測試即可。

CREATE INDEX idx_first_name
ON employees(first_name);
CREATE INDEX idx_last_name
ON employees(last_name);
EXPLAIN
SELECT COUNT(*)
FROM employees
WHERE first_name='Georgi'
AND last_name='Facello';

執行結果為:

+------+-------------+-----------+-------------+------------------------------+------------------------------+---------+------+------+-------------------------------------------------------------------------+
| id   | select_type | table     | type        | possible_keys                | key                          | key_len | ref  | rows | Extra                                                                   |
+------+-------------+-----------+-------------+------------------------------+------------------------------+---------+------+------+-------------------------------------------------------------------------+
|    1 | SIMPLE      | employees | index_merge | idx_first_name,idx_last_name | idx_last_name,idx_first_name | 66,58   | NULL |    1 | Using intersect(idx_last_name,idx_first_name); Using where; Using index |
+------+-------------+-----------+-------------+------------------------------+------------------------------+---------+------+------+-------------------------------------------------------------------------+
1 row in set (0.00 sec)

也即 MYSQL 內部和執行了對多個單列索引合並為一個復合索引的優化,此時效率不受影響。
對於已定義的復合索引,從最左側開始的列是可用的,例如:

EXPLAIN SELECT COUNT(*) FROM employees WHERE last_name='Facello';
+------+-------------+-----------+-------+---------------+----------+---------+------+--------+--------------------------+
| id   | select_type | table     | type  | possible_keys | key      | key_len | ref  | rows   | Extra                    |
+------+-------------+-----------+-------+---------------+----------+---------+------+--------+--------------------------+
|    1 | SIMPLE      | employees | index | NULL          | idx_name | 124     | NULL | 299290 | Using where; Using index |
+------+-------------+-----------+-------+---------------+----------+---------+------+--------+--------------------------+

這里在已定義了復合索引的情況下只查詢第二個條件,發現索引沒有被采用,而是進行了全表掃描。

2 安全性語言實驗

2.1 自主存取控制實驗

2.1.1 實驗目的

掌握自主存取控制權限的定義和維護方法。

2.1.2 實驗內容和要求

定義用戶、角色,分配權限給用戶、角色,回收權限,以相應的用戶名登陸數據庫驗證權限分配是否正確。選擇一個應用場景,使用自主存取控制機制設計權限分配。可以采用兩種方案。
方案一:采用 SYSTEM 超級用戶登錄數據庫(對於 MYSQL(Linux) 則是 root),完成所有權限分配工作,然后用相應用戶名登陸數據庫以驗證權限分配正確性;
方案二:采用 SYSTEM 用戶登陸數據庫創建三個部門經理用戶,並分配相應的權限,然后分別用三個經理用戶名登陸數據庫,創建相應部門的 USER、ROLE ,並分配相應權限。

2.1.3 實驗重點和難點

實驗重點:定義角色,分配權限和回收權限。
實驗難點:實驗方案二實現權限的再分配和回收。

2.1.4 實驗內容記錄

本次實驗中,為財務部(finance)創建一個管理員,部門的管理員只有管理自己部門內部的員工的權限以及訪問部門表(departments)、部門管理員表(dept_manager)的權限。

注:為了減少工作量,只為財務部門創建相關用戶。其它部門的操作可以類比。

2.1.4.1 建立視圖

數據庫基本表沒有區分不同的部門,因此通過視圖來划分不同部門的員工。

先創建員工所在部門視圖:

CREATE VIEW v_emp_dept(emp_no, dept_no, dept_name) AS
SELECT de.emp_no, d.dept_no, d.dept_name
FROM dept_emp de, departments d
WHERE de.dept_no=d.dept_no;

然后為各個表創建部門視圖:

CREATE VIEW v_finance_employees AS
SELECT e.*
FROM employees e, v_emp_dept ved
WHERE e.emp_no=ved.emp_no
AND ved.dept_name='Finance'
WITH CHECK OPTION;
CREATE VIEW v_finance_salaries AS
SELECT s.*
FROM salaries s, v_emp_dept ved
WHERE s.emp_no=ved.emp_no
AND ved.dept_name='Finance'
WITH CHECK OPTION;
CREATE VIEW v_finance_dept_emp AS
SELECT de.*
FROM dept_emp de, v_emp_dept ved
WHERE de.emp_no=ved.emp_no
AND ved.dept_name='Finance'
WITH CHECK OPTION;
CREATE VIEW v_finance_titles AS
SELECT t.*
FROM titles t, v_emp_dept ved
WHERE t.emp_no=ved.emp_no
AND ved.dept_name='Finance'
WITH CHECK OPTION;
2.1.4.2 建立角色

為財務部門建立一個經理角色(完全控制本部門數據,同時能查詢其它部門的數據,能夠為本部門職員分配權限),一個職員角色(可以插入和查詢本部門數據)。

CREATE ROLE 
  role_finance_manager,
  role_finance_staff;

為角色分配權限。

注:MYSQL 只有 WITH GRANT OPTION,並且行為是非級聯權限收回。

先分配部門經理角色對本部門表的權限。

GRANT ALL 
ON emplorees.v_finance_emplorees
TO role_finance_manager
WITH GRANT OPTION;
GRANT ALL 
ON emplorees.v_finance_salaries
TO role_finance_manager
WITH GRANT OPTION;
GRANT ALL 
ON emplorees.v_finance_dept_emp
TO role_finance_manager
WITH GRANT OPTION;
GRANT ALL 
ON emplorees.v_finance_titles
TO role_finance_manager
WITH GRANT OPTION;

分配部門經理對其它部門表的查詢權限。

GRANT SELECT
ON employees.*
TO role_finance_manager;

分配員工對本部門的權限。

GRANT SELECT, INSERT
ON employees.v_finance_employees
TO role_finance_staff;
GRANT SELECT, INSERT
ON employees.v_finance_salaries
TO role_finance_staff;
GRANT SELECT, INSERT
ON employees.v_finance_dept_emp
TO role_finance_staff;
GRANT SELECT, INSERT
ON employees.v_finance_titles
TO role_finance_staff;
2.1.4.3 建立用戶

為財務部門建立一個經理用戶和一個職員用戶。

CREATE USER user_manager
IDENTIFIED BY '123456';
CREATE USER user_staff
IDENTIFIED BY '123456';

將角色賦予用戶。

GRANT role_finance_manager
TO user_manager;
GRANT role_finance_staff
TO user_staff;
2.1.4.4 查詢角色和用戶的權限

查詢權限使用 SHOW GRANTS 。

查詢角色的權限。

SHOW GRANTS FOR role_finance_manager;
MariaDB [employees]> SHOW GRANTS FOR role_finance_manager;
+-------------------------------------------------------------------------------------------------------+
| Grants for role_finance_manager                                                                       |
+-------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'role_finance_manager'                                                          |
| GRANT SELECT ON `employees`.* TO 'role_finance_manager'                                               |
| GRANT ALL PRIVILEGES ON `emplorees`.`v_finance_titles` TO 'role_finance_manager' WITH GRANT OPTION    |
| GRANT ALL PRIVILEGES ON `emplorees`.`v_finance_emplorees` TO 'role_finance_manager' WITH GRANT OPTION |
| GRANT ALL PRIVILEGES ON `emplorees`.`v_finance_dept_emp` TO 'role_finance_manager' WITH GRANT OPTION  |
| GRANT ALL PRIVILEGES ON `emplorees`.`v_finance_salaries` TO 'role_finance_manager' WITH GRANT OPTION  |
+-------------------------------------------------------------------------------------------------------+
SHOW GRANTS FOR role_finance_staff;
MariaDB [employees]> SHOW GRANTS FOR role_finance_staff;
+-----------------------------------------------------------------------------------+
| Grants for role_finance_staff                                                     |
+-----------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'role_finance_staff'                                        |
| GRANT SELECT, INSERT ON `employees`.`v_finance_salaries` TO 'role_finance_staff'  |
| GRANT SELECT, INSERT ON `employees`.`v_finance_titles` TO 'role_finance_staff'    |
| GRANT SELECT, INSERT ON `employees`.`v_finance_employees` TO 'role_finance_staff' |
| GRANT SELECT, INSERT ON `employees`.`v_finance_dept_emp` TO 'role_finance_staff'  |
+-----------------------------------------------------------------------------------+

查詢用戶的權限。

SHOW GRANTS FOR user_manager;
MariaDB [employees]> SHOW GRANTS FOR user_manager;
+-------------------------------------------------------------------------------------------------------------+
| Grants for user_manager@%                                                                                   |
+-------------------------------------------------------------------------------------------------------------+
| GRANT role_finance_manager TO 'user_manager'@'%'                                                            |
| GRANT USAGE ON *.* TO 'user_manager'@'%' IDENTIFIED BY PASSWORD '*6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9' |
+-------------------------------------------------------------------------------------------------------------+
SHOW GRANTS FOR user_staff;
MariaDB [(none)]> SHOW GRANTS FOR user_staff;
+-----------------------------------------------------------------------------------------------------------+
| Grants for user_staff@%                                                                                   |
+-----------------------------------------------------------------------------------------------------------+
| GRANT role_finance_staff TO 'user_staff'@'%'                                                              |
| GRANT USAGE ON *.* TO 'user_staff'@'%' IDENTIFIED BY PASSWORD '*6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9' |
+-----------------------------------------------------------------------------------------------------------+
2.1.4.5 驗證權限分配正確性

僅僅對職員用戶進行嘗試。

使用職員用戶登錄。

訪問數據庫時,會出現報錯:

ERROR 1044 (42000): Access denied for user 'user_staff'@'%' to database 'employees'

原因是角色未啟用。 [1]

登錄后,啟用角色

SET ROLE role_finance_staff;

此時查詢表,為

MariaDB [employees]> show tables;
+---------------------+
| Tables_in_employees |
+---------------------+
| v_finance_dept_emp  |
| v_finance_employees |
| v_finance_salaries  |
| v_finance_titles    |
+---------------------+
4 rows in set (0.00 sec)

思考

  • 請分析 WITH CHECK OPTION、WITH GRANT OPTION、WITH ADMIN OPTION 有何區別和聯系。

WITH CHECK OPTION 是視圖選項,用於達到對視圖進行更新時檢查視圖的條件的要求。WITH GRANT OPTION 和 WITH ADMIN OPTION 在授權時使用,前者(WITH GRANT OPTION)在權限被撤銷時,會級聯撤銷,后者不會級聯撤銷。
注:MYSQL 中只有 WITH GRANT OPTION ,且其行為類似 WITH ADMIN OPTION 。

  • 請結合上述實驗示例分析使用角色進行權限分配有何優缺點。

使用角色進行權限分配便於管理權限(一個角色對應一種權限,而不是一個用戶對應一種權限)。

2.2 審計實驗

注:SQL 標准中不包含審計語句。
暫略。

3 完整性語言實驗

3.1 實體完整性實驗

3.1.1 實驗目的

掌握實體完整性的定義和維護方法。

3.1.2 實驗內容和要求

定義實體完整性,刪除實體完整性。能夠寫出兩種方式定義實體完整性的 SQL 語句:創建表時定義實體完整性、創建表后定義實體完整性。設計 SQL 語句驗證完整性約束是否起作用。

3.1.3 實驗重點和難點

實驗重點:創建表時定義實體完整性。
實驗難點:有多個候選碼時實體完整性的定義。

3.1.4 實驗內容記錄

這里參照 employees 數據庫創建一個新的數據庫 test;

CREATE DATABASE test;
3.1.4.1 創建表時定義實體完整性(列級實體完整性)

創建雇員表。

CREATE TABLE employees (
    emp_no      INT             NOT NULL PRIMARY KEY,
    birth_date  DATE            NOT NULL,
    first_name  VARCHAR(14)     NOT NULL,
    last_name   VARCHAR(16)     NOT NULL,
    gender      ENUM ('M','F')  NOT NULL,    
    hire_date   DATE            NOT NULL
);
3.1.4.2 創建表時定義實體完整性(表級實體完整性)

創建雇員表。

CREATE TABLE employees (
    emp_no      INT             NOT NULL,
    birth_date  DATE            NOT NULL,
    first_name  VARCHAR(14)     NOT NULL,
    last_name   VARCHAR(16)     NOT NULL,
    gender      ENUM ('M','F')  NOT NULL,    
    hire_date   DATE            NOT NULL,
    CONSTRAINT PK_empno PRIMARY KEY(emp_no)
);

注:MYSQL 不支持約束命名。

3.1.4.3 創建表后定義實體完整性

創建雇員表。

CREATE TABLE employees (
    emp_no      INT             NOT NULL,
    birth_date  DATE            NOT NULL,
    first_name  VARCHAR(14)     NOT NULL,
    last_name   VARCHAR(16)     NOT NULL,
    gender      ENUM ('M','F')  NOT NULL,    
    hire_date   DATE            NOT NULL
);
ALTER TABLE employees
ADD PRIMARY KEY(emp_no);
3.1.4.4 定義實體完整性(多屬性主碼)

創建職位表。

CREATE TABLE titles (
    emp_no      INT             NOT NULL,
    title       VARCHAR(50)     NOT NULL,
    from_date   DATE            NOT NULL,
    to_date     DATE,
    FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON DELETE CASCADE,
    PRIMARY KEY (emp_no,title, from_date)
); 
3.1.4.5 有多個候選碼時定義實體完整性

當存在多個候選碼時,只能定義一個主碼,其它的候選碼定義唯一性約束。

創建部門表。

CREATE TABLE departments (
    dept_no     CHAR(4)         NOT NULL,
    dept_name   VARCHAR(40)     NOT NULL,
    PRIMARY KEY (dept_no),
    UNIQUE  KEY (dept_name)
);
3.1.4.6 刪除實體完整性

刪除部門表上的實體完整性。

ALTER TABLE departments
DROP INDEX dept_name;
ALTER TABLE departments
DROP PRIMARY KEY;

3.1.5 思考

  • 所有列級完整性約束都可以改寫為表級完整性約束,而表級完整性約束不一定能改寫為列級完整性約束。請舉例說明。

例如多屬性主碼就只能通過表級完整性約束定義。

  • 什么情況下會違反實體完整性約束,DBMS 將做何種違約處理?請用實驗驗證。

當更新數據時,新的數據可能違反實體完整性約束。此時 DBMS 會拒絕執行。
舉例:略。

3.2 參照完整性實驗

3.2.1 實驗目的

掌握參照完整性的定義和維護方法。

3.2.2 實驗內容和要求

定義參照完整性,定義參照完整性的違約處理,刪除參照完整性。寫出兩種方式定義參照完整性的 SQL 語句:創建表時定義參照完整性、創建表后定義參照完整性。

3.2.3 實驗重點和難點

實驗重點:創建表時定義參照完整性。
實驗難點:參照完整性的違約處理定義。

3.2.4 實驗內容記錄

3.2.4.1 創建表時定義參照完整性

在已有員工表的情況下,定義工資表。

列級參照完整性定義。

CREATE TABLE salaries (
    emp_no      INT             NOT NULL REFERENCES employees (emp_no),
    salary      INT             NOT NULL,
    from_date   DATE            NOT NULL,
    to_date     DATE            NOT NULL,
    PRIMARY KEY (emp_no, from_date)
); 

注:經測試,MYSQL 定義列級外鍵約束無效。

表級參照完整性定義。

注:MYSQL 支持外鍵約束命名。

CREATE TABLE salaries (
    emp_no      INT             NOT NULL,
    salary      INT             NOT NULL,
    from_date   DATE            NOT NULL,
    to_date     DATE            NOT NULL,
    PRIMARY KEY (emp_no, from_date),
    CONSTRAINT FK_empno FOREIGN KEY (emp_no) REFERENCES employees(emp_no)
); 
3.2.4.2 創建表后定義參照完整性
CREATE TABLE salaries (
    emp_no      INT             NOT NULL,
    salary      INT             NOT NULL,
    from_date   DATE            NOT NULL,
    to_date     DATE            NOT NULL,
    PRIMARY KEY (emp_no, from_date)
); 
ALTER TABLE salaries
ADD CONSTRAINT FK_empno
FOREIGN KEY(emp_no) REFERENCES employees(emp_no);
3.2.4.3 定義參照完整性的違約處理

定義兩張表:

CREATE TABLE departments (
  dept_no CHAR(5) NOT NULL,
  dept_name VARCHAR(40) NOT NULL,
  PRIMARY KEY (dept_no)
);
CREATE TABLE emp_dept (
  emp_no CHAR(10) NOT NULL,
  dept_no CHAR(5),
  CONSTRAINT FK_deptno 
  FOREIGN KEY (dept_no) 
  REFERENCES departments(dept_no)
  ON DELETE SET NULL
  ON UPDATE SET NULL,
  PRIMARY KEY(emp_no)
);

注:如果要定義刪除后的行為,則必須不能與已有的限制沖突(如不能定義 NOT NULL)。

插入兩條數據:

INSERT INTO departments 
VALUES('00001', 'Finance');
INSERT INTO emp_dept
VALUES('10000', '00001');

然后刪除被參照表中的元組,之后參照表會變成:

+--------+---------+
| emp_no | dept_no |
+--------+---------+
| 10000  | NULL    |
+--------+---------+
3.2.4.4 刪除參照完整性

刪除 emp_dept 上的外碼。

注:和書中寫法稍有不同。

ALTER TABLE emp_dept
DROP FOREIGN KEY FK_deptno;

3.2.5 思考

  • 對於自引用表,例如課程表(課程號、課程名、先修課程號、學分)中的先修課程號引用該表的課程號,請完成如下任務:
    (1)寫出課程表上的實體完整性和參照完整性。
    (2)在考慮實體完整性約束的條件下,試舉出幾種錄入課程數據的方法。
CREATE TABLE course (
  cno CHAR(4),
  cname VARCHAR(40),
  cpno CHAR(4),
  ccredit SMALLINT
);
ALTER TABLE course
ADD PRIMARY KEY(cno);
ALTER TABLE course
ADD CONSTRAINT FK_cpno
FOREIGN KEY(cpno) REFERENCES course(cno);

在錄入數據時,可以按照引用的順序錄入數據(總是先錄入不存在引用或引用已存在的數據),也可以臨時移除完整性約束,在錄入數據后,再添加完整性約束。

如果數據本身無法滿足約束,則引入約束時會失敗。

比如先加入數據:

INSERT INTO course
VALUES('0001','DataStructure','0002',80);

這一條數據存在依賴問題,此時再引入完整性約束,則會出現無法引入的情況。

3.3 用戶自定義完整性實驗

3.3.1 實驗目的

掌握用戶自定義完整性的定義和維護方法。

3.3.2 實驗內容和要求

針對具體應用語義,選擇 NULL、NOT NULL、DEFAULT、DEFAULT、UNIQUE、CHECK 等,定義屬性上的約束條件。

3.3.3 實驗重點和難點

實驗重點:NULL、NOT NULL、DEFAULT
實驗難點:CHECK

3.3.4 實驗內容記錄

3.3.4.1 定義屬性 NULL、NOT NULL 約束

注:默認約束即為 NULL 約束,即可以為 NULL 。
這里不作示例。

3.3.4.2 定義屬性 DEFAULT 約束

DEFAULT 約束用於給定屬性的默認值,即不提供值的時候自動填充的值。
這里不作示例。

3.3.4.3 定義 UNIQUE 約束

UNIQUE 約束即必須唯一。

3.3.4.4 使用 CHECK 約束條件

CHECK 可以跟表達式,並且可以引用多個屬性。
如:

...
CHECK (from_date <= to_date),
...
CHECK (grade >= 0 AND grade <=100)
...

4 觸發器實驗

4.1 觸發器實驗

4.1 實驗目的

掌握數據庫觸發器的設計和使用方法。

4.2 實驗內容和要求

定義 BEFORE 觸發器和 AFTER 觸發器。能夠理解不同類型觸發器的作用和執行原理,驗證觸發器的有效性。

4.3 實驗重點和難點

實驗重點:觸發器的定義。
實驗難點:利用觸發器實現較為復雜的用戶自定義完整性。

4.4 實驗內容記錄

4.4.1 AFTER 觸發器

AFTER 觸發器可以用來維護一致性。
比如有一張員工總工資表,那么在加入新的工資記錄后,需要更新總工資表。

總工資表:

CREATE TABLE emp_salary_total (
  emp_no INT NOT NULL,
  salary_total INT NOT NULL DEFAULT 0,
  CONSTRAINT FK_empno FOREIGN KEY (emp_no)
  REFERENCES employees(emp_no)
);

建立總工資表后,初始化一次數據:

INSERT INTO emp_salary_total
SELECT emp_no, SUM(salary)
FROM salaries
GROUP BY emp_no;

在總工資表上定義觸發器:

DELIMITER $$
CREATE TRIGGER TRI_salary_total_DELETE
AFTER DELETE ON salaries
FOR EACH ROW
BEGIN
  UPDATE emp_salary_total e
  SET e.salary_total = (
    SELECT SUM(salary)
    FROM salaries s
    WHERE s.emp_no=OLD.emp_no
  )
  WHERE e.emp_no=OLD.emp_no;
END $$

下面進行驗證:
先查詢 10001 的總工資:

SELECT * 
FROM emp_salary_total
WHERE emp_no=10001 $$

得到總工資為:

+--------+--------------+
| emp_no | salary_total |
+--------+--------------+
|  10001 |      1281612 |
+--------+--------------+
MariaDB [employees]> select * from salaries where emp_no=10001$$
+--------+--------+------------+------------+
| emp_no | salary | from_date  | to_date    |
+--------+--------+------------+------------+
|  10001 |  60117 | 1986-06-26 | 1987-06-26 |
|  10001 |  62102 | 1987-06-26 | 1988-06-25 |
|  10001 |  66074 | 1988-06-25 | 1989-06-25 |
|  10001 |  66596 | 1989-06-25 | 1990-06-25 |
|  10001 |  66961 | 1990-06-25 | 1991-06-25 |
|  10001 |  71046 | 1991-06-25 | 1992-06-24 |
|  10001 |  74333 | 1992-06-24 | 1993-06-24 |
|  10001 |  75286 | 1993-06-24 | 1994-06-24 |
|  10001 |  75994 | 1994-06-24 | 1995-06-24 |
|  10001 |  76884 | 1995-06-24 | 1996-06-23 |
|  10001 |  80013 | 1996-06-23 | 1997-06-23 |
|  10001 |  81025 | 1997-06-23 | 1998-06-23 |
|  10001 |  81097 | 1998-06-23 | 1999-06-23 |
|  10001 |  84917 | 1999-06-23 | 2000-06-22 |
|  10001 |  85112 | 2000-06-22 | 2001-06-22 |
|  10001 |  85097 | 2001-06-22 | 2002-06-22 |
|  10001 |  88958 | 2002-06-22 | 9999-01-01 |
+--------+--------+------------+------------+
17 rows in set (0.00 sec)

從中刪除一條數據:

DELETE FROM salaries 
WHERE salary=60117 
AND emp_no=10001 $$

再查詢總工資為:

+--------+--------------+
| emp_no | salary_total |
+--------+--------------+
|  10001 |      1221495 |
+--------+--------------+

符合預期。

在觸發器上不應定義過於復雜(耗時)的動作,例如本例中觸發器執行了全掃描更新。更好的處理方式是利用被刪除的行的信息來增量更新總工資。

DELIMITER $$
CREATE TRIGGER TRI_salary_total_DELETE
AFTER DELETE ON salaries
FOR EACH ROW
BEGIN
  UPDATE emp_salary_total e
  SET e.salary_total = e.salary_total - OLD.salary
  WHERE e.emp_no=OLD.emp_no;
END $$

此時再刪除一條數據,

DELETE FROM salaries 
WHERE salary=62102 
AND emp_no=10001 $$

然后查詢總工資為

+--------+--------------+
| emp_no | salary_total |
+--------+--------------+
|  10001 |      1159393 |
+--------+--------------+

同樣符合預期。

4.4.2 BEFORE 觸發器

BEFORE 觸發器可以用來檢查數據更新的合法性,可以用來實現比斷言更為復雜的檢查(斷言只能定義一個 CHECK 子句)。

例如,加入一個日期檢查,要求 to_date 必須大於 from_date 。

注:MYSQL 拋出異常的寫法和書中給出的不同。

DELIMITER $$
CREATE TRIGGER TRI_salaries_INSERT
BEFORE INSERT ON salaries
FOR EACH ROW
BEGIN
  DECLARE
    v_msg VARCHAR(200);
  IF (NEW.from_date >= NEW.to_date) 
  THEN
    BEGIN
      SET v_msg = 'to_date is EARLIER than from_date!';
      SIGNAL SQLSTATE 'HY000' SET MESSAGE_TEXT = v_msg;
    END;
  END IF;
END; $$

然后插入一條一場數據:

INSERT INTO salaries
VALUES (10001, 15000, '2020-03-01', '2020-01-01');

會得到錯誤:

ERROR 1644 (HY000): to_date is EARLIER than from_date!

5 數據庫設計實驗

5.1 數據庫設計實驗

5.1.1 實驗目的

掌握數據庫設計基本方法級數據庫設計工具。

5.1.2 實驗內容和要求

掌握數據庫設計基本步驟,包括數據庫概念結構設計、邏輯結構設計、物理結構設計、數據庫模式 SQL 語句生成。能夠使用數據庫設計工具進行數據庫設計。

5.1.3 實驗重點和難點

實驗重點:概念結構設計、邏輯結構設計。
實驗難點:邏輯結構設計。邏輯結構設計雖然可以按照一定的規則從概念結構轉換而來,但是由於概念結構通常比較抽象,較少考慮更多細節,因此轉換而成的邏輯結構還需要進一步調整和優化。邏輯結構承接概念結構和物理結構,處於核心地位,因而是數據庫設計的重點,也是難點。

5.1.4 實驗內容記錄

5.1.4.1 要求

設計一個采購、銷售和客戶管理應用數據庫。其中,一個供應商可以供應多種零件,一種零件也可以有多個供應商。一個客戶訂單可以訂購多種供應商供應的零件。客戶和供應商都分屬不同的國家,而國家按世界五大洲八大洋划分地區。請利用 PowerDesigner 或者 ERwin 等數據庫設計工具設計該數據庫。

由於 PowerDesigner 和 ERwin 均不是免費軟件,因此本實驗使用描述的方式構建數據庫。

5.1.4.2 數據庫概念結構設計

首先識別出數據庫中存在的實體。
供應商和零件是多對多的關系,即有供應商和零件。
客戶訂單和零件是一對多的關系,即有訂單和零件。
一個訂單對應多個訂單項,每個訂單項包含零件和供應商,即有訂單項。
國家和地區是多對一的關系,即有國家和地區。
客戶和國家是多對一的關系,即還有客戶。
因此實體大致有供應商、零件、客戶、訂單、訂單項、國家、地區。

可以作出 E-R 圖:

graph TD; GYS-LJ{供應商-零件} ---|m| GYS[供應商] LJ[零件] --- |n| GYS-LJ{供應商-零件} BH1{包含} ---|1| GYS[供應商] LJ[零件] ---|1| BH1{包含} DDX[訂單項] ---|p| BH1{包含} DDX[訂單項] ---|m| BH2{包含} BH2{包含} ---|1| DD[訂單] GM{購買} ---|m| DD[訂單] KH[客戶] ---|1| GM{購買} KH[客戶] ---|m| BH3{包含} BH3{包含} ---|1| GJ[國家] BH4{包含} ---|m| GJ[國家] DQ[地區] ---|1| BH4{包含} GYS[供應商] ---|m| BH5{包含} GJ[國家] ---|1| BH5{包含}
5.1.4.3 數據庫邏輯結構設計

略。將具體的屬性寫出。

6 存儲過程實驗

6.1 存儲過程實驗

6.1.1 實驗目的

掌握 PL/SQL 編程語言,以及數據庫存儲過程的設計和使用方法。

6.1.2 實驗內容和要求

存儲過程定義、存儲過程運行、存儲過程更名、存儲過程刪除、存儲過程的參數傳遞。掌握 PL/SQL 編程語言和編程規范,規范設計存儲過程。

6.1.3 實驗重點和難點

實驗重點:存儲過程定義和運算。
實驗難點:存儲過程的參數傳遞方法。

6.1.4 實驗內容記錄

6.1.4.1 無參數的存儲過程
  • 定義一個存儲過程,更新部門的員工總數統計表。

先定義部門員工總數統計表:

DELIMITER $$
CREATE TABLE dept_emp_count(
  dept_no CHAR(4) NOT NULL,
  dept_name VARCHAR(40) NOT NULL,
  emp_count INT NOT NULL DEFAULT 0,
  PRIMARY KEY(dept_no)
) $$

然后定義更新部門總人數的存儲過程:

DELIMITER $$
CREATE PROCEDURE  pro_cal_dept_emp_count()
BEGIN
  -- 保證行存在
  INSERT INTO dept_emp_count(dept_no, dept_name, emp_count) 
  SELECT DISTINCT de.dept_no, d.dept_name, 0
  FROM dept_emp de, departments d
  WHERE NOT EXISTS (
    SELECT *
    FROM dept_emp_count dec_
    WHERE dec_.dept_no=de.dept_no
  ) 
  AND de.dept_no=d.dept_no;

  -- 更新行的值
  UPDATE dept_emp_count dec_
  SET dec_.emp_count = (
    SELECT COUNT(*)
    FROM dept_emp de
    WHERE dec_.dept_no=de.dept_no
  );
END $$

調用存儲過程進行更新:

DELIMITER $$
CALL pro_cal_dept_emp_count() $$
6.1.4.2 有參數的存儲過程
  • 定義一個存儲過程,更新給定部門的總人數(給定部門名字)。
CREATE PROCEDURE proc_cal_dept_emp_count(dept_name VARCHAR(40))
BEGIN
  -- 保證行存在
  INSERT INTO dept_emp_count (dept_no, dept_name, emp_count)
  SELECT 
  FROM 
END $$

7 數據庫應用開發實驗

8 數據庫設計與應用開發大作業

9 數據庫監視與性能優化實驗

10 數據庫恢復技術實驗

11 並發控制實驗


  1. 參見 Roles Overview https://mariadb.com/kb/en/roles_overview/#:~:text=Roles were introduced in MariaDB 10.0.5. A role,users was by changing each user's privileges individually. ↩︎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM