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為主 -- 右連接[圖三] > 很少用, 因為左連接的兩表對調即可達成效果 ``` > - 圖一 > ![](https://i.imgur.com/WYEciY0.png) > - 圖二 > ![](https://i.imgur.com/CzpAVk3.png) > - 圖三 > ![](https://i.imgur.com/zKGBCoh.png) ### 自關聯 > 一個表中的值使用表中另一個值 ``` --外鍵-- # 表一 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 的數據結構來完成 > ![](https://i.imgur.com/IBrO6U6.png) ## 帳戶管理 > - 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 ... # 表示同步運行正常 ```