Database
===
### E-R模型
- 數據列(字段)
- 能唯一標記某字段: 主鍵(primary key)
- 數據行(紀錄)
- 數據表(行集合)
- 數據庫(表集合)
### 字段
- 類型
- 整數: int,
- 小數: decimal(需指定總位數與小數位數)
- `decimal(5,2)` > 總共5位, 小數2位
- 1.1(o), 1000.11(x)
- 字符串: char, varchar, text
- char & varchar > 有限長度的str
- char>
- 固定
- char(8) > "abcd " > 不足會補空格
- varchar
- 可變
- varchar(8) > "abcd" > 不會補空格
- text > 文本
- datetime
- bit
- enum
- 說明:
- 圖片、音檔、影檔等存在服務器上,表中存路徑
- 使用數據類型原則: 夠用即可, 越小越好
- [參考](https://blog.csdn.net/anxpp/article/details/51284106)
### 約束
- primary key
- 不會重複, 唯一標示
- `vs.unique`
- 只能建有一個
- 存儲結構系按照主鍵來維護
- not null
- unique
- 可以建立多個惟一
- default
- foreign key: 存的值都是別的表的主鍵, 該列稱為外鍵
- auto_increment 表自動增長
## RDBMS(Relational Database Management System)
### 主要
- Oracal: 常用於大型項目
- MySQL: 常用於Web
- MS SQL Server: 微軟
- SQLite: 行動端
### [排名](https://db-engines.com/en/ranking)
## SQL(Structured Query Language)
> 用來操作RDBMS的數據庫語言, 亦即SQL可操作上述產品
- 主要語句
- DQL: 數據查詢語言, 如select
- DML: 數據操作語言, 如insert、udpate、delete
- DDL: 數據定義語言, 如create、drop
- TPL: 事務處理語言, 如begin transaction、commit、rollback
- DCL: 數據控制語言, 如grant、revoke
- CCL: 指針控制語言, 如declare cursor
- Web程序員的重點在於CRUD(增刪改查)
- SQL不區分大小寫
### 小結
> - RDBMS: 關係數據庫的管理系統, 有多種產品MySQL, Oracal等
> - 數據庫: 各系統產生其特殊文件, 稱為數據庫
> - 數據庫由各種數據表組成, 數據表由各種行(紀錄)列(字段)組成
> - SQL係用來與各系統溝通的語言
## MySQL
- 服務器端
- 啟動/重啟/停止(start/restart/stop)
- Ubuntu `$ sudo service mysql start`
- Mac `$ sudo /usr/local/mysql/support-files/mysql.server start`
- 配置文件目錄
- Ubuntu`/etc/mysql/mysql.cnf`
- Mac 默認沒有, 可以自己建
- 配置項目
- Ubuntu`/etc/mysql/mysql.cnf.d/mysql.cnf`
- cnf > config 簡稱
- Mac 默認沒有
- 客戶端
- 改帳密
- 進去
- 先用sudo`$ sudo -uroot -proot`
- 或查帳密
- `$ sudo cat /etc/mysql/debian.cnf`
- `$ sudo mysql -u debian-sys-maint -p密碼`
- 重設
```SQL
mysql> use mysql;
mysql> update user set authentication_string=PASSWORD('密碼') where user='帳號' and Host ='localhost';
mysql> update user set plugin = "mysql_native_password" where user='帳號';
mysql> flush privileges;
mysql> quit;
```
- 重啟 `$ sudo /etc/init.d/mysql restart`
```SQL
-- 進入
-- $ mysql -uooxx -pxxoo
-- $ mysql -uooxx -p
-- $ mysql -uhaha -phehe
-- 退出
exit;/quit;/ctrl+d
-- 查看所有數據庫
show databases;
-- 建立數據庫
create database DatabaseName;
create database DatabaseName charset=utf8;
-- 沒打編碼會預設
-- 查看創建數據庫
show create database DatabaseName;
-- 刪除數據庫
drop database DatabaseName;
-- 注意, 如果database name 有一些特殊符號, 如haha-4
-- 那麼可能在刪除的時候會出錯
-- 可以把整個名字用``匡起來, 代表一個整體
-- 例如, drop database `haha-4`;
-- 查看當前使用數據庫:
select database();
-- 使用數據庫:
use DatabaseName;
-- 查看當前數據庫的表
show tables;
-- 創建表
-- create table 數據表名字 (字段 類型 約束[, 字段 類型 約束])
create table ooxx(id int, name varchar(30));
create table xxoo(id int primary key not null auto_increment, name varchar(30));
-- 太長?換行寫
create table oxox(
id int primary key not null auto_increment,
name varchar(30)
);
insert into students values(0, "GodJJ", 168.88, "he", 0);
-- 查看表之結構
-- desc 表名;
-- show create table 表名;
-- Q. 創建students表(id, name, high, gender, cls_id)
create table students(
id int unsigned not null auto_increment primary key ,
name varchar(30),
high decimal(5,2),
gender enum("he", "she", "zir", "hir", "secret") default "secret",
cls_id int unsigned
);
-- 先int 才接unisgned , 與C相反(unsigned int a;)
-- Q. 創建classes(id, name)
create table classes(
id int unsigned not null primary key auto_increment,
name varchar(30)
);
insert into classes values(0, "Alimamado");
-- if not exists
-- 如果不存在, 我就執行; 反之, 不執行
create table if not exists classes(oo xx,xx oo);
-- 添加字段:
--alter table 表名 add 列名 類型;
alter table students add birthday datetime;
-- 修改字段(不改名):
-- alter table 表名 modify 列名 類型 約束;
alter table students modify birthday date;
-- 修改字段(改名):
-- alter table 表名 change 舊列名 新列名 類型 約束;
alter table students change birthday birth date default "2019-08-06";
-- 刪除字段:
-- alter table 表名 drop 列名;
alter table students drop high;
-- 刪除表:
-- drop table 表名;
drop table ooxx;
-- CRUD(增刪改查)
desc students;
+--------+---------------------------------------+------+-----+------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------+---------------------------------------+------+-----+------------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(30) | YES | | NULL | |
| gender | enum('he','she','zir','hir','secret') | YES | | secret | |
| cls_id | int(10) unsigned | YES | | NULL | |
| birth | date | YES | | 2019-08-06 | |
+--------+---------------------------------------+------+-----+------------+----------------+
-- 新增數據
-- insert into ooxx vaulues();
insert into students values(0, "Toyz", 168.88, "he", "1993-01-01");
insert into students values(null, "Toyz", 168.88, "he", "1993-01-01");
insert into students values(default, "Toyz", 168.88, "he", "1993-01-01");
-- enum
-- 輸入非選項
insert into students values(default, "Toyz", "other", 1, "1993-01-01");
Error: Data truncated for column 'gender' at row 1
-- enum下標從1開始
insert into students values(default, "Toyz", 0, 1, "1993-01-01");
ERROR: Data truncated for column 'gender' at row 1
insert into students values(default, "Toyz", 1, 1, "1993-01-01");
insert into students values(default, "Toyz", 6, 1, "1993-01-01");
ERROR: Data truncated for column 'gender' at row 1
-- 部分插入
-- insert into 表名(列,...) values (值,...);
insert into students (name, gender) values ("Harry", 1);
-- 多行插入
insert into students (name, gender) values ("Snape", 1), ("Albus", 1);
insert into students values
(0, "Naluto", 168.88, 1, "1993-01-01"),
(0, "Sasuki", 168.88, 1, "1993-01-01");
-- 修改數據
-- update 表名 set 列1=值1,列2=值2,... where 條件;
update students set gender=1; -- 性別全改成男
update students set gender=1 where name="Toyz"; -- 叫Toyz的都改成男
update students set gender=1 where id=4; -- 標號4改男
update students set gender=1, cls_id=3 where id=7; -- 標號7改男跟三班
-- 查詢數據
-- 查詢所有
-- select * form 表名;
select * from students;
-- 查詢條件數據
-- select * from 表名 where 條件;
select * from students where id>=3;
-- 查詢指定列
-- select 列1,列2,.. from 表名;
select name, cls_id from students;
-- 改名顯示
-- select 列1 as 名1, 列2 as 名2,.. from 表名;
select name as 姓名, gender as 性別 from students;
-- 字段順序
select gender, name from 表名;
-- 刪除
-- 物理刪除
-- delete from 表名 where 條件;
delete from students; -- 全刪
delete from students where name="Toyz";
-- 實務上盡量不要物理刪除, Data非常重要
-- 邏輯刪除
-- 用一個字段來表示這條紀錄是否能使用
alter table students add is_delete default 0;
update students set is_delete=1 where id=10;
-- 利用查詢來看有效帳號
-- bit 太小, 故會有無法顯示的情形, 屬正常
-- 顯示當前版本
select version();
-- 顯示當前時間
select now();
-- ctrl + a 回行首
-- ctrl + e 回行末
```
#### MySQL儲存引擎
- InnoDB
- 支援外鍵
- 支援事務處理: 要馬不做
- 支援行級鎖
### 查詢補充
```SQL
-- select 表名.列1, 表名.列2... from 表名;
select students.name, students.age from students
-- 給表取別名
-- select 別名.列1, 別名.列2... from 表名 as 別名;
select s.name, s.age from students as s;
-- 取別名就要用
select students.name, s.age from students as s;
ERROR: Unknown column 'students.name' in 'field list'
-- 消除重複行
-- distinct 字段
select distinct gender from students;
-- 條件查詢
-- 比較運算符 >, <, =, >=, <=, !=(or <>)
select * from students where age>18;
-- cf. 指定顯示訊息沒有包含條件也沒有衝突
select name, gender from students where age>18;
-- cf. <> 在python2可以用, python3就不能用了
-- 邏輯判斷符 and, or, not
select * from students where age>18 and age<28;
-- 判斷語句必須寫完整
select * from students where age >18 and <28;
Erroe: You have an error in your SQL syntax;
select * from students where age>18 and gender=2;
select * from students where age>18 and gender="she";
-- 不在18歲以上女性的訊息
select * from students where not (age>=18 and gender=2);
-- 不在18歲以上且為女性的訊息
select * from students where not age>=18 and gender=2;
-- 模糊查詢
-- like
-- % 替換一個或多個
-- _ 替換一個
-- 查詢以"小"開頭的名字
select name from students where name like "小%";
-- 查詢名字有"小"的名字
select name from students where name like "%小%";
-- 查詢名字兩個字
select name from students where name like "__";
-- 查詢名字兩個字以上
select name from students where name like "__%";
-- rlike 加 正則
-- 周開頭
select name from students where name rlike "^周.*";
-- 周開頭倫結尾
select name from students where name rlike "^周.*倫$";
-- 範圍查詢
-- 非連續: in, not in
-- 查詢年齡18,12,34
select name, age from students where age=18 or age=12 or age=34;
select name, age from students where age in (18, 12, 34);
select name, age from students where age not in (18, 12, 34);
-- 連續: between
-- 查詢年齡18~28
select name, age from students where age between 18 and 28;
-- 查詢年齡非18~28
select name, age from students where age not between 18 and 28;
select name, age from students where not age between 18 and 28;
-- cf not between 為一個用法, 不能拆開
select name, age from students where age not (between 18 and 28);
Error: You have an error in your SQL syntax;
-- 判空 is null
select * from students where height is null;
select * from students where height is not null;
-- 排序
-- order by 字段
-- 由低到高 asc
-- 18~34歲男性 年齡由低到高排序
select * from students where (age between 18 and 34) and gender=1 order by age;
select * from students where (age between 18 and 34) and gender=1 order by age asc;
-- order by 預設為 asc.
-- (age between) 小括號增加可讀性, 沒括也沒問題
-- 由高到低 desc
-- 18~34歲女性 年齡由高到低排序
select * from students where (age between 18 and 34) and gender=2 order by age desc;
-- order by 多字段,
-- 18~34女性, 身高高到矮, 相同身高照年齡小到大
select * from students where (age between 18 and 34) order by height desc, age asc;
-- 按年齡小到大, 身高高到矮, id大到小
select * from students order by age, height desc, id desc;
-- 聚合函數
-- 總數count
-- 查詢男性有多少人
select count(*) from students where gender=1;
-- 最大值max
-- 查詢最大年齡
select max(age) from students;
-- 查詢女性最高身高
select max(height) from students where gender=2;
-- 和sum
-- 所有年齡總合
select sum(age) from students;
-- 平均avg
select avg(age) from students;
-- 計算平均年齡
select sum(age)/count(*) from students;
-- 四捨五入 round(value, 小數)
-- 四捨五入平均年齡到小數第二位
select round(sum(age)/count(*), 2) from students;
-- ps. C語言存儲的小數都是約等於,
-- 在銀行裡絕對不允許約等於
-- 解決辦法: 將小數放大, 不要使用小數
-- ex. 3.14 -> 3.14*100
-- ERROR 1140
select sum(age), name from students;
-- 總和為一個值, 要用哪個名字來代表?故失敗
-- 分組
-- group by
-- 按性別分組
select gender from students group by gender;
-- 失敗 ERROR 1055
select * from students group by gender;
select name from students group by gender;
-- 名字有很多個, 要用哪一個來當組名?故失敗
-- 計算各性別人數
select gender, count(*) from students group by gender;
-- group_concat(欲顯示的東西)
-- 查詢同性別的姓名
select gender, group_concat(name) from students group by gender;
-- (裡面寫啥都可)
select gender, group_concat("GodJJ") from students group by gender;
-- 查詢同性姓名與年齡
select gender, group_concat(name, " ", age) from students group by gender;
-- 查詢男性姓名與年齡
select gender, group_concat(name, " ", age) from students where gender=1 group by gender;
-- Q. group by 與 distinct 差別?
-- group by 效率較低, 但延展性較高
-- distinct 效率高, 但就是單純把重複的名稱刪掉
-- having 條件判斷
-- 查詢平均年齡超過30的性別及該性別姓名
select gender, group_concat(name), avg(age) from students group by gender having avg(age)>30;
-- 查詢性別人數大於二的信息
select gender, group_concat(name) from students group by gender haveing count(*)>2;
-- Q. where與having區別?
-- where是對表進行條件, having是對分組的組別進行條件
-- 分組實際運用: 常用於按照部門分
-- 分組與聚合一起運用才有意義
-- 分頁: 限制查詢各數, 避免一次顯示太多
-- limit start, count
-- 每頁顯示2個, 頁面1
select * from students limit 0, 2;
-- 每頁顯示2個, 頁面2
select * from students limit 2, 2;
-- 每頁顯示2個, 頁面3
select * from students limit 4, 2;
-- 每頁顯示2個, 頁面4
select * from students limit 6, 2;
-- 每頁顯示2個, 頁面5
select * from students limit 8, 2;
-- limit (第n頁-1)*每頁個數, 每頁個數;
-- 注意, 不要使用式子
-- ERROR 1064
select * from students limit (5-1)*2, 2;
-- 注意, limit一定放在最後
-- ERROR 1064
select * from students limit 5,2 order by age asc;
-- 正確
select * from students order by age asc limit 5,2;
-- 連接查詢
-- 內連接[圖一]: 只顯示雙方有交集的相關信息
-- inner join ... on
-- 查詢能夠對應班級及學生的信息
select * from students inner join classes on students.id_cls = classes.id;
-- id_cls跟classes.id 重複顯示了
select students.*, classes.name from students inner join classes on students.cls_id = classes.id;
-- 取別名
select s.*, c.name from students as s inner join classes as c on s.cls_id = c.id;
-- 將上述信息按班級排, 同班級按id小至大, 班級信息在第一列
select c.name, s.* from students as s inner join classes as c on c.id = s.cls_id order by c.name, s.id;
-- 左連接[圖二]: 以左邊為基準, 顯示與左右交集的右邊信息
-- left join ... on
select * from students as s left join classes as c on c.id = s.cls_id;
-- 查詢沒有對應班級的學生信息
select * from students as s left join classes as c on c.id = s.cls_id having c.id is null;
select * from students as s left join classes as c on c.id = s.cls_id where c.id is null;
-- 雖然having跟where都可以,
-- 習慣上對於原表的篩選條件都是以where為主
-- 而對於新產生的表所使用的篩選條件則以having為主
-- 此連接表屬於新產生的表, 故習慣上還是以having為主
-- 右連接[圖三] > 很少用, 因為左連接的兩表對調即可達成效果
```
> - 圖一
> 
> - 圖二
> 
> - 圖三
> 
### 自關聯
> 一個表中的值使用表中另一個值
```
--外鍵--
# 表一 city # 表二 District # 表三
id c_name id d_name c_id id Village d_id
1 台北市 1 信義區 1 1 西村里 1
2 新竹縣 2 竹北市 2 2 東平里 2
3 金門縣 3 金城鎮 3 3 珠沙里 3
--自關聯--
id name tw_id
1 台北市 null
2 新竹縣 null
3 金門縣 null
4 信義區 1
5 竹北市 2
6 金城鎮 3
7 西村里 4
8 東平里 5
9 珠沙里 6
```
```sql
-- 查詢所有直轄市/縣/市
select * from areas where tw_id is null;
-- 查隸屬台北市的所有區
select id from areas where name="台北市";
select * from areas where tw_id=1;
-- 一行搞定
-- 邏輯: 叫表兩次後關聯在一起
select * from areas as city inner join areas as district
on city.id = district.tw_id having city.name = "台北市";
-- 顯示需要的信息
select c.name, d.name from areas as c inner join areas as d
on c.id = d.tw_id having c.name = "台北市";
```
#### 如何直接匯入.sql數據
```$
# 首先進入SQL前要先到該檔案的同層路徑
$ ls
xxx.sql
$ cat xxx.sql
INSERT INTO `haha` VALUES ('9527', 'GodJJ', '65535');
INSERT INTO `haha` VALUES ('9527', 'Toyz', '65536');
$ mysql -uxx -pxx
```
```SQL
mysql> use xxx
mysql> source xxx.sql;
```
### 子查詢
> 查詢裡面嵌套查詢
```SQL
-- 查詢身高最高男生的資料
select * from students where height = (select max(height) from students where gender=1);
-- 查詢台北市資料
select * from areas where tw_id = (select id from areas where name = "台北市");
```
## 數據庫設計
#### 範式1: 列拆到不能再拆
```
contact name tel addr
小明, 0987654321, US > 小明 0987.. US
```
#### 範式2:
1. 表需有主鍵
2. 其他信息需完全依賴主鍵, 不能部分依賴
```
表 OrderDetail
OrderID ProductID UnitPrice Quantity ProcductName
100 01 10 2 洗髮水
- 主鍵可以有兩個以上
- 此表的主鍵應該是OrderID跟ProductID所構成
- 如此才能描述這張訂單的全貌
- 惟商品名跟商品單價僅依賴商品ID, 而非完全依賴主鍵(訂單ID+商品ID)
- 故此數據不符合範式2
表 OrderDetail
OrderID ProductID Quantity
100 01 2
表 Product
ProductID UnitPrice ProcductName
01 10 洗髮水
- 如此才符合範式2
```
#### 範式3: 非主鍵列須直接依賴主鍵, 不能傳遞依賴
```
表 Order
OrderID Date CustomerID CustomerName CustomerAddr CustomerPhone
100 0305 10 小明 台北 0987..
- 顧客ID依賴訂單ID沒問題, 但是顧客訊息沒有直接依賴訂單ID
- 顧客訊息先依賴顧客ID後, 顧客ID才依賴訂單ID
表 Order
OrderID Date CustomerID
100 0305 10
表 Customer
CustomerID CustomerName CustomerAddr CustomerPhone
10 小明 台北 0987..
```
```
表 Order
OrderID Date CustomerID
100 0305 10
表 OrderDetail
OrderID ProductID Quantity
100 01 2
100 03 1
表 Product
ProductID UnitPrice ProcductName
01 10 洗髮水
02 5 牙膏
03 2 牙刷
表 Customer
CustomerID CustomerName CustomerAddr CustomerPhone
1 小花 台中 0978..
2 小子 花蓮 0912..
10 小明 台北 0987..
- 表Order的訂單ID關聯到訂單細節,
- 表Order的顧客ID關聯到顧客資料
- ...
- 如此稱之為關係數據庫
```
## E-R模型
#### 一對一
```
# 當列太多時,可以拆表,將不常用的資訊放到另一張表
- Customer
id name tel infor_id
1 GodJJ 0987 1
- Information
id mom dad
1 haha hehe
```
#### 一對多
```
# 一對多時, 必須在多的表添加
# 否則未來haha跟hehe又生小孩時, 該關系列也要修改
# 故實務上多在N表添加
- Customer
id name tel infor_id
1 GodJJ 0987 1
2 GodGG 0932 1
- Information
id mom dad
1 haha hehe
```
#### 多對多
```
科目 學生
id subject id name
1 國文 1 haha
2 英文 2 hehe
3 數學 3 hoho
# 一個學生可以上多個課,
# 一堂課也可以有多個學生上
# 解法: 多一張表, 該表稱為『聚合表』
科目-學生
1 1
1 2
2 2
2 3
```
#### 16.2練習
```SQL
-- 查詢每種類型中最貴的信息
-- 問題: 只有一個表 > 叫表兩次, 關聯在一起
-- 表一
select * from goods;
-- 表二
select cate_name, max(price) from goods group by cate_name;
-- 實驗: 查詢出來的表也可以直接使用, 只是要有別名
-- ERROR 1248 (42000): Every derived table must have its own alias
select * from (select cate_name from goods group by cate_name);
-- 取別名就沒問題
select * from (select cate_name from goods group by cate_name) as g;
-- 邏輯:
-- select * from 表二 left join goods on
-- 表二.max_price = goods.price and 表二.cate_name = goods_cate_name;
select * from (
select cate_name,
max(price) as max_price from goods group by cate_name
) as g_new left join goods as g
on g_new.max_price = g.price and g_new.cate_name = g.cate_name;
-- 因為不能寫 g_new.max(price),
-- 所以在 max(price) 所產生的列取一個別名 max_price
-- 整理
select g.name, g.cate_name, g.price from (
select cate_name,
max(price) as max_price from goods group by cate_name
) as g_new left join goods as g
on g_new.max_price = g.price and g_new.cate_name = g.cate_name
order by g.cate_name;
-- 此方法稱為『表級子查詢』
```
#### 16.2 拆表
```SQL
-- 把商品類型表拆出來
-- 1. 創建
create table goods_cates(
id int unsigned primary key auto_increment,
name varchar(40) not null
);
-- 2. 寫入數據
-- 我要的數據
select cate_name from goods group by cate_name;
-- 部分插入:
-- insert into 表名(列,...) values (值,...);
-- 注意! 使用子查詢來新增時沒有Values, 否則語法錯誤
insert into goods_cates(name) select cate_name from goods group by cate_name;
-- ERROR 1064 (42000): You have an error in your SQL syntax;
insert into goods_cates(name) values select cate_name from goods group by cate_name;
-- 補充: 創表時直接插入數據
create table goods_cates (
id int unsigned primary key auto_increment,
name varchar(40) not null)
select cate_name as name from goods group by cate_name;
-- 注意查詢出來的列名要跟欲插入的列名相同,
-- 不同名時要取別名
-- 3. 同步表數據
-- update 表名 set 列1=值1,列2=值2
-- update goods set cate_name = xxx;
-- 邏輯: 使用關聯條件,
-- goods的cate_name跟goods_cates的name一樣時,
goods as g inner join goods_cates as c on g.cate_name = c.name
-- 就把cate_name列改成goods_cates的ID
update goods as g inner join goods_cates as c on g.cate_name = c.name set g.cate_name = c.id;
-- 4. 修改表結構
-- 由於兩表的這兩列要同步, 故表結構也應該相同
desc goods_cates;
desc goods;
-- 習慣上會把該列改成欲同步的表名_id > cate_id
-- alter table 表名 change 舊列名 新列名 類型 約束;
alter table goods change cate_name cate_id int unsigned not null;
-- 5. 設定外鍵
-- 為了避免cate_id列出現goods_cates.id列以外的東西
-- 例如類別只有十種, cate_id列卻有17的數字, 造成數據對不起來
-- 故應做一些限制, 方法有二
-- (1) 在新增數據的時候做判斷該id或名稱是否存在, 符合才能新增
-- (2) 將該列新增外鍵限制, 不符合時系統會報錯
-- alter table 表名 add foreign key (列) references 對應表名(對應列名);
alter table goods add foreign key (cate_id) references goods_cates(id);
-- 注意
-- (1) 如果表裡已經有不符合的數據, 則會新增失敗> 先把不符合的數據刪改後再設定
-- (2) 外鍵只有innodb數據引擎支援
-- 刪除外鍵
-- 事實上實務上很少使用外鍵, 因為外鍵的效率低
-- 1. 先查出外鍵約束名稱, 由系統自動產生
-- show create table ooxx;
show create table goods;
goods | CREATE TABLE `goods` (
...
CONSTRAINT `goods_ibfk_1` FOREIGN KEY (`cate_id`) REFERENCES `goods_cates` (`id`),
CONSTRAINT `goods_ibfk_2` FOREIGN KEY (`brand_id`) REFERENCES `goods_brands` (`id`)
...
)
-- CONSTRAINT 後面就是外鍵約束名稱
-- 2. alter table goods drop foreign key 外键名称;
alter table goods drop foreign key goods_ibfk_1;
alter table goods drop foreign key goods_ibfk_2;
-- 或是一次改
alter table goods
drop foreign key goods_ibfk_1,
drop foreign key goods_ibfk_2;
-- MySQL 基本上係存儲於硬盤中
-- 速度: CPU>內存>固態硬盤>機械硬盤
-- 故硬盤速度慢
-- 解法: 把主鍵存在內存中, 要搜尋時到內存中找要快得多
-- Q. 為何不都存到內存?
-- 貴
```
## Python操作MySQL
#### 首先先載模塊
```
$ pip3 install pymysql
$ pip insatll
```
#### Python訪問數據庫流程
1. 創建connection > 獲取cursor
2. 執行SQL
3. 關閉cursor > 關閉connection
### 查
```python
In [1]: from pymysql import connect
# 創建conn對象
In [4]: conn = connect(host="localhost", port=3306, user='root', password='youknow.', database='jing_dong', charset='utf8')
# 獲取cursor
In [5]: cursor = conn.cursor()
# 使用execute來執行SQL語句
In [6]: cursor.execute("select * from goods")
# execute執行成功後會返回生效行數
Out[6]: 21
# Q. 返回行數沒用啊, 數據呢?
# 存在該游標對象裡, 透過fatch來取出
# fatch有三種 all, one, many, 跟字面上一樣意思
#
# fetchone返回一行元祖
In [7]: cursor.fetchone()
Out[7]: (1, 'r510vc 15.6英吋筆記本', 1, 1, Decimal('3399.000'), b'\x01', b'\x00')
In [8]: cursor.fetchone()
Out[8]: (2, 'y400n 14.0英吋筆記本電腦', 1, 2, Decimal('4999.000'), b'\x01', b'\x00')
In [9]: cursor.fetchone()
Out[9]: (3, 'g150th 15.6英吋遊戲本', 2, 3, Decimal('8499.000'), b'\x01', b'\x00')
# fatchmany可以輸入參數, 代表取幾行, 預設一行
# fatchmany返回元祖嵌元祖
In [10]: cursor.fetchmany()
Out[10]: ((4, 'x550cc 15.6英吋筆記本', 1, 1, Decimal('2799.000'), b'\x01', b'\x00'),)
In [11]: cursor.fetchmany(2)
Out[11]:
((5, 'x240 超極本', 3, 2, Decimal('4880.000'), b'\x01', b'\x00'),
(6, 'u330p 13.3英吋超極本', 3, 2, Decimal('4299.000'), b'\x01', b'\x00'))
In [12]: cursor.fetchall()
((7, 'svp13226scb 觸控超極本', 3, 4, Decimal('7999.000'), b'\x01', b'\x00'),
...
(21, '商務雙肩背包', 7, 4, Decimal('99.000'), b'\x01', b'\x00'))
# 取完就沒了
In [13]: cursor.fetchone()
# 實際運用範例
In [14]: cursor.execute("select * from goods")
Out[14]: 21
# 找對象存起來
In [15]: content = cursor.fetchone()
In [17]: content
Out[17]: (1, 'r510vc 15.6英吋筆記本', 1, 1, Decimal('3399.000'), b'\x01', b'\x00')
In [16]: content[0]
Out[16]: 1
In [18]: content[1]
Out[18]: 'r510vc 15.6英吋筆記本'
In [19]: for temp in content:
...: print(temp)
...:
1
...
b'\x00'
In [20]: content = cursor.fetchmany(3)
In [21]: content
Out[21]:
((2, 'y400n 14.0英吋筆記本電腦', 1, 2, Decimal('4999.000'), b'\x01', b'\x00'),
(3, 'g150th 15.6英吋遊戲本', 2, 3, Decimal('8499.000'), b'\x01', b'\x00'),
(4, 'x550cc 15.6英吋筆記本', 1, 1, Decimal('2799.000'), b'\x01', b'\x00'))
In [22]: content[1]
Out[22]: (3, 'g150th 15.6英吋遊戲本', 2, 3, Decimal('8499.000'), b'\x01', b'\x00')
# 使用完要把cursor跟conn關閉
In [24]: cursor.close()
In [25]: conn.close()
```
### 實作
> - 查所有信息
> - 查所有分類
> - 查所有品牌分類
```python
#coding:utf-8
from pymysql import connect
conn = connect(host="localhost", port=3306, user="root", password="youknow.", database="jing_dong", charset="utf8")
cs = conn.cursor()
while True:
a = input("請問您要查詢什麼? 1.所有商品信息, 2. 所有分類 3. 所有品牌分類 4. 結束 ")
if a == "1":
cs.execute("select * from goods")
elif a == "2":
cs.execute("select name from goods_cates")
elif a == "3":
cs.execute("select name from goods_brands")
elif a == "4":
break
lines = cs.fetchall()
for temp in lines:
print(temp)
cs.close()
conn.close()
```
```python
#coding:utf-8
from pymysql import connect
class JD(object):
def __init__(self):
self.conn = connect(host="localhost", port=3306, user="root", password="youknow.", database="jing_dong", charset="utf8")
self.cs = self.conn.cursor()
def __del__(self):
self.cs.close()
self.conn.close()
@staticmethod
def print_menu():
print("--京東--")
print("1. 所有商品信息")
print("2. 所有分類")
print("3. 所有品牌分類")
print("0. 結束")
return input("請輸入功能號碼")
def execute_sql(self, sql):
self.cs.execute(sql)
lines = self.cs.fetchall()
for temp in lines:
print(temp)
def show_all_items(self):
sql = "select * from goods;"
self.execute_sql(sql)
def show_all_cates(self):
sql = "select name from goods_cates;"
self.execute_sql(sql)
def show_all_brands(self):
sql = "select name from goods_brands;"
self.execute_sql(sql)
def run(self):
while True:
num = self.print_menu()
if num == "1":
self.show_all_items()
elif num =="2":
self.show_all_cates()
elif num =="3":
self.show_all_brands()
elif num =="0":
break
def main():
jd = JD()
jd.run()
if __name__ == "__main__":
main()
```
### 增刪改
> - 增刪改除SQL語句不同外
> - 增刪改需要多一道connect.commit的動作來確認, 因為他會改變數據庫
> - 注意是connect 不是 cursor
> - 反悔不改時, 提交connect.rollback即可
```python
In [1]: from pymysql import connect
In [2]: conn = connect(host="localhost", port=3306, user="root", password="youknow.", database="jing_dong", charset="utf8")
In [3]: cs = conn.cursor()
# show create table goods_cates;
# AUTO_INCREMENT=8
# 注意
# In [6]: cs.execute("insert into goods_cates (name) value ("硬盤1")")
# SyntaxError: invalid syntax
# 前兩個雙引號包再一起了,會兩個也是, 硬盤1沒有包到
# 解決方法有二:
# 1. 內雙引號改成單引號
# 2. 外雙引號改成三個雙引號
In [7]: cs.execute("""insert into goods_cates (name) value ("硬盤1")""")
Out[7]: 1
In [8]: cs.execute("""insert into goods_cates (name) value ("硬盤2")""")
Out[8]: 1
In [9]: cs.execute("""insert into goods_cates (name) value ("硬盤3")""")
Out[9]: 1
# show create table goods_cates;
# AUTO_INCREMENT=11
# 但是select * from goods_cates; 沒有變
# 經過commit後才有新增進數據庫
# Q 為何AUTO_INCREMENT會新增?
# A 為了避免多任務併發產生問題
In [10]: conn.commit()
# rollback
In [11]: cs.execute("""insert into goods_cates (name) value ("硬盤4")""")
Out[11]: 1
In [12]: cs.execute("""insert into goods_cates (name) value ("硬盤5")""")
Out[12]: 1
In [14]: cs.execute("""insert into goods_cates (name) value ("硬盤6")""")
Out[14]: 1
In [15]: conn.rollback()
# 此時查詢數據沒有新增
# AUTO_INCREMENT=14
# select * from goods_cates;
# | 14 | 硬盤7 |
# 11~13在剛剛被取消了
In [16]: cs.execute("""insert into goods_cates (name) value ("硬盤7")""")
Out[16]: 1
In [17]: conn.commit()
```
```python
# 新增品牌分類
@staticmethod
def print_menu():
print("--京東--")
print("1. 所有商品信息")
print("2. 所有分類")
print("3. 所有品牌分類")
print("4. 新增品牌分類")
print("0. 結束")
return input("請輸入功能號碼")
def add_brands(self):
brands_name = input("請輸入品牌名稱: ")
sql = """insert into goods_brands (name) value ("%s")""" % brands_name
self.cs.execute(sql)
self.conn.commit()
def run(self):
while True:
num = self.print_menu()
if num == "1":
self.show_all_items()
elif num =="2":
self.show_all_cates()
elif num =="3":
self.show_all_brands()
elif num =="4":
self.add_brands()
elif num =="0":
break
```
### SQL注入
```python
# 新增一個指名查詢功能
@staticmethod
def print_menu():
print("--京東--")
print("1. 所有商品信息")
print("2. 所有分類")
print("3. 所有品牌分類")
print("4. 新增品牌分類")
print("5. 查詢指定商品信息")
print("0. 結束")
return input("請輸入功能號碼: ")
def get_info_by_name(self):
find_name = input("請輸入商品名稱: ")
sql = """select * from goods where name = '%s';""" % find_name
print("-->%s<--"% sql)
self.execute_sql(sql)
def run(self):
while True:
num = self.print_menu()
if num == "1":
self.show_all_items()
elif num =="2":
self.show_all_cates()
elif num =="3":
self.show_all_brands()
elif num =="4":
self.add_brands()
elif num =="5":
self.get_info_by_name()
elif num =="0":
break
```
```
$ python3 SQL注入.py
--京東--
1. 所有商品信息
2. 所有分類
3. 所有品牌分類
4. 新增品牌分類
5. 查詢指定商品信息
0. 結束
請輸入功能號碼: 1
(1, 'r510vc 15.6英吋筆記本' ...)
(...)
--京東--
1. 所有商品信息
2. 所有分類
3. 所有品牌分類
4. 新增品牌分類
5. 查詢指定商品信息
0. 結束
請輸入功能號碼: 5
請輸入商品名稱: r510vc 15.6英吋筆記本
-->select * from goods where name = 'r510vc 15.6英吋筆記本';<--
(1, 'r510vc 15.6英吋筆記本' ...)
--京東--
1. 所有商品信息
2. 所有分類
3. 所有品牌分類
4. 新增品牌分類
5. 查詢指定商品信息
0. 結束
請輸入功能號碼5
請輸入商品名稱: 'or 1=1 or '1
-->select * from goods where name = ''or 1=1 or '1';<--
(1, 'r510vc 15.6英吋筆記本' ...)
(...)
# 竟然全部都弄出來了!!!
# 因為將where條件補齊前後兩個引號無關緊要的條件,
# 並加上1=1, 讓條件判斷為True
# """select * from goods where name = '%s';""" % find_name
# 當輸入普通的商品名時
"""select * from goods where name = 'apple';""" % find_name
# SQL注入 ' or 1=1 or '1
"""select * from goods where name = ''or 1=1 or '1'""";
# KeyWord 烏雲網
```
```python
# 解決辦法: 不要自己填%s ,使用execute參數列表來填
def get_info_by_name(self):
find_name = input("請輸入商品名稱: ")
# sql = """select * from goods where name = '%s';""" % find_name
# print("-->%s<--"% sql)
# self.execute_sql(sql)
sql = """select * from goods where name = %s"""
# 元祖列表都行, 可迭代都好
self.cs.execute(sql, [find_name])
print(self.cs.fetchall())
# sql = """select * from goods where name = %s"""
# 如此就沒有'%s'的漏洞可以讓人補
#
# 注意:
# 如果有多個參數, 那%s就要有多個讓函數填
# [V1, V2,...]
```
## 視圖
> - Select語句返還的結果,
> - 對若干表引用,
> - 原數據表更改數據, 視圖亦同步修改
#### 建立
```SQL
-- 建立
-- create view 視圖名 as select語句;
-- 建議以 v_ 開頭
create view v_goods_info as select
g.*, c.name as cate_name, b.name as brand_name from goods as g
left join goods_cates as c on g.cate_id = c.id
left join goods_brands as b on g.brand_id = b.id;
-- 查詢
-- select * from v_goods_info;
-- 刪除
-- drop view 視圖名;
drop view v_goods_info
```
#### 優點
> - 修改數據而不影響程序運行
> - 當程序編碼的SQL語句都以視圖為主時, 原數據重構, 不影響程序運行
> - 注意! 修改數據是修改原數據表的數據, 而非修改視圖
> ```SQL
> update v_goods_info set name = "GodJJ" where id = 21;
> ERROR: The target table v_goods_info of the UPDATE is not updatable
> ```
> - 提高安全性
> - 僅提供用戶視圖, 可以選擇性提供信息
> - SQL簡潔
> - select xxxx 好長一坨, 把他弄成一張視圖
> - `select * from xx;` 搞定
>
## 事務
> 保證操作序列要馬全部成功, 要馬全部失敗
> 舉例: A轉帳給B, A-10, B+10
> - 檢查A帳戶>10
> - A-10
> - B+10
> - 為了避免A扣款後系統故障而導致B沒加10的窘境
> - 三個都完成才提交,
> - 說白話就是 connect.commit 與 connect.rollback 的操作
```SQL
-- 1. 打開事務
start transaction;
-- 或
begin;
-- 2. 操作序列
select balance from checking where customer_id = 1;
update checking set balance = balance - 10 where customer_id = 1;
update savings set balance = balance + 10 where customer_id = 2;
-- 3. 提交
commit;
-- 3. 退回
rollback;
-- Python在使用pymysql的增刪改時, 預設就是開啟事務, 故最後才需要commit
-- MySQL也是, 惟每次Enter後會自動調用commit
```
#### 事務4特性(ACID)
> 1. 原子性(atomicity)
> - 事務不可分割, 不會只改其中一部分(A-10)
> 2. 一致性(consistency)
> - 要馬成功, 要馬不成功
> 3. 隔離性(isolation)
> - 當你手動開啟事務時, 打完操作序列後`select * from xx;`
> - 此時你看到的表是已修改的
> - 惟另外開啟一個MySQL來查看時會發現尚未修改!!
> - 因為該修改僅僅存於本地緩存中
> ```SQL
> -- MySQL1
> start transaction;
> update goods set name = "GodJJ" where id = 21;
> select id, name from goods;
> | 21 | Toyz喊在 |
>
> -- MySQL2
> select id, name from goods;
> | 21 | 商務雙肩背包 |
> ```
> - 另外, 當兩個客戶端欲更改『同一行數據』時, (不同的沒差)
> - 必須等先開啟事務者commit後才能操作
> ```SQL
> -- MySQL2
> -- 此時MySQL1還沒結束, 故須等待, 等待時間過久就會報錯
> update goods set name = "Toyz喊在" where id = 21;
> ERROR: Lock wait timeout exceeded; try restarting transaction
>
> -- MySQL1
> commit;
>
> -- MySQL2
> update goods set name = "Toyz喊在" where id = 21;
> Query OK, 1 row affected
>
> -- 此設定類似於線程搶鎖, 誰搶到誰用, 沒搶到就等
> -- 故將兩個客戶端隔離了
> ```
> 4. 持久性(durability)
> - 提交後永久保存於數據庫
>
## 索引
> - 對數據表裡所記錄的引用指針
> - InnoDB 數據表組成本來就包含索引, 有些引擎是獨立成一個文件
> - 主鍵外鍵其實就是索引的具體實現
```SQL
-- 查看索引
Show index from 表名;
-- 創建索引
-- 字段類型是字符串時需指定長度, 一般跟定義相同即可
-- 不是字符串就不用寫長度
create index 索引名 on 表名(字段名(長度))
-- 刪除索引
drop index 索引名 on 表名
```
```SQL
-- 創建了一個十萬筆資料的數據
select count(*) from test_index;
+----------+
| count(*) |
+----------+
| 100000 |
+----------+
-- 打開時間監測
set profiling=1;
-- 查詢最後一筆
select * from test_index where title='ha-99999';
-- 建立索引
create index title_index on test_index(title(10));
-- 查詢最後一筆
select * from test_index where title='ha-99999';
-- 查看時間
show profiles;
+----------+------------+---------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+---------------------------------------------------+
| 1 | 0.03428600 | select * from test_index where title='ha-99999' |
| 2 | 0.15015800 | create index title_index on test_index(title(10)) |
| 3 | 0.00240100 | select * from test_index where title='ha-99999' |
+----------+------------+---------------------------------------------------+
```
> - SQL索引是透過B Tree 的數據結構來完成
> 
## 帳戶管理
> - database裡有一個mysql數據庫, 裡面管理所有帳戶相關
> `use mysql;`
> - 用戶管理及權限存在user數據庫的user表中
> `desc user;`
> - user表有各種字段, 其中
> `select host, user, authentication_string from user;`
> - Host: 允許訪問主機
> - % 表示任意電腦都可
> - Localhost 表示本地
> - User: 用戶名
> - authentication_string: 密碼
> - 該行顯示的值為加密後的密碼
#### 創建用戶
```
# 使用root帳戶
$ mysql -uroot -p
```
```SQL
-- 創建用戶
-- create user '用戶名'@'訪問主機' identified by '密碼';
create user 'godjj'@'localhost' identified by '123456'
select user, host, authentication_string from user;
| godjj | localhost | 加密後密碼 |
-- 查看權限
-- show grants for 用戶名@訪問主機;
show grants for godjj@localhost;
| GRANT USAGE ON *.* TO `godjj`@`localhost` |
-- 授與權限
-- grant 權限 on 數據庫 to 用戶名@訪問主機
-- 主要權限包含: create、alter、drop、insert、update、delete、select
-- 分配所有權限: all privileges
grant select on jing_dong.* to godjj@"localhost" identified by "123456"
show grants for godjj@localhost;
| GRANT USAGE ON *.* TO `godjj`@`localhost` |
| GRANT SELECT ON `jing_dong`.* TO `godjj`@`localhost` |
```
```
$ mysql -ugodjj -p123456
```
```SQL
show databases;
| information_schema |
| jing_dong |
use jing_dong;
select * from goods;
21 rows in set (0.00 sec)
select * from goods_cates;
11 rows in set (0.00 sec)
-- 剛剛只有給select, 故只能做select操作
update goods set name = "Toyz" where id = 21;
Error: UPDATE command denied to user 'godjj'@'localhost' for table 'goods'
```
#### 修改權限
```
$ mysql -uroot -p
```
```SQL
-- 修改權限
-- grant 權限 on 數據庫 to 用戶名@訪問主機 with grant option;
-- 刷新權限
flush privileges;
```
```
$ mysql -ugodjj -p123456
```
```SQL
use jing_dong;
update goods set name = "Toyz" where id = 21;
Query OK, 1 row affected (0.01 sec
```
#### 修改密碼
```
$ mysql -uroot -p
```
```SQL
use mysql;
-- alter user '用戶'@'訪問主機' indentified by '密碼';
ALTER USER 'godjj'@'localhost' IDENTIFIED BY '123';
flush privileges;
-- update user set authentication_string=password('密碼') where user='用戶';
-- 一直語法錯誤, 不知道為啥
```
#### 遠程登入
`用戶@'%'`
```
$ mysql -u帳號 -p密碼 -hIP -p端口
$ mysql -utoyz -p123 -h192.111.23.34 -p3306
```
> - Ubuntu 要把mysqld.cnf文件裡的bind-address 給註釋(#)掉
> - 非必要絕對不要用!
> - 避免有心人士使用[IP端口掃描器](https://zh.wikipedia.org/wiki/%E7%AB%AF%E5%8F%A3%E6%89%AB%E6%8F%8F%E5%B7%A5%E5%85%B7)找到你的IP端口
> - 大家的帳戶都有個root, 接著只要瘋狂輸入密碼, 被找到就進去了, 接著你的Database就GG了
> - 替代方案: SSH, 帳號不知道的話很難破解, 但只缺密碼難度就相對低了
#### 刪除帳戶
```SQL
-- 1. root登入
-- 2. drop user '用户'@'主機';
drop user godjj@localhost;
-- 或
-- 2. delete from user where user='用户';
-- 沒試過
```
## 主從(我還沒成功過XD)
> 讀寫分離,負載均衡,數據備份
```
Step1 手動備份
# 主備份
$mysqldump -uroot -p 數據庫名 > 備份名.sql
$ mysqldump -uroot -p jing_dong > test.sql
# 從導入
# 1. 先登入msyql, 創建一個數據庫
# 2. 退出登入, $ mysqldump -uroot -p 數據庫名 < 備份名.sql
$ mysqldump -uroot -p test < test.sql
# 或
use 數據庫;
source xxx.sql;
-------------------------------------------------
# 註: 如果一開始就倒出所有數據, 導入就不用寫數據庫了
$ mysqldump -uroot -p --all-databases --lock-all-tables > ~/test2.sql
# --lock-all-tables: 防止操作時有數據修改
# > 後面可以寫位置, 沒寫就是當前
$ mysql –uroot –p < ~/test2.sql
```
```
Step2 調整設置
# Ubuntu
$ sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
# 主服務器把 server-id 與 log_bin 打開(刪掉#)
# 從服務器把 server-id打開
# server-id 不能一樣, 習慣上都會改成IP最後的值
# 配置完後sudo service mysql restart
```
```SQL
Step3 連接
# 主服務器創建用於同步的帳號
-- create user '用戶名'@'訪問主機' identified by '密碼';
create user 'slave'@'%' identified by '123';
-- grant 權限 on 數據庫 to 用戶名@訪問主機
grant replication slave on *.* to 'slave'@'%';
flush privileges;
# 查詢日誌文件名與位置 master_log_file, matser_log_pos
show master status;
# 從服務器設置連接
change master to master_host='主服務器IP',
master_user='同步用戶帳號', master_password='同步用戶密碼',
master_log_file='日誌文件名', master_log_pos=日誌文件位置;
# 開啟同步
start slave;
# 查看同步狀態
show slave status \G;
...
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
...
# 表示同步運行正常
```