# MySQL-3
## 數據備份與還原
> - 數據備份還原方式: 數據表備份, 單表數據備份, SQL 備份, 增量備份
### 數據表備份(這把我的MySQL 搞掛了==)
> - 直接找到數據庫文件夾, 複製對應的表結構與數據文件
> - 還原時, 直接導入備份內容
> - 數據表備份根據不同存儲引擎, 會有不同區別
> - 存儲引擎:
> - mysql 存儲數據方式
> - 主要為 innodb 與 myisam, 因為免費
> - innodb
> - 雖然有存儲限制, 但限制數據大小已經大到沒有限制的級別(64TB)
> - innodb 批量插入速度低
> - 主因是 innodb 是行鎖
> - archive 雖然是行鎖, 但是他是在內存中操作的
> - 現實中除非特別節慶, 否則很少需要批量插入數據
> - innodb 佔用空間大的原因在於他是集中式存儲
> - innodb 只有表結構, 其他數據都存在 `ibdate1` 文件中
> - 優點: 效率高
> - 缺點: 算法複雜
> - innodb 的數據表只能在系統創建的數據庫下面, 手動複製不會被識別
> 亦即數據表備份 innodb 無法用
> - myisam 存儲數據方式為單獨分開存儲 (表, 數據, 索引)
> - 數據表備份通常是用於 `myisam` 引擎, 直接複製貼上三個文件即可
> 表, 數據, 索引都是單獨存在, 不會綁定數據庫
> - 但是我一複製貼上, mysql 就掛了...
> - 創建使用 myisam 引擎的表
```sql
mysql> -- 創建一個 myisam 存儲引擎的表
mysql> create table my_myisam (id int) charset utf8 engine = myisam;
Query OK, 0 rows affected, 1 warning (0.01 sec)
```
```shell
$ % tree /usr/local/var/mysql/test2
├── my_class.ibd
├── my_myisam.MYD # 數據
├── my_myisam.MYI # 索引
├── my_myisam_462.sdi # 結構
└── my_stu.ibd
```
> - 手動備份與還原(失敗且害我的 mysql 掛了= =)
```sql
mysql> use test_engine;
Database changed
mysql> show tables;
Empty set (0.00 sec)
```
```shell
$ # 直接去/usr/local/var/mysql/test2 裡ctrl c + v
$ # 複製一組 myisam 跟 一張 innodb 到 test_engine 資料夾
$ tree /usr/local/var/mysql/test_engine
├── my_class.ibd
├── my_myisam.MYD
├── my_myisam.MYI
└── my_myisam_462.sdi
$ # 我一手動複製貼上文件到另外一個數據庫, 我就發現我的 mysql 掛了...
$ mysql.server restart
ERROR! MySQL server PID file could not be found!
Starting MySQL
. ERROR! The server quit without updating PID file
```
### 存儲引擎
||Myisam|innodb|BDB|Memory|Archive|
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|批量插入速度|高|低|高|高|非常高|
|事務安全||支持|支持|||
|全文索引|支持|5.5後支持||||
|鎖|表鎖|行鎖|頁鎖|表鎖|行鎖|
|存儲限制|無|64TB|無|有|無|
|B樹索引|支持|支持|支持|支持||
|Hash索引||支持||支持||
|集群索引||支持||||
|數據緩存||支持||支持||
|索引緩存|支持|支持||支持||
|數據壓縮|支持||||支持|
|空間使用|低|高|低|N/A|非常低|
|緩存使用|低|高|低|中|低|
|外鍵||支持||||
### 單表數據備份 (遇到問題, 沒測試成功)
> - 顧名思義, 每次只能備份一張表, 且備份的內容為數據(沒有表結構)
> - 使用場景: 將表中的數據導出到文件
> #### 基本語句
> `select */字段列表 into outfile '文件所在路徑' from 數據源`
> - 前提: 外部文件不存在
```sql
mysql> select * from my_class;
+----+--------------+------+
| id | name | item |
+----+--------------+------+
| 1 | 三年三班 | JS |
| 2 | 二年四班 | PHP |
| 3 | 一年五班 | C |
+----+--------------+------+
3 rows in set (0.00 sec)
mysql> -- 備份
mysql> select * into outfile '~/Desktop/my_class.txt' from my_class;
mysql> -- 問題: 5.7 後, MySQL 有做了一些限制, 必須去修改 my_cnf 或 my_ini
ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
```
> #### 1290 解法
> - 參考
> - https://blog.csdn.net/fdipzone/article/details/78634992
> - https://ithelp.ithome.com.tw/articles/10197804?sc=rss.qu
> - 原因:
> - 預設 `secure_file_priv` 的參數為 NULL
> - `NULL` : 不允許導出導入
> - `路徑` : 只允許導入到這個路徑中
> - 沒有值 : 沒有限制
```sql
mysql> show global variables like '%secure_file_priv%';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_file_priv | NULL |
+------------------+-------+
1 row in set (0.01 sec)
```
> - 找到 `my_cnf` 檔, 在裡面新增 `secure_file_priv=''` 後重啟 mysql
> - 有陰影啦幹, 先不亂改
```shell
$ find /* -name my.cnf
find: ...
/System/.../usr/local/etc/my.cnf
/System/.../Cellar/mysql/8.0.19/.../my.cnf
find: ...
$ vi /System/.../Cellar/mysql/8.0.19/.../my.cnf
# 在裡面加入 secure_file_priv='' 後重啟 mysql
# 不過我在弄數據表備份時已經把我 MySQL 搞掛過一次了, 先不要亂搞, 放著等看到更多資料再說
```
> #### 高級備份
> `select */字段列表 into outfile '文件所在路徑' [fields 字段處理 lines 行處理] from 數據源`
> - 假設一切正常備份, 應該會出現一個文檔, 以上面那張表為例, 應該會長這樣
```txt
1 三年三班 JS
2 二年四班 PHP
3 一年五班 C
```
> - `fields` : 字段處理
> - `enclosed by` : 字段用什麼內容包裹, 默認為`''` (空字符串)
> - `terminated by` : 字段以什麼結束, 默認為 `\t` (tab)
> - `escaped by` : 特殊符號用什麼方式處理, 默認用 `\\` (反斜線轉譯)
> - `lines` : 行處理
> - `starting by` : 每行以什麼開始, 默認為 `''` (空字符串)
> - `terminated by` : 每行以什麼結束, 默認為 `\n` (換行符)
```sql
mysql> select * into outfile '~/Desktop/my_class.txt'
> fields enclosed by '"' terminated by '|'
> lines starting by 'Start: '
> from my_class;
```
```txt
Start: "1"|"三年三班"|"JS"
Start: "2"|"二年四班"|"PHP"
Start: "3"|"一年五班"|"C"
```
> #### 數據還原
> `load data infile 文件路徑 into table 表名[(自段列表)] [fields 字段處理 lines 行處理]`
> - 將外部數據恢復到表中
> - 表結構必須存在, 因為備份出去的只有數據沒有表結構
> - 怎麼備份的就怎麼還原, 亦即如果備份有特別處理字段跟行, 還原也要寫一次
```sql
mysql> load data infile '~/Desktop/my_class.txt' into table my_class
> fields enclosed by '"' terminated by '|'
> lines starting by 'Start: ';
```
### SQL 備份
> - 備份 SQL 語句:
> - 系統對結構與數據進行處理而全部變成 SQL 語句來備份
> - 還原時執行 SQL 語句即可
> - 優點: 可以備份表結構
> - 缺點:
> - 浪費空間(額外多一堆SQL指令),
> - 備份時間也比較長(一定是整個表備份, 如果原先存在表結構, 該表結構會先被幹掉)
> #### 備份
> - MySQL 沒有提供備份指令, 需要利用 MySQL 提供的 mysqldump
```shell
$ tree /System/Volumes/Data/usr/local/Cellar/mysql/8.0.19/bin/
...
├── mysql # 客戶端
├── mysql.server -> ../support-files/mysql.server
...
├── mysqld # 服務端
...
├── mysqldump # 備份用的客戶端
...
```
> #### mysqldump 備份
> `$ mysqldump [-h電腦] [-p端口] -u帳號 -p密碼 數據庫名 [數據表名1 [數據表名2...]] > 外部文件目錄(最好使用 .sql)`
> - mysqldump 也是一種客戶端, 所以當然需要連接認證
```shell
$ # 整張表備份
$ mysqldump -uroot -p test my_class > ~/Desktop/my_class.sql # 備份
$ tree ~/Desktop/
└── my_class.sql # 備份成功
$ # 整個數據庫備份
$ mysqldump -uroot -p test > ~/Desktop/test.sql
$ tree ~/Desktop/
├── my_class.sql
└── test.sql
$ cat ~/Desktop/my_class.sql # 看一下裡面寫啥
-- MySQL dump 10.13 Distrib 8.0.19, for osx10.15 (x86_64)
--
-- Host: localhost Database: test
-- ------------------------------------------------------
-- Server version 8.0.19
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `my_class`
--
DROP TABLE IF EXISTS `my_class`; -- 如果裡面有同名表, 執行導入時會被幹掉
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `my_class` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`item` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `my_class`
--
LOCK TABLES `my_class` WRITE; -- 寫數據的時候會鎖表(別人不能用)
/*!40000 ALTER TABLE `my_class` DISABLE KEYS */;
INSERT INTO `my_class` VALUES (1,'三年三班','JS'),(2,'二年四班','PHP'),(3,'一年五班','C');
/*!40000 ALTER TABLE `my_class` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2020-01-20 1:08:25
```
#### 還原
> - 還原有兩種方式: mysql 客戶端還原 跟
> #### mysql 客戶端還原
> `$ mysql -hpup 數據庫名 < 備份文件目錄`
```sql
mysql> -- 刪除數據
mysql> use test;
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| my_class |
+----------------+
1 row in set (0.00 sec)
mysql> drop table my_class;
Query OK, 0 rows affected (0.02 sec)
mysql> show tables;
Empty set (0.00 sec)
```
```shell
$ # 還原數據
$ mysql -uroot -p test < ~/Desktop/my_class.sql
```
```sql
mysql> -- 查看成功與否
mysql> use test;
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| my_class |
+----------------+
1 row in set (0.01 sec)
mysql> select * from my_class;
+----+--------------+------+
| id | name | item |
+----+--------------+------+
| 1 | 三年三班 | JS |
| 2 | 二年四班 | PHP |
| 3 | 一年五班 | C |
+----+--------------+------+
3 rows in set (0.00 sec) -- 還原成功
```
> #### SQL 指令備份
> `source 備份SQL文件路徑`
```sql
mysql> -- 刪除數據
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| my_class |
+----------------+
1 row in set (0.00 sec)
mysql> drop table my_class;
Query OK, 0 rows affected (0.02 sec)
mysql> show tables;
Empty set (0.00 sec)
mysq> -- 備份數據
mysql> source ~/Desktop/my_class.sql
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| my_class |
+----------------+
1 row in set (0.00 sec)
mysql> select * from my_class;
+----+--------------+------+
| id | name | item |
+----+--------------+------+
| 1 | 三年三班 | JS |
| 2 | 二年四班 | PHP |
| 3 | 一年五班 | C |
+----+--------------+------+
3 rows in set (0.00 sec)
```
### 增量備份
> - 針對 MySQL 服務器的日誌文件做備份
> - 所有操作都會備份
> - 指定時間開始進行備份
> - 備份數據不會重複
> - 不會刪表備份
> - 上次備份到哪, 下次接著備份
> - 效率較高
## 事務(transaction)
> - 問題: A轉帳給B的過程斷電, A帳戶減少, B帳戶沒有增加
> - 解決: A減少錢並不馬上修改數據表, 待B收到錢後, 同時修改數據表
> - 事務: 一系列要發生的連續操作
> - 轉帳需要A轉給B, 且B收到
> - 領錢需要插卡>驗證身份>選擇領錢>輸入金額>銀行服務器確認你有錢>確認ATM有錢>吐錢>還卡
> - 事務安全: 保護連續操作同時滿足的機制
> - 事務前提: 存儲引擎必須為 `innodb` 或 `BDB`(收費)
> - 事務原理:
> - 事務開啟後, 所有操作都會保存到`事務日誌`
> - `事務日誌` 會有兩種情況清空
> - `commit;` : 提交數據到數據庫後清空
> - `rollback, 關閉連接SQL, 不正常關閉...等` : 直接清空
> - 事務流程
> - 客戶端連接服務器時, 服務器會做三件事
> - 驗證身份
> - 建立連接
> - 開啟臨時事務日誌(當前用戶當次連接)
> - 客戶端直接寫 SQL 時, 服務器會
> - 接收 SQL
> - 執行 SQL
> - 同步到數據庫表(自動事務)
> - 事實上他也是會寫到臨時日誌中
> - 只是 MySQL 預設自動提交(`autocommit`), 所以寫進去馬上就提交了
> - 查詢操作也會直接從數據庫表拿數據後返回
> - 客戶端開啟事務 (`start transaction;`)
> - 服務器會改變原來的機制, 所有寫入操作都會先寫道臨時事務日誌中
> - 寫入操作
> - 接收 SQL
> - 執行 SQL
> - 將結果寫入臨時事務日誌
> - 查詢操作
> - 從數據表拿數據
> - 將數據拿到臨時日誌加工
> (所以用戶操作時會看到修改後數據, 但實際上沒有改到數據庫)
> - 返回數據
> - 事務結束 (`commit`, `rollback`, 斷開連接)
> - `commit` : 將臨時日誌數據提交數據庫, 清空臨時日誌
> - `rollvack` 或 斷開SQL 連接: 清空臨時日誌
> - 事務操作是針對數據操作, 千萬別以為開事務寫 drop table 不用坐牢~~
```sql
mysql> create table my_count(
-> id int primary key auto_increment,
-> num char(16) not null unique comment '帳戶',
-> name varchar(20) not null,
-> money decimal(10,2) default 0.0 comment '餘額') charset utf8;
Query OK, 0 rows affected, 1 warning (0.01 sec)
mysql> insert into my_count values(
-> null, '0000000000000001', '小明', 1000),(
-> null, '0000000000000002', '小花', 2000);
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> select * from my_count;
+----+---------------+--------+---------+
| id | num | name | money |
+----+---------------+--------+---------+
| 1 | 0000000000001 | 小明 | 1000.00 |
| 2 | 0000000000002 | 小花 | 2000.00 |
+----+---------------+--------+---------+
2 rows in set (0.00 sec)
mysql> -- 沒有事務操作而遇到停電時
mysql> update my_count set money = money - 1000 where id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> exit; -- 扣了一千就結束
Bye
```
### 事務操作
> - 事務操作分為自動事務(默認)跟手動事務
#### 手動事務
> - 開啟事務
> `start transaction`
> - 告訴系統以下操作先別寫到數據庫,
> - 而是先放到事務日誌(`ib_logfile()` 與 `ib_logfile1`)
```sql
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
```
> - 事務操作
> - 事務操作時, 並不會真的修改數據庫
> 
```sql
mysql> select * from my_count;
+----+---------------+--------+---------+
| id | num | name | money |
+----+---------------+--------+---------+
| 1 | 0000000000001 | 小明 | 1000.00 |
| 2 | 0000000000002 | 小花 | 2000.00 |
+----+---------------+--------+---------+
2 rows in set (0.00 sec)
mysql> update my_count set money = money - 1000 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update my_count set money = money + 1000 where id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
> - 關閉事務
> - 選擇將日誌文件保存到數據庫或直接清空
> - 提交事務:
> `commit;`
> - 提交後, 別人就會看到你修改後的數據
> 
> - 回滾事務(不提交直接清掉)
> `rollback;`
> - rollback 後, 啥事都當作沒發生
```sql
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from my_count;
+----+---------------+--------+---------+
| id | num | name | money |
+----+---------------+--------+---------+
| 1 | 0000000000001 | 小明 | 0.00 |
| 2 | 0000000000002 | 小花 | 3000.00 |
+----+---------------+--------+---------+
2 rows in set (0.00 sec)
```
### 回滾點
> - 在某個成功操作的數據結果標記一個記憶點, 以提供後續失敗操作的返回點, 而不是全部返回, 該點就是回滾點
> - 設置回滾點
> `savepoint 回滾點名字`
> - 回到回滾點
> `rollback to 回滾點名字`
> - 測試:
> - 給小明 20000
> - 扣手續費五趴卻扣到小花
```sql
mysql> select * from my_count;
+----+---------------+--------+---------+
| id | num | name | money |
+----+---------------+--------+---------+
| 1 | 0000000000001 | 小明 | 0.00 |
| 2 | 0000000000002 | 小花 | 3000.00 |
+----+---------------+--------+---------+
2 rows in set (0.00 sec)
mysql> -- 開啟事務
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> -- 給小明 20000
mysql> update my_count set money = money + 20000 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 20000.00 |
| 2 | 0000000000002 | 小花 | 3000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
mysql> -- 設定回滾
mysql> savepoint sp1;
Query OK, 0 rows affected (0.00 sec)
mysql> -- 扣手續費扣錯人
mysql> update my_count set money = money - 20000 * 0.05 where id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 20000.00 |
| 2 | 0000000000002 | 小花 | 2000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
mysql> -- 回到回滾
mysql> rollback to sp1;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 20000.00 |
| 2 | 0000000000002 | 小花 | 3000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
mysql> -- 扣對人
mysql> update my_count set money = money - 20000 * 0.05 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 19000.00 |
| 2 | 0000000000002 | 小花 | 3000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
mysql> -- 提交
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
```
### 自動事務(autocommit)
> - MySQL 默認都是自動事務, 用戶操作後會立即同步到數據庫表中
> - 系統透過 `autocommit` 控制是否自動提交
> #### 查看 `autocommit` 狀態
> `show variables like 'autocommit';`
```sql
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)
```
> #### 開關自動事務
> - 關閉: `set autocommit = off/0;`
> - 開啟: `set autocommit = on/1;`
```sql
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)
```
> - 再次執行操作時, 就不會自動提交了, 必須手動提交
> 
```sql
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 19000.00 |
| 2 | 0000000000002 | 小花 | 3000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
mysql> -- 關閉自動提交後, 必須手動提交數據才能同步到數據庫表中
mysql> update my_count set money = money - 9000 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 10000.00 |
| 2 | 0000000000002 | 小花 | 3000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
```
### 事務特性(ACID)
> #### A (Atomic) :
> - 原子性, 事務操作是一個整體, 不可分割(要馬成功要馬失敗)
> #### C (Consistency) :
> - 一致性, 數據操作前後, 數據表中沒有變化(commit才會改)
> #### I (Isolation) :
> - 隔離性, 事務操作互相隔離而不受影響
> - 當客戶端開啟事務後, 數據就會被拿到臨時事務文件處理, 此時外面怎樣修改都不影響自己的數據
> - 開啟事務時,
> - 查詢操作只會去數據庫表拿一次數據而放在臨時文件中
> - 操作數據時, 會再次去拿數據來操作, 而非剛剛查詢的那筆數據
```sql
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 21000.00 |
| 2 | 0000000000002 | 小花 | 5000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
mysql> -- A 客戶端開啟事務 -------------------------------
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
```
```sql
mysql> -- 此時 B 客戶端馬上修改資料
mysql> update my_count set money = 3000 where id = 2;
```
```sql
mysql> -- A 客戶端查詢操作會拿到最新的數據(他會拿到臨時文件)
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 21000.00 |
| 2 | 0000000000002 | 小花 | 3000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
```
```sql
mysql> -- 但是在A客戶端拿過數據之後 B客戶端再改資料
mysql> update my_count set money = 4000 where id = 2;
```
```sql
mysql> -- 此時結果證明 A 客戶端不會再去數據庫拿資料, 而是直接使用剛剛拿的
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 21000.00 |
| 2 | 0000000000002 | 小花 | 3000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
mysql> -- 但是此時如果 A 客戶端是操作數據時, 會再次去數據庫表拿數據來操作
mysql> update my_count set money = money - 3000 where id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 21000.00 |
| 2 | 0000000000002 | 小花 | 1000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
mysql> -- 字面上減 3000 應該為 0, 但事實上還有一千, 證明他還有去拿一次最新的(4000)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
```
> - 鎖機制:
> - `innodb` 默認為行鎖(操作哪一行時, 別人就不能操作該行)
> - 但如沒用到索引, 系統會自動轉為表鎖(操作某張表時, 別人不能操作該表)
> - 行鎖 (鎖住索引行)
```sql
mysql> select * from my_count;
+----+---------------+--------+----------+
| id | num | name | money |
+----+---------------+--------+----------+
| 1 | 0000000000001 | 小明 | 21000.00 |
| 2 | 0000000000002 | 小花 | 1000.00 |
+----+---------------+--------+----------+
2 rows in set (0.00 sec)
mysql> -- A 客戶端開啟事務
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> -- 操作有用到索引
mysql> update my_count set money = money - 1000 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
```sql
mysql> -- 此時B客戶端如果也操作該行, 會等一段時間, 如果A表都沒提交, 就會報錯
mysql> update my_count set money = money + 2000 where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> -- 但是操作其他行就不會有問題, 因為使用事務操作索引數據只會行鎖
mysql> update my_count set money = money + 2000 where id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
> - 表鎖 (鎖住整張表)
```sql
mysql> -- A 客戶端開啟事務
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> -- 操作時沒有用到索引時
mysql> update my_count set money = money - 2000 where name = '小明';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
```sql
mysql> -- B 客戶端整張表都不能動了, 因為會改成表鎖
mysql> update my_count set money = money + 2000 where id = 2;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
```
> #### D (Durability) :
> - 持久性, 數據一提交, 永久改變數據
## 變量
### 系統變量
> - 系統定義好的變量
> - 系統用來控制服務器所使用的
> #### 查看系統變量
> `show variables;`
> #### 查看具體變量值
> `select @@變量名[,@@變量名[,@@...]];`
> - 任何有數據返回的都是用 select 查看
```sql
mysql> select @@version, @@autocommit;
+-----------+--------------+
| @@version | @@autocommit |
+-----------+--------------+
| 8.0.19 | 1 |
+-----------+--------------+
1 row in set (0.00 sec)
```
> #### 修改系統變量
> - 會話級別(臨時修改)
> `set 變量名 = 值;` 或 `set @@變量名 = 值;`
> - 當前客戶端當次連接有效
> <img src='https://i.imgur.com/4vrfiL5.png' style='width: 400px'/>
> - 全局級別
> `set global 變量名 = 值`
> 
> - 必須退出重登後才生效, 當次連接尚未生效
> 
### 自定義變量
> - 所有自定義變量為會話級別(當次連接有效), 因為他只保存在內存中
> - 自定義變量屬於用戶級別, 不認數據庫, 在哪都能叫
> #### 定義變量
>`set @變量名 = 值;` 或 `set @變量名 := 值`
> - 系統為了區分系統變量, 規定自定義變量使用一個 @
> - MySQL 的 `=` 通常為比較符, MySQL 因此創造了 `:=` 來表達賦值符
```sql
mysql> set @name = '小美';
Query OK, 0 rows affected (0.00 sec)
mysql> set @age := 18;
Query OK, 0 rows affected (0.00 sec)
```
> #### 查看變量
> `select @變量名;`
```sql
mysql> select @name;
+--------+
| @name |
+--------+
| 小美 |
+--------+
1 row in set (0.00 sec)
```
> #### select 賦值1
> `select @變量名 := 字段名 from 數據源`
> - 從字段取值給變量名
> - 如果寫成 `@變量名 = 字段名` 會變成比較符而輸出 0 跟 1, 沒有賦值效果
> - 這方法會顯示 Select Return
```sql
mysql> select * from my_class;
+----+--------------+------+
| id | name | item |
+----+--------------+------+
| 1 | 三年三班 | JS |
| 2 | 二年四班 | PHP |
| 3 | 一年五班 | C |
+----+--------------+------+
3 rows in set (0.00 sec)
mysql> select @name = name from my_class;
+--------------+
| @name = name |
+--------------+
| 0 | -- 比較 @name(小美) 是否 = name(三年三班) => 0
| 0 | -- 比較 @name(小美) 是否 = name(二年四班) => 0
| 0 | -- 比較 @name(小美) 是否 = name(一年五班) => 0
+--------------+
3 rows in set (0.00 sec)
mysql> select @name;
+--------+
| @name |
+--------+
| 小美 | -- 沒有賦值
+--------+
1 row in set (0.00 sec)
```
```sql
mysql> select @name := name from my_class;
+---------------+
| @name := name |
+---------------+
| 三年三班 | -- 跑第一圈時, @name 被賦值成 '三年三班'
| 二年四班 |
| 一年五班 | -- 跑第三圈時, @name 被賦值成 '一年五班'
+---------------+
3 rows in set, 1 warning (0.00 sec)
mysql> select @name;
+--------------+
| @name |
+--------------+
| 一年五班 |
+--------------+
1 row in set (0.00 sec)
```
> #### select 賦值2
> `select 自段列表 from 數據源 [where] into 變量列表;`
> - 嚴格要求數據只能有一條(MySQL不支持陣列)
> - 不會顯示 select Return
```sql
mysql> -- 多條數據會報錯
mysql> select name, item from my_class into @name, @item;
ERROR 1172 (42000): Result consisted of more than one row
mysql> -- 單條數據沒事兒
mysql> select name, item from my_class where id = 1 into @name, @item;
Query OK, 1 row affected (0.01 sec)
mysql> select @name, @item;
+--------------+-------+
| @name | @item |
+--------------+-------+
| 三年三班 | JS |
+--------------+-------+
1 row in set (0.00 sec)
```
## 觸發器 (trigger)
> - 事先為表綁定一段 code, 當表中某些內容改變時(增刪改), 執行該段代碼
> - 說白了就是類似 JS 的事件
> - 一張表只能擁有一種觸發時間,
> 一種觸發時間只能擁有一種事件類型,
> 故最多6種觸發器(`before`, `after`) x (`增`, `刪`, `改`)
> - 觸發時間: `before` 或 `after`
> - 事件類型: `insert` 或 `delete` 或 `update`
> - 觸發對象: 每一條數據(行)
> - 千萬記得, 避免死循環
```sql
create trigger tmp after update on my_order for each row -- 訂單更新數據時觸發
begin
update my_order set .....; -- 觸發內容是更新數據 => 死循環 GG
end
```
### 創建觸發器
> - MySQL 中沒有大括號, 都是利用對應字符符號代替
```sql
delimiter 自定義符號 -- 後續代碼碰到此符號才算結束
create trigger 觸發器名字 觸發時間 事件類型 on 表名 for each row
begin -- 觸發器開始
xxx; -- 觸發器內容
end -- 觸發器結束
自定義符 -- 表示結束
delimiter ; -- 把結束符改回來
```
> #### 修改語句結束符
> - 預設語句結束符為 `;`, 但是在這裡, 觸發器內容也必須寫 `;` 來表達一條語句結束
> 但是實際上還沒結束整個觸發器, 所以必須在創建觸發器之前修改結束符,
> 讓系統看到 `;` 不會以為結束而報錯
```sql
mysql> ; -- 預設結束符
ERROR:
No query specified
mysql> delimiter $$ -- 修改結束符為 $$
mysql> $$
ERROR:
No query specified
mysql> ; -- 因為結束符修改過了, 所以這裡沒有結束
-> $$ -- 而是這裡結束
ERROR 1065 (42000): Query was empty
mysql> delimiter ; -- 改回來
mysql> ;
ERROR:
No query specified
```
```sql
mysql> -- 創建一些測試用數據
mysql> select * from my_goods;
+----+--------+-------+------+
| id | name | price | inv |
+----+--------+-------+------+
| 1 | apple | 20.00 | 100 |
| 2 | banana | 25.00 | 200 |
+----+--------+-------+------+
2 rows in set (0.00 sec)
mysql> select * from my_order;
Empty set (0.00 sec)
mysql> delimiter | -- 臨時結束符
mysql> create trigger after_order after insert on my_order for each row -- 觸發語句
-> begin -- 觸發開始 ( 類似 '{' )
-> update my_goods set inv = inv - 1 where id = 2; -- 綁定的內容
-> -- 在這裡如果沒有臨時結束符, 系統會以為; 代表結束而結束這東西, 故必須改結束符
-> end -- 觸發結束 ( 類似 '}' )
-> | -- 真正的結束符
Query OK, 0 rows affected (0.01 sec)
mysql> delimiter ;
mysql> -- 整個事件就是在說,
mysql> -- 當每次 my_order 有數據插入後, my_goods 的第二項商品(香蕉)數量 -1
```
### 查看觸發器
> #### 查看所有觸發器(模糊查詢)
> `show triggers [like 'pattern'];`
```sql
mysql> show triggers\G
*************************** 1. row ***************************
Trigger: after_order
Event: INSERT
Table: my_order
Statement: begin
update my_goods set inv = inv - 1 where id = 2;
end
Timing: AFTER
Created: 2020-01-20 17:08:34.50
sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
Definer: root@localhost
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ci
Database Collation: utf8mb4_general_ci
1 row in set (0.00 sec)
```
> #### 查看觸發器創建語句
> `show create trigger 觸發器名;`
```sql
mysql> show create trigger after_order\G
*************************** 1. row ***************************
Trigger: after_order
sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
SQL Original Statement: CREATE DEFINER=`root`@`localhost` TRIGGER `after_order` AFTER INSERT ON `my_order` FOR EACH ROW begin
update my_goods set inv = inv - 1 where id = 2;
end
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ci
Database Collation: utf8mb4_general_ci
Created: 2020-01-20 17:08:34.50
1 row in set (0.00 sec)
```
> #### information_schema.triggers
> `select * from information_schema.triggers;`
> - 所有的觸發器都會存在 `information_schema.triggers` 中
```sql
mysql> select * from information_schema.triggers\G
*************************** 1. row ***************************
TRIGGER_CATALOG: def
TRIGGER_SCHEMA: sys
TRIGGER_NAME: sys_config_insert_set_user
EVENT_MANIPULATION: INSERT
EVENT_OBJECT_CATALOG: def
EVENT_OBJECT_SCHEMA: sys
EVENT_OBJECT_TABLE: sys_config
ACTION_ORDER: 1
ACTION_CONDITION: NULL
ACTION_STATEMENT: BEGIN
IF @sys.ignore_sys_config_triggers != true AND NEW.set_by IS NULL THEN
SET NEW.set_by = USER();
END IF;
END
ACTION_ORIENTATION: ROW
ACTION_TIMING: BEFORE
ACTION_REFERENCE_OLD_TABLE: NULL
ACTION_REFERENCE_NEW_TABLE: NULL
ACTION_REFERENCE_OLD_ROW: OLD
ACTION_REFERENCE_NEW_ROW: NEW
CREATED: 2020-01-18 00:19:06.82
SQL_MODE: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
DEFINER: mysql.sys@localhost
CHARACTER_SET_CLIENT: utf8mb4
COLLATION_CONNECTION: utf8mb4_0900_ai_ci
DATABASE_COLLATION: utf8mb4_0900_ai_ci
*************************** 2. row ***************************
TRIGGER_CATALOG: def
TRIGGER_SCHEMA: sys
TRIGGER_NAME: sys_config_update_set_user
EVENT_MANIPULATION: UPDATE
EVENT_OBJECT_CATALOG: def
EVENT_OBJECT_SCHEMA: sys
EVENT_OBJECT_TABLE: sys_config
ACTION_ORDER: 1
ACTION_CONDITION: NULL
ACTION_STATEMENT: BEGIN
IF @sys.ignore_sys_config_triggers != true AND NEW.set_by IS NULL THEN
SET NEW.set_by = USER();
END IF;
END
ACTION_ORIENTATION: ROW
ACTION_TIMING: BEFORE
ACTION_REFERENCE_OLD_TABLE: NULL
ACTION_REFERENCE_NEW_TABLE: NULL
ACTION_REFERENCE_OLD_ROW: OLD
ACTION_REFERENCE_NEW_ROW: NEW
CREATED: 2020-01-18 00:19:06.82
SQL_MODE: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
DEFINER: mysql.sys@localhost
CHARACTER_SET_CLIENT: utf8mb4
COLLATION_CONNECTION: utf8mb4_0900_ai_ci
DATABASE_COLLATION: utf8mb4_0900_ai_ci
*************************** 3. row ***************************
TRIGGER_CATALOG: def
TRIGGER_SCHEMA: test
TRIGGER_NAME: after_order
EVENT_MANIPULATION: INSERT
EVENT_OBJECT_CATALOG: def
EVENT_OBJECT_SCHEMA: test
EVENT_OBJECT_TABLE: my_order
ACTION_ORDER: 1
ACTION_CONDITION: NULL
ACTION_STATEMENT: begin
update my_goods set inv = inv - 1 where id = 2;
end
ACTION_ORIENTATION: ROW
ACTION_TIMING: AFTER
ACTION_REFERENCE_OLD_TABLE: NULL
ACTION_REFERENCE_NEW_TABLE: NULL
ACTION_REFERENCE_OLD_ROW: OLD -- 觸發器紀錄
ACTION_REFERENCE_NEW_ROW: NEW -- 觸發器紀錄
CREATED: 2020-01-20 17:08:34.50
SQL_MODE: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
DEFINER: root@localhost
CHARACTER_SET_CLIENT: utf8mb4
COLLATION_CONNECTION: utf8mb4_general_ci
DATABASE_COLLATION: utf8mb4_general_ci
3 rows in set (0.01 sec)
```
### 觸發觸發器
> - 當綁定事件觸發時, 就會觸發觸發器
```sql
mysql> select * from my_goods;
+----+--------+-------+------+
| id | name | price | inv |
+----+--------+-------+------+
| 1 | apple | 20.00 | 100 |
| 2 | banana | 25.00 | 200 |
+----+--------+-------+------+
2 rows in set (0.00 sec)
mysql> select * from my_order;
Empty set (0.01 sec)
mysql> desc my_order;
+-------+------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| g_id | int | NO | | NULL | |
| g_num | int | NO | | NULL | |
+-------+------+------+-----+---------+----------------+
3 rows in set (0.01 sec)
mysql> -- 當訂單數據插入後, my_goods 的香蕉會 -1
mysql> insert into my_order values (null, 1,1);
Query OK, 1 row affected (0.01 sec)
mysql> select * from my_goods;
+----+--------+-------+------+
| id | name | price | inv |
+----+--------+-------+------+
| 1 | apple | 20.00 | 100 |
| 2 | banana | 25.00 | 199 | -- 香蕉 -1
+----+--------+-------+------+
2 rows in set (0.00 sec)
mysql> select * from my_order;
+----+------+-------+
| id | g_id | g_num |
+----+------+-------+
| 1 | 1 | 1 | -- 數據成功插入
+----+------+-------+ -- 雖然有點問題, 但這只是證明觸發器有成功觸發
1 row in set (0.00 sec)
```
### 修改觸發器 & 刪除觸發器
> - 觸發器只能刪除後新增
> #### 刪除觸發器
> `drop trigger 觸發器名`
```sql
mysql> drop trigger after_order;
Query OK, 0 rows affected (0.01 sec)
mysql> show triggers\G
Empty set (0.00 sec)
```
### 觸發器紀錄
> - 不管觸發器是否觸發, 當某種操作準備執行時, 系統就會將當前的狀態與即將執行的狀態記錄下來, 記錄在
> - `ACTION_REFERENCE_OLD_ROW` : 紀錄當前紀錄
> - `ACTION_REFERENCE_NEW_ROW` : 紀錄即將執行的新紀錄(假設會執行的結果)
> - 刪除沒有NEW, 新增沒有OLD
> - NEW 與 OLD 都代表紀錄本身, 除了數據還有字段
> - 使用方式:
> `old.字段名 / new.字段名`
```sql
mysql> select * from my_goods;
+----+--------+-------+------+
| id | name | price | inv |
+----+--------+-------+------+
| 1 | apple | 20.00 | 100 |
| 2 | banana | 25.00 | 199 |
+----+--------+-------+------+
2 rows in set (0.00 sec)
mysql> select * from my_order;
+----+------+-------+
| id | g_id | g_num |
+----+------+-------+
| 1 | 1 | 1 |
+----+------+-------+
1 row in set (0.00 sec)
mysql> -- 寫觸發器
mysql> delimiter |
mysql> create trigger order_after after insert on my_order for each row
-> begin
-> -- INSERT 只有 NEW, 代表新的紀錄
-> -- 中文應該是: 當新增一條數據時, 訂單購買的品項數量減少訂單購買數量
-> update my_goods set inv = inv - new.g_num where id = new.g_id;
-> end
-> |
Query OK, 0 rows affected (0.01 sec)
mysql> delimiter ;
mysql> -- 觸發觸發器, 買蘋果三十個
mysql> insert into my_order value (null, 1,30);
Query OK, 1 row affected (0.01 sec)
mysql> select * from my_order;
+----+------+-------+
| id | g_id | g_num |
+----+------+-------+
| 1 | 1 | 1 |
| 2 | 1 | 30 | -- 買蘋果三十個
+----+------+-------+
2 rows in set (0.00 sec)
mysql> select * from my_goods;
+----+--------+-------+------+
| id | name | price | inv |
+----+--------+-------+------+
| 1 | apple | 20.00 | 70 | -- 扣蘋果三十個 !
| 2 | banana | 25.00 | 199 | -- 剛剛不管買啥都是扣香蕉一個!
+----+--------+-------+------+
2 rows in set (0.00 sec)
```
```sql
mysql> select trigger_name,ACTION_REFERENCE_OLD_ROW,action_reference_new_row from information_schema.triggers;
+----------------------------+--------------------------+--------------------------+
| TRIGGER_NAME | ACTION_REFERENCE_OLD_ROW | ACTION_REFERENCE_NEW_ROW |
+----------------------------+--------------------------+--------------------------+
| sys_config_insert_set_user | OLD | NEW |
| sys_config_update_set_user | OLD | NEW |
| order_after | OLD | NEW | -- 有狀態, 但 INSERT 的 OLD 其實為空
+----------------------------+--------------------------+--------------------------+
3 rows in set (0.00 sec)
```
## 代碼執行結構
> - 順序結構, 分支結構, 循環結構
### 分支結構
> - MySQL 只有 if 分支
```sql
if 條件 then
-- 滿足條件要幹嘛
[else]
-- 否則要幹嘛
end if; -- 結束 if 判斷
```
> - 新增訂單前, 先判斷庫存是否足夠
```sql
mysql> delimiter ^^
mysql> create trigger before_order before insert on my_order for each row
-> begin
-> -- 獲取庫存
-> select inv from my_goods where id = new.g_id into @g_inv;
-> -- 判斷庫存是否足夠
-> if @g_inv < new.g_num then
-> -- 如果庫存不夠, 由於觸發器沒有阻止事件發生的方法, 所以只能暴力報錯來阻止
-> insert into OOOOOOOO values (XXXXXXX);
-> end if;
-> end
-> ^^
Query OK, 0 rows affected (0.02 sec)
mysql> delimiter ;
mysql> -- 查看效果
mysql> select * from my_goods;
+----+--------+-------+------+
| id | name | price | inv |
+----+--------+-------+------+
| 1 | apple | 20.00 | 69 |
| 2 | banana | 25.00 | 199 |
+----+--------+-------+------+
2 rows in set (0.00 sec)
mysql> select * from my_order;
+----+------+-------+
| id | g_id | g_num |
+----+------+-------+
| 1 | 1 | 1 |
| 2 | 1 | 1 |
| 3 | 1 | 30 |
+----+------+-------+
3 rows in set (0.00 sec)
mysql> -- 新增一筆一千顆蘋果的訂單
mysql> insert into my_order values (null, 1, 1000);
ERROR 1146 (42S02): Table 'test.OOOOOOOO' doesn't exist -- 報錯
mysql> -- 訂單沒有增加
mysql> select * from my_order;
+----+------+-------+
| id | g_id | g_num |
+----+------+-------+
| 1 | 1 | 1 |
| 2 | 1 | 1 |
| 3 | 1 | 30 |
+----+------+-------+
3 rows in set (0.01 sec)
mysql> -- 當然也就沒有觸發剛剛定義的 after_order 觸發器
mysql> select * from my_goods;;
+----+--------+-------+------+
| id | name | price | inv |
+----+--------+-------+------+
| 1 | apple | 20.00 | 69 |
| 2 | banana | 25.00 | 199 |
+----+--------+-------+------+
2 rows in set (0.00 sec)
```
### 循環結構
> - 某段代碼在指定條件下重複執行
> #### 基本語法
```sql
while 條件判斷 do
-- 滿足條件要執行啥
-- 變更循環條件(想辦法停止循環)
end while;
```
> #### 循環控制
> `iterate/leave 循環名字`
> - MySQL 中沒有 `continue` 跟 `break`
> - `iterate` : 迭代, 類似 `continue` > 後面代碼不執行, 跳下一次循環
> - `leave` : 離開, 類似 `break` > 跳出循環
> - 既然循環控制後面需要接循環名字, 當然就要先定義循環名字
```sql
循環名字: while 條件 do -- 定義循環名字
-- 循環體
-- 循環控制
iterate/leave 循環名字 -- 循環控制循環名字
end while;
```
## 函數
> - 將一段代碼封裝到一個結構中, 需要執行時, 調用結構執行即可(代碼復用)
### 系統函數
> - 任何函數都有返回值
> - 任何有數據返回的, 都是用 `select` 查看
> - 所以函數是用 `select` 調用
> - MySQL 中, 字符串基本操作通常以`字符`為單位
> - 有些語言會以字節(`Byte`), 有些語言會以位(`bit`) 為單位
> - 以 `Byte` 或 `bit` 都容易有亂碼的情形發生
> - 以字符為單位來操作就不會有亂碼發生
#### string function
> #### substring
> `substring(str, pos, len)`
> - MySQL 下標從 1 開始
> - 中文沒有亂碼, 證明以字符擷取
```sql
mysql> set @tmp1 = 'GODJJ';
Query OK, 0 rows affected (0.00 sec)
mysql> set @tmp2 = '八八包';
Query OK, 0 rows affected (0.00 sec)
mysql> select substring(@tmp1, 1,1);
+-----------------------+
| substring(@tmp1, 1,1) |
+-----------------------+
| G |
+-----------------------+
1 row in set (0.00 sec)
mysql> select substring(@tmp2, 1,1);
+-----------------------+
| substring(@tmp2, 1,1) |
+-----------------------+
| 八 |
+-----------------------+
1 row in set (0.00 sec)
```
> #### char_length & length
> - `char_length(str)`: 字符長度
> - `length(str)`: 字節長度
```sql
mysql> select char_length(@tmp1), char_length(@tmp2);
+--------------------+--------------------+
| char_length(@tmp1) | char_length(@tmp2) |
+--------------------+--------------------+
| 5 | 3 |
+--------------------+--------------------+
1 row in set (0.01 sec)
mysql> select length(@tmp1), length(@tmp2);
+---------------+---------------+
| length(@tmp1) | length(@tmp2) |
+---------------+---------------+
| 5 | 9 |
+---------------+---------------+
1 row in set (0.00 sec)
```
> - 中文3字符9字節, 因為當前環境為 `utf8`: 一個中文佔 3B
> - 英文存儲會進行優化, 最終一個英文只佔 1B
> - 但是分配單位還是必須以至少 15B(5*3B) 來分配,
> - 因為必須保證可以插入中文
> #### instr
> `instr(str, substr)`
> - 判斷字符串是否在某個字符串中存在, 返回位置
> - 起始位置為 1
> - 0 表示沒找到
```sql
mysql> select instr(@tmp1, 'DJ'), instr(@tmp2, '包'), instr('喜八路媽', 'WOW');
+--------------------+---------------------+------------------------------+
| instr(@tmp1, 'DJ') | instr(@tmp2, '包') | instr('喜八路媽', 'WOW') |
+--------------------+---------------------+------------------------------+
| 3 | 3 | 0 |
+--------------------+---------------------+------------------------------+
1 row in set (0.00 sec)
```
> #### LPAD & RPAD
> `lpad(str, len, padstr)`
> - 用 `padstr` 將 `str` 填滿到 `len` 的長度
> - 如果原本 `str` 就超過 `len`, 那會被縮減到 `len` 的長度
```sql
mysql> select lpad(@tmp1, 10, 'abc'), lpad(@tmp2, 10, '一二三');
+------------------------+--------------------------------+
| lpad(@tmp1, 10, 'abc') | lpad(@tmp2, 10, '一二三') |
+------------------------+--------------------------------+
| abcabGODJJ | 一二三一二三一八八包 |
+------------------------+--------------------------------+
1 row in set (0.02 sec)
mysql> select rpad(@tmp1, 10, 'abc'), rpad(@tmp2, 10, '一二三');
+------------------------+--------------------------------+
| rpad(@tmp1, 10, 'abc') | rpad(@tmp2, 10, '一二三') |
+------------------------+--------------------------------+
| GODJJabcab | 八八包一二三一二三一 |
+------------------------+--------------------------------+
1 row in set (0.01 sec)
mysql> -- 截斷都是從右截斷
mysql> select rpad(@tmp1, 2, 'abc'), rpad(@tmp2, 2, '一二三');
+-----------------------+-----------------------------+
| rpad(@tmp1, 2, 'abc') | rpad(@tmp2, 2, '一二三') |
+-----------------------+-----------------------------+
| GO | 八八 |
+-----------------------+-----------------------------+
1 row in set (0.00 sec)
mysql> select lpad(@tmp1, 2, 'abc'), lpad(@tmp2, 2, '一二三');
+-----------------------+-----------------------------+
| lpad(@tmp1, 2, 'abc') | lpad(@tmp2, 2, '一二三') |
+-----------------------+-----------------------------+
| GO | 八八 |
+-----------------------+-----------------------------+
1 row in set (0.00 sec)
```
> #### insert
> `insert(str, pos, len, newstr)`
> - 替換, 找到目標位置, 指定要被替換的長度, 用指定字符串替換
> - 在 `str` 的第 `pos` 位置開始用 `newstr` 替換 `len` 個字符串
```sql
mysql> select insert(@tmp1, 3, 1 ,'t');
+--------------------------+
| insert(@tmp1, 3, 1 ,'t') |
+--------------------------+
| GOtJJ | -- 在`GODJJ`的第3位開始用`t`替換1個字符
+--------------------------+
1 row in set (0.00 sec)
mysql> -- 替換的字數如果超過剩餘的字數也沒差
mysql> select insert(@tmp1, 3, 5,'t');
+-------------------------+
| insert(@tmp1, 3, 5,'t') |
+-------------------------+
| GOt | -- 在`GODJJ`的第3位開始用`t`替換5個字符
+-------------------------+
1 row in set (0.00 sec)
mysql> -- 但如果找不到 pos 的字符, 會返回原字符串
mysql> select insert(@tmp1, 10, 5,'t');
+--------------------------+
| insert(@tmp1, 10, 5,'t') |
+--------------------------+
| GODJJ |
+--------------------------+
1 row in set (0.00 sec)
```
> #### strcmp
> `strcmp(str, str)`
> - cmp = compare, 字符集比較
> - 一參跟二參比, 從左至右每位相比編碼大小
> - 小者 -1
> - 相等 0
> - 大者 1
> - 區不區分大小寫是以校對集而定
```sql
mysql> select strcmp('abc', 'bcde'), strcmp('abc', 'ABC'), strcmp('bcde', 'abc');
+-----------------------+----------------------+-----------------------+
| strcmp('abc', 'bcde') | strcmp('abc', 'ABC') | strcmp('bcde', 'abc') |
+-----------------------+----------------------+-----------------------+
| -1 | 0 | 1 |
+-----------------------+----------------------+-----------------------+
1 row in set (0.00 sec)
```
### 自定義函數
> #### 定義函數
```sql
-- 定義函數
create function 函數名([形參列表]) returns 數據類型 deterministic/NO SQL/READS SQL DATA
[begin] -- 如果只有一條 return 的話, begin 跟 end 可以省略
-- 函數體
-- 返回值
[end]
-- 調用函數
select 函數名([實參列表])
```
> - 必須指定返回值得數據類型
> - 如果實際返回的值不是當時指定的數據類型, 那不會返回
> - 但是在編譯時(定義函數)不會報錯, 因為編譯時還沒執行, 不會知道數據是啥
> - 看不懂為啥要加 `deterministic`/`NO SQL`/`READS SQL DATA`, 反正就是要選一個寫, 三個差別是什麼我也看不懂==,反正就是安全理由
> - https://www.iteye.com/blog/bijian1013-2306796
> - [第十三章p335](https://books.google.com.tw/books?id=xmgmDwAAQBAJ&pg=PA335&lpg=PA335&dq=DETERMINISTIC,+NO+SQL,or+READS+SQL+DATA+差別&source=bl&ots=hMilf6TvYy&sig=ACfU3U2qlFEmV2QqqAaGmKKpQZhO7VIdtQ&hl=zh-TW&sa=X&ved=2ahUKEwi626qi2JPnAhVsF6YKHWBUAUMQ6AEwA3oECAoQAQ#v=onepage&q=DETERMINISTIC%2C%20NO%20SQL%2Cor%20READS%20SQL%20DATA%20差別&f=false)
```sql
mysql> create function int_100() returns int deterministic
-> return 100;
Query OK, 0 rows affected (0.00 sec)
mysql> select int_100();
+-----------+
| int_100() |
+-----------+
| 100 |
+-----------+
1 row in set (0.00 sec)
```
> #### 查看函數
> - 查看所有函數
> `show function status [like 'pattern']`
```sql
mysql> show function status\G
...
*************************** 23. row ***************************
Db: test -- 他有綁定數據庫
Name: int_100
Type: FUNCTION
Definer: root@localhost
Modified: 2020-01-21 10:45:58
Created: 2020-01-21 10:45:58
Security_type: DEFINER
Comment:
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ci
Database Collation: utf8mb4_general_ci
23 rows in set (0.00 sec)
```
> - 查看創建函數語句
> `show create function 函數名`
```sql
mysql> show create function int_100\G
*************************** 1. row ***************************
Function: int_100
sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
Create Function: CREATE DEFINER=`root`@`localhost` FUNCTION `int_100`() RETURNS int
DETERMINISTIC
return 100
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ci
Database Collation: utf8mb4_general_ci
1 row in set (0.00 sec)
```
> #### 修改函數 & 刪除函數
> `drop function 函數名`
```sql
mysql> show create function int_100\G
*************************** 1. row ***************************
Function: int_100
sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
Create Function: CREATE DEFINER=`root`@`localhost` FUNCTION `int_100`() RETURNS int
DETERMINISTIC
return 100
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ci
Database Collation: utf8mb4_general_ci
1 row in set (0.00 sec)
mysql> drop function int_100;
Query OK, 0 rows affected (0.01 sec)
mysql> show create function int_100\G
ERROR 1305 (42000): FUNCTION int_100 does not exist
```
> #### 參數
> `function 函數名 (形參名 字段類型) returns 數據類型...`
> - 實參: 可以是數值, 也可以是變量
> - 形參: 必須指定數據類型
```sql
mysql> create function printNum(num int) returns int deterministic return num;
Query OK, 0 rows affected (0.01 sec)
mysql> select printNum(1);
+-------------+
| printNum(1) |
+-------------+
| 1 |
+-------------+
1 row in set (0.00 sec)
mysql> select printNum(100);
+---------------+
| printNum(100) |
+---------------+
| 100 |
+---------------+
1 row in set (0.00 sec)
```
> #### 作用域
> - MySQL 作用域跟 JS 一模一樣
> - 全局變量:
> - 可以在任何地方使用
> - 使用 `set` 聲明, 且使用 @符號標誌 `set @i = 1;`
> - 局部變量:
> - 只能在函式內使用
> - 使用 `declare` 聲明, 沒有 @ 符號
> - 但是任何變量修改都還是要使用 set !
> - 計算 1 ~ n 的和
> - 修改變量都要使用 set
> - MySQL 沒有 += 之類的東西
```sql
delimiter ^^
create function sum_1_to_n (n int) returns int deterministic
begin
set @i = 1; -- 全局變量
set @res = 0;
while @i <= n do
-- 任何變量要修改, 都需要使用 set
-- MySQL 沒有 += ++ ...
set @res = @res + @i;
set @i = @i + 1;
end while;
return @res;
end
^^
delimiter ;
mysql> select sum_1_to_n(10);
+----------------+
| sum_1_to_n(10) |
+----------------+
| 55 |
+----------------+
1 row in set (0.00 sec)
mysql> -- 在外面就可以取循環跑到最後的值
mysql> select @i, @res;
+------+------+
| @i | @res |
+------+------+
| 11 | 55 |
+------+------+
1 row in set (0.00 sec)
```
> - 求 1~n 的和, 5的倍數不取
```sql
mysql> delimiter ^_^
mysql> create function sum_1_to_n_not_5 (n int) returns int deterministic
-> begin
-> declare i int default 1;
-> declare res int default 0;
-> mywhile: while i < n do -- 因為要用到 iterate, 所以命名
-> if i % 5 = 0 then -- 判斷是否為 5 的倍數
-> set i = i + 1; -- 如果是, 先讓控制項+1, 否則原控制項跳出會死循環
-> iterate mywhile; -- 跳出這圈循環
-> end if; -- 結束判斷
-> set res = res + i; -- 不是5的倍數會繼續走這來加值
-> set i = i + 1;
-> end while;
-> return res;
-> end
-> ^_^
Query OK, 0 rows affected (0.00 sec)
mysql> delimiter ;
mysql> select sum_1_to_n_not_5;
ERROR 1054 (42S22): Unknown column 'sum_1_to_n_not_5' in 'field list'
mysql> select sum_1_to_n_not_5(10);
+----------------------+
| sum_1_to_n_not_5(10) |
+----------------------+
| 40 |
+----------------------+
1 row in set (0.00 sec)
mysql> -- 局部變量外面就拿不到了,
mysql> select i;
ERROR 1054 (42S22): Unknown column 'i' in 'field list'
mysql> select res;
ERROR 1054 (42S22): Unknown column 'res' in 'field list'
```
### 存儲過程(procedure)
> - 簡稱過程
> - 一種沒有返回值的函數
> - 有些增刪改操作並不需要返回值, 此時既可以考慮用 `procedure`
> - 如果過程想要顯示數據, 就在過程體裡寫 select 就好了
> #### 創建過程
```sql
create procedure 過程名 ([參數列表])
begin
-- 過程體
end
```
```sql
mysql> create procedure pro1()
-> select * from my_class;
Query OK, 0 rows affected (0.00 sec)
```
> #### 調用過程
> `call 過程名([實參列表])`
> - 過程沒有返回值, `select` 是無法訪問的
```sql
mysql> call pro1();
+----+--------------+------+
| id | name | item |
+----+--------------+------+
| 1 | 三年三班 | JS |
| 2 | 二年四班 | PHP |
| 3 | 一年五班 | C |
+----+--------------+------+
3 rows in set (0.00 sec)
```
> #### 查看過程
> - 查看所有過程
> `show procedure status [like 'pattern'];`
```sql
mysql> show procedure status\G
...
*************************** 27. row ***************************
Db: test
Name: pro1
Type: PROCEDURE
Definer: root@localhost
Modified: 2020-01-21 15:24:42
Created: 2020-01-21 15:24:42
Security_type: DEFINER
Comment:
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ci
Database Collation: utf8mb4_general_ci
27 rows in set (0.00 sec)
```
> - 查看過程創建語句
> `select create procedure 過程名`
```sql
mysql> show create procedure pro1\G
*************************** 1. row ***************************
Procedure: pro1
sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `pro1`()
select * from my_class
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ci
Database Collation: utf8mb4_general_ci
1 row in set (0.00 sec)
```
> #### 修改過程 & 刪除過程
> `drop procedure 過程名`
> - 過程只能先修改後刪除
```sql
mysql> show create procedure pro1\G
*************************** 1. row ***************************
Procedure: pro1
sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `pro1`()
select * from my_class
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ci
Database Collation: utf8mb4_general_ci
1 row in set (0.00 sec)
mysql> -- 刪除過程
mysql> drop procedure pro1;
Query OK, 0 rows affected (0.01 sec)
mysql> -- 找不到了
mysql> show create procedure pro1\G
ERROR 1305 (42000): PROCEDURE pro1 does not exist
```
> #### 過程參數
> - 過程參數嚴格限定種類
> - `in` :
> - 接受從外面傳值 (值傳遞) 進去,
> - 可以是值或變量
> - `out` :
> - 不使用外部數據, 且==最後==會返回局部變量給全局變量 (傳址, 引用傳遞)
> - 想像: 假設A送B一個禮盒, B 會把禮盒的東西全部丟掉, 然後只留盒子, 最後還會裝一些東西回送給A
> - 只接受變量
> - `inout` :
> - 使用外部數據, 且==最後==會返回局部變量給全局變量 (傳址, 引用傳遞)
> - 只接受變量
> - 基本用法
> `create procedure 過程名([in/out/inout 形參名 數據類型])`
```sql
mysql> delimiter ^_^
mysql> create procedure por1 (in x int, out y int, inout z int)
-> begin
-> select x,y,z;
-> end
-> ^_^
Query OK, 0 rows affected (0.01 sec)
mysql> delimiter ;
mysql> -- out 跟 inout 只接受變量(傳址)
mysql> call por1(1,2,3);
ERROR 1414 (42000): OUT or INOUT argument 2 for routine test.por1 is not a variable or NEW pseudo-variable in BEFORE trigger
mysql> set @x = 1;
Query OK, 0 rows affected (0.00 sec)
mysql> set @y = 2;
Query OK, 0 rows affected (0.00 sec)
mysql> set @z = 3;
Query OK, 0 rows affected (0.00 sec)
mysql> call por1(@x,@y,@z);
+------+------+------+
| x | y | z |
+------+------+------+
| 1 | NULL | 3 | -- out 不使用外部數據!!!
+------+------+------+
1 row in set (0.00 sec)
mysql> -- out 跟 inout 如果修改了參數值, 會直接修改傳進去的變量
mysql> select @x,@y,@z;
+------+------+------+
| @x | @y | @z |
+------+------+------+
| 1 | NULL | 3 | -- @y 被改為 NULL 了
+------+------+------+
1 row in set (0.00 sec)
```
> - 存儲過程對於變量的操作, 是在存儲過程調用結束後, 才會重新將內部修改的值賦予外部傳入的變量
```sql
mysql> delimiter ^_^
mysql> create procedure pro2 (in x int, out y int, inout z int)
-> begin
-> select x,y,z; -- 查看傳進來的變量值
-> -- 修改局部變量
-> set x = 9;
-> set y = 99;
-> set z = 999;
-> select x,y,z; -- 查看局部變量
-> select @x,@y,@z; -- 查看全局變量有沒有被修改
-> -- 修改全局變量
-> set @x = 'a';
-> set @y = 'b';
-> set @z = 'c';
-> select @x,@y,@z; -- 查看這時的
-> end
-> ^_^
Query OK, 0 rows affected (0.01 sec)
mysql> delimiter ;
mysql> -- 重設變量值
mysql> set @x = 1;
Query OK, 0 rows affected (0.00 sec)
mysql> set @y = 2;
Query OK, 0 rows affected (0.00 sec)
mysql> set @z = 3;
Query OK, 0 rows affected (0.00 sec)
mysql> -- 調用並傳變量
mysql> call pro2(@x,@y,@z);
+------+------+------+
| x | y | z |
+------+------+------+
| 1 | NULL | 3 | -- in 不使用外部數據(禮物被丟了)
+------+------+------+
1 row in set (0.00 sec)
-- 修改局部變量之後
+------+------+------+
| x | y | z |
+------+------+------+
| 9 | 99 | 999 | -- 局部變量被修改了
+------+------+------+
1 row in set (0.00 sec)
+------+------+------+
| @x | @y | @z |
+------+------+------+
| 1 | 2 | 3 | -- 局部變量與全局變量無關
+------+------+------+
1 row in set (0.00 sec)
-- 全局變量被修改之後
+------+------+------+
| @x | @y | @z |
+------+------+------+
| a | b | c | -- 全局變量被修改了
+------+------+------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
mysql> -- 在存儲過程結束之後, 再看一次全局變量
mysql> select @x,@y,@z;
+------+------+------+
| @x | @y | @z |
+------+------+------+
| a | 99 | 999 | -- out 跟 inout 被改成當時局部變量的值了
+------+------+------+ -- 在調用完後, 這兩類形參會將外部傳來的實參賦值返回
1 row in set (0.00 sec)
```