--- tags: 學習筆記, DB --- SQL 基本 ====== ## SQL 是什麼 幫助我們記憶許多的資料,如果不希望遺失任何資訊,SQL 會成為我們的最佳選擇。 SQL 可以幫我們把資料做好分類並適當的保存,就像是表格一樣。 |last_name|first_name|email| |:---:|:---:|:---:| |Branson|Ann|annie@boards-r-us.com| |Hamilton|Jamie|dontbother@breakneckpizza.com| |Soukup|Alan|soukup@breakneckpizza.com| ### 與 Excel 的不同 除了提供空間幫助我們儲存資料外,他也為這些 SQL structure 或 table 做了許多關聯以便操作。 ### 名詞釋義 - database: 資料庫 - table:資料表,一個資料庫可以有許多資料表就像是上面的 table 一樣紀錄許多類似內容的資料。 - row:經常與 record 一起被提及 ### 如何使用資料庫 使用 SQL 指令輸入命令使用,以下皆為 MySQL 語法。 ### 建立資料庫 資料庫命名不得有空白 ```sql CREATE DATABASE myDataBase; ``` ### 使用資料庫 透過 use 指令我們可以指定要使用哪個資料庫來操作。 ```sql USE mydataBase; ``` ### 建立資料表 透過 CREATE TABLE 可以建立資料表並定義要記錄的資料欄位以及資料型態。 ```sql CREATE TABLE person ( firstName VARCHAR(10), lastName VARCHAR(10), email VARCHAR(20) ); ``` ### 常見形態 - VARCHAR:文字,透過(數字)表達允許儲存多長的內容。 - CHAR:與 VARCHAR 主要差異在於 CHAR 所限制的長度是固定的,通常適用於該欄位資料格式一致。 - INT - DATE - TIME - FLOAT ### 檢視資料表的定義 ```sql DESC person; ``` |Column|Type|Null|Key|Defult|extra| |:---:|:---:|:---:|:---:|:---:|:---:| |firstName|varchar(10)|YES||NULL|| |lastName|varchar(10)|YES||NULL|| |email|varchar(20)|YES||NULL|| 不能重複建立已存在的資料庫或資料表。 那麼當我們發現之前定義的資料表有些疏漏,需要重新增加欄位該怎麼辦?重新來過。 ### 清空資料表 ```sql DROP TABLE person; ``` 將會移除該資料表並清空該資料表中所有資料,並告訴你這次指令影響了多少資料。 ### 插入資料 完成資料庫及資料表的建置後,我們就可以正式把我們希望紀錄的資料輸入。 ```sql INSERT INTO person(firstName, lastName, email) VALUES('Branson', 'Ann', 'annie@boards-r-us.com'); INSERT INTO person VALUES('Branson', 'Ann', 'annie@boards-r-us.com'); INSERT INTO person(firstName) VALUES('Branson'); ``` 未輸入插入表格欄位順序時將依照資料表定義順序來插入資料。 如果該資料欄位有設置預設值時,可以省略該欄位的資料插入,這種情況將會以預設值取代。 ### 插入多筆資料 ```sql INSERT INTO person VALUES('Branson', 'Ann', 'annie@boards-r-us.com') , ('person2', 'person22', 'person2@boards-r-us.com') , ('person3', 'person3', 'person3@boards-r-us.com'); ``` ### 取得資料 ```sql SELECT * FROM person; ``` |last_name|first_name|email| |:---:|:---:|:---:| |Branson|Ann|annie@boards-r-us.com| |Hamilton|Jamie|dontbother@breakneckpizza.com| |Soukup|Alan|soukup@breakneckpizza.com| ```sql SELECT firstName FROM person; ``` |first_name| |:---:| |Ann| |Jamie| |Alan| ### NULL 正常來說,我們並不期望自己的資料中有 NULL 的存在,畢竟那意味著資料的不完善。因此很多時候我們建立資料表時就會限制該欄位為 NOT NULL。 ```sql CREATE TABLE person ( firstName VARCHAR(10) NOT NULL, lastName VARCHAR(10) NOT NULL, email VARCHAR(20) NOT NULL, money INT NOT NULL DEFAULT 0; ); ``` 透過這樣的方式我們可以要求欄位不得為空,甚至設定好一些初始值以防漏填出錯。 ## SELECT 很多時候我們並不期望取得所有的資料,也許只想要符合要求的某部分資訊而已。 因此 SELECT 提出了強大的搜尋功能,幫助我們篩選這些資料。 ### WHERE:選取符合要求的資料 ```sql SELECT * FROM person WHERE firstName = 'Ann'; ``` |last_name|first_name|email| |:---:|:---:|:---:| |Branson|Ann|annie@boards-r-us.com| 進一步簡化我希望取得的資訊。 ```sql SELECT email FROM person WHERE firstName = 'Ann'; ``` |email| |:---:| |annie@boards-r-us.com| ### single quote ' 這是個特殊的符號兩兩出現標注出一些特殊的資料: - CHAR - VARCHAR - DATE - DATETIME - TIME - TIMESTAMP - BLOB 也因此如果我們真的希望在儲存的文字中儲存`'`,需要透過`\`來跳脫。 `\'` ### AND OR WHERE 子句可以幫我們將結果限制在某一個條件,然而我們有時候需要一些更複雜的條件。 ```sql SELECT location FROM doughnut_rating WHERE type = 'plai\'n glazed' AND rating = 10; ``` type 必須是 plain glazed 而且 rating 為 10 ```sql SELECT location FROM doughnut_rating WHERE type = 'plain glazed' OR rating = 10; ``` type 是 plain glazed 或 raing 為 10 ### =, >, <, <>, <=, >= - =:相等 - \>:左邊大於右邊 - <:左邊小於右邊 - <>:不相等 - <=:左邊小於等於右邊 - \>=:大於等於 ### NULL 某些時候我們也會希望特定出資料為空的資料。 我們可以使用 is 來選擇。 ```sql SELECT * FROM drink_info WHERE calories is NULL ``` ### LIKE 關於字串的尋找,很多時候我們想要找的是一些相近資料,例如姓許的人、姓陳的人。 ```sql SELECT name, career FROM person WHERE career LIKE '%師%'; ``` 透過 IKE 關鍵字以及%(表萬用字元,可以是任意數量的任何文字)_(表萬用字元,一個任意文字),以此例我們可以找出職業有師字的人,老師、工程師、師父。 萬用字元同時也可以是沒有字的意思->'老師'師後面沒東西但也會被找出來。 ### BETWEEN 透過 BETWEEN 關鍵字,我們可以取出符合某個數值至某個數值的資料。 ```sql SELECT drink_name FROM drink_info WHERE calories BETWEEN 30 AND 60; ``` 注意:BETEEN ? AND ? 必須小的數值在前。 ### IN ```sql SELECT date_name FROM black_book WHERE rating = 'innovative' OR rating = 'fabulous' OR ....; ``` 當我們希望取得符合某個欄位某些值的資料時,雖然已經可以透過寫一長串的 OR 來達成,但 SQL 提供了我們更方便的關鍵字 IN。 ```sql SELECT date_name FROM black_book WHERE rating IN ('innovative', 'fabulous', 'delightful', 'pretty good'); ``` ### NOT WHERE 子句中我們已經學會了許多種取出特定資料的方法: - AND - OR - LIKE - \>, <, <>, >=, <= - LIKE - BETWEEN - IN 現在我們還可以透過 NOT 關鍵字來反轉結果。 ```sql SELECT name FROM person WHERE NOT age > 10; ``` ```sql SELECT name FROM person WHERE age <= 10; ``` ## DELETE, UPDATE ### DELETE 有些時候我們也許會有些不必要的舊資料希望刪除,這時候我們可以利用 DELETE 以及 WHERE 來鎖定刪除我們不需要的資料。 ```sql DELETE FROM clown_info WHERE activities = 'dancing'; ``` ### DELETE 的限制 - 只能刪除完整資料,不能只刪除某一欄資料。 - DELETE 不加 WHERE 子句將會把該表中所有資料刪除。 - 但也許這種情境你會想用 TRUNCATE 語法 ```sql TRUNCATE TABLE clown_info; ``` ### UPDATE 雖然我們可以利用 DELETE 及 INSERT 來達成修改資料的目的,但這樣很容易產生一些問題以及產生麻煩,因此單純想修改資料時最好使用 UPDATE 及 WHERE 子句來修改特定資料。 ```sql UPDATE doughnut_rating SET type = 'glazed' WHERE type = 'plain glazed'; UPDATE table_name SET first_colunm = first_new_value , second_column = second_new_value WHERE type = 'plain glazed'; UPDATE drink_info SET cost = cost + 1 WHERE dring_name IN('Blue Moon' , 'Oh My Gosh' , 'Lime Fizz'); ``` ## 資料表設計 目前來說我們已經可以正常的使用資料庫功能了,能夠產生我們想要的資料表,紀錄我們的資料,隨時取得符合要求的資料,刪除或是修改這些資料。 然而,一個隨意設計的資料表可能會讓我們陷入不斷修改的困境,也許需要加入一個欄位、去除一個欄位、不斷重複的修正某些資料。 欄位的設計應該遵循一些原則: 1. 你的資料表想要描述什麼 2. 使用這張表時希望得到哪些資料 3. 整理這些需求並將資料化為適合達到這些目的的最小單位 4. 確定這張表所要描述的東西 5. 你將透過什麼來查找這張表(優化搜尋的方式) 6. 資料是否有達成最小單位以讓搜尋簡單而有效 具體來說我們可以: 1. 消除每個資料表中同類型的欄位 2. 為每個相關的資料建立一個獨立的資料表 3. 使用一個主鍵值(KEY)來識別每一筆資料 ### KEY 又叫做 Primary Key,可以幫助我們識別資料表中的資料,因為 Primary Key 不能為 NULL、插入資料時必須決定、在整張表中這個 Key 是唯一的、無法被改變。如果在資料表中無法找到適合的主鍵欄位,我們新增一欄流水序號定義一個簡單的主鍵。 ### 重建資料表 我們可以直接刪除整張表並重新建立來完成這項工作。 ```sql CREATE TABLE my_contacts ( contact_id INT NOT NULL AUTO_INCREMENT, last_name VARCHAR(30) DEFAULT NULL, first_name VARCHAR(30) DEFAULT NULL, email VARCHAR(50) DEFAULT NULL, gender CHAR(1) DEFAULT NULL, birthday DATE DEFAULT NULL, ...., ...., ...., PRIMARY KEY(contact_id) ); ``` 然而這樣也意味著會失去所有過往儲存的資料,因此 SQL 提供了 ALTER。 ### ALTER ```sql ALTER TABLE my_contacts ADD COLUMN contact_id INT NOT NULL AUTO_INCREMENT FIRST, ADD PRIMARY KEY(contact_id); ``` 我們可以透過在 ADD COLUMN 最後加上 FIRST、LAST、SECOND、AFTER column_name、BEFORE column_name 來調整新欄位加入的位置。 除了新增欄位外我們其實也可以透過 ALTER 與其他語句的配合做到: - CHANGE:改變某個欄位的名稱及類型 - MODIFY:改變某個欄位的位置以及類型 - DROP:移除某個欄位 ### 資料表更名 ```sql ALTER TABLE projekts RENAME TO project_list; ``` ### 一些變更資料表的例子 |Filed|Type|Null|Key|Default|Extra| |:---:|:---:|:---:|:---:|:---:|:---:| |number|INT(11)|YES||NULL|| |descriptionofproj|VARCHAR(50)|YES||NULL|| |contractoronjob|VARCHAR(10)|YES||NULL|| ```sql ALTER TABLE project_list CHANGE COLUMN number proj_id INT NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY('proj_id'); ``` |Filed|Type|Null|Key|Default|Extra| |:---:|:---:|:---:|:---:|:---:|:---:| |proj_id|INT|NO|YES||| |descriptionofproj|VARCHAR(50)|YES||NULL|| |number|VARCHAR(10)|YES||NULL|| ```sql ALTER TABLE project_list CHANGE COLUMN descriptionofproj proj_desc VARCHAR(100), CHANGE COLUMN contractoronjob con_name VARCHAR(30); ``` |Filed|Type|Null|Key|Default|Extra| |:---:|:---:|:---:|:---:|:---:|:---:| |number|INT|NO|YES||| |proj_desc|VARCHAR(100)|YES||NULL|| |con_name|VARCHAR(30)|YES||NULL|| 注意:若某些字串形式資料更改成更短的格式限制,很可能會切掉一些舊的資料。 例如:VARCHAR(5) -> VARCHAR(1) BOB -> B ### 拆分欄位 為了正規化資料,我們很常會需要將一個欄位拆成許多欄位,為了不喪失資料,甚至讓舊的資料協助我們填充新欄位,我們可以透過 SQL 提供的一些 STRING FUNCTION 來切割字串(可以用在 SELECT、UPDATE、DELETE)。 - RIGHT('STRING', 2):NG - LEFT('STRING', 2):ST - SUBSTRING('San Antonio, TX', 5, 3):Ant(從第五個字開始長度為三的子字串) - UPPER('uSa'):USA - LOWER('uSa'):usa - REVERSE('spaGHEtti'):ittEHGaps - LTRIM(' catfood '):`catfood ` - RTRIM(' catfood '):` catfood` - LENGTH(' catfood '):` catfood` TestTable |location| |:---:| |Seattle, WA| |Natchez, MS| |Las Vegas, NV| |Palo Alto, CA| ```sql UPDATE TestTable SET location = RIGHT(location, 2); ``` TestTable |location| |:---:| |WA| |MS| |NV| |CA| ### CASE, WHEN, THEN, ELSE |movie_id|title|rating|drama|comedy|for_kids|cartoon|purchased| |:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |1|Monsters, Inc.|G|F|T|T|T|3-6-2002| |2|The Godfather|R|F|F|F|F|2-5-2001| |3|Gone with the Wind|G|T|F|F|F|11-20-1999| 取出資料時我們看著 T、F 難以辨識這些資料的實際含意。 應該將這些資料統整成一個 category。 ```sql UPDATE movie_table SET category = 'drama' WHERE drama = 'T'; UPDATE movie_table SET category = 'comedy' WHERE comedy = 'T'; UPDATE movie_table SET category = 'family' WHERE for_kids = 'T'; UPDATE movie_table SET category = 'family' WHERE cartoon = 'T' AND RATING = 'G'; UPDATE movie_table SET category = 'misc' WHERE cartoon = 'T' AND RATING <> 'G'; ``` 但這樣寫又顯得太麻煩。 ```sql UPDATE movie_table SET category = ( CASE WHEN drama = 'T' THEN 'drama' WHEN comedy = 'T' THEN 'comedy' WHEN for_kids = 'T' THEN 'family' WHEN cartoon = 'T' AND RATING = 'G' THEN 'family' WHEN cartoon = 'T' AND RATING <> 'G' THEN 'misc' ELSE 'other' END ); ``` 除了用於 UPDATE,CASE 還可作用於 SELECT、INSERT、DELETE。 ## 進階 SELECT 單純的取出資料表內容未必能完全滿足我們的需求,這時我們需要更強力的工具來協助我們調整資料呈現。 ### ORDER 當我們希望取出資料時,資料是經過排序的。可以使用 ORDER 來協助我們。 ```sql SELECT title, category FROM movie_table WHERE category = 'family' ORDER BY title; ``` |title|category| |:---:|:---:| |1 Crazy Alien|family| |10 Big Bugs|family| |101 Alsation|family| |....|family| |....|family| |....|family| |....|family| |....|family| 此時會以 SQL 的 order rule 來排序。 012345abcdABCD!@#$% 排序時也可以要求其他欄位也加入排序。 ```sql SELECT title, category, purchased FROM movie_table WHERE category = 'family' ORDER BY title, purchased; ``` 可透過 DESC 關鍵字去反轉排序順序,預設為 ASC。 ```sql SELECT title, category, purchased FROM movie_table WHERE category = 'family' ORDER BY title ASC, purchased DESC; ``` ### GROUP、SUM、AVG |ID|fistName|sales|saleDate| |:---:|:---:|:---:|:---:| |1|Lindsay|32.02|3-6-2007| |2|Paris|26.53|3-6-2007| |3|Britney|11.25|3-6-2007| |4|Nicole|18.96|3-6-2007| |5|Lindsay|9.16|3-7-2007| |6|Paris|1.52|3-7-2007| |7|Britney|43.21|3-7-2007| |8|Nicole|8.05|3-7-2007| 有時候我們會儲存這樣的資料,也許我想知道某一天的銷售額總額,也許想知道某人的總銷售額。 這時候 GROUP 跟一些 MATH FUNCTION 就派上用場了。 ```sql SELECT SUM(sales) FROM coockie_sales WHERE first_name = 'Nicole'; ``` |SUM(sales)| |:---:| |27.01| 透過 SUM 以及 WHERE 我們已經可以知道某人的銷售總額了,但這樣要取出所有人的各自總額就顯得很麻煩。 ```sql SELECT firstName, SUM(sales) FROM coockie_sales GROUP BY firstName ORDER BY SUM(sales); ``` |firstName|SUM(sales)| |:---:|:---:| |Nicole|27.01| |Paris|28.05| |Lindsay|41.18| |Britney|54.46| 透過 GROUP 將資料分組,可以幫助我們取得每個人各自的總銷售額。 ```sql SELECT firstName, AVG(sales) FROM coockie_sales GROUP BY firstName ORDER BY AVG(sales); ``` |firstName|AVG(sales)| |:---:|:---:| |Nicole|13.505| |Paris|14.025| |Lindsay|20.59| |Britney|27.23| ### MAX、MIN 透過 GROUP 以及 MIN、MAX 的組合,我們也可以將資料集中最大或最小的資料找出。 ```sql SELECT firstName, MIN(sales) FROM coockie_sales GROUP BY firstName; ``` |fistName|MIN(sales)| |:---:|:---:| |Britney|11.25| |Lindsay|9.16| |Paris|1.52| |Nicole|8.05| 除此之外還有一種 COUNT FUNCTION,可以告訴你資料表中有多少資料被選取。 ```sql SELECT COUNT(saleDate) FROM coockie_sales GROUP BY firstName; ``` 4 ```sql SELECT COUNT(saleDate) FROM coockie_sales; ``` 8 ### DISTINCT ```sql SELECT DISTINCT saleDate FROM coockie_sales ORDER BY saleDate; ``` |saleDate| |:---:| |3-6-2007| |3-7-2007| ```sql SELECT COUNT(DISTINCT saleDate) FROM coockie_sales; ``` 2 透過 DISTINCT 關鍵字可以讓重覆資料只出現一次。 ### LIMIT 有時候我們未必需要取得所有資料,可能只會需要前三位或是第幾位資料。 這時候我們可以利用 LIMIT 來縮限所需的資料。 ```sql LIMIT $1 LIMIT $1,$2 ``` LIMIT 可給予一個或兩個參數,只給予一個參數時結果會僅止給出 $1 筆資料;給予兩個參數時會先跳過 $1 筆資料再取得 $2 筆資料。 ```sql SELECT firstName, SUM(sales) FROM coockie_sales GROUP BY firstName ORDER BY SUM(sales); ``` |firstName|SUM(sales)| |:---:|:---:| |Nicole|27.01| |Paris|28.05| |Lindsay|41.18| |Britney|54.46| 以此例 ```sql SELECT firstName, SUM(sales) FROM coockie_sales GROUP BY firstName ORDER BY SUM(sales) LIMIT 2; ``` |firstName|SUM(sales)| |:---:|:---:| |Nicole|27.01| |Paris|28.05| ```sql SELECT firstName, SUM(sales) FROM coockie_sales GROUP BY firstName ORDER BY SUM(sales) LIMIT 1,1; ``` |firstName|SUM(sales)| |:---:|:---:| |Paris|28.05| 跳過第一筆資料取得一筆資料意味著選取第二筆資料。 ## 過大的資料表 在資料不龐大時,單一資料表足以應對所有問題。然而隨著資料不斷增加,原本好用的資料表可能漸漸的變得越來越龐大,搜尋也越來越複雜,浪費了空間以及效能。 例如我們可能有個 ||| |:---:|:---:| |contactId|873| |lastName|Suliivan| |firstName|Regies| |phone|5552311122| |me@kathieleeisaflake.com| |gender|M| |birthday|1955-03-20| |profession|Comdian| |city|Cambridge| |state|MA| |status|single| |interests|animals, trading cards, geocaching| |seeking|single F| 這樣一個表表示一個人的基本訊息,以及興趣、單身與否。 興趣欄位顯然是一個會有不定數量資料的欄位,當我們希望尋找有相似興趣的人時會變得十分麻煩。 ```sql SELECT name, interests FROM my_contacts WHERE interests LIKE '%animals%' AND interests LIKE '%trading cards%' AND interests LIKE '%geocaching%'; ``` 這樣可以幫助我們找到有完全相同興趣的人,但 query 的撰寫變得十分麻煩,我們需要透過寫死的方式輸入各個興趣的資料。 ```sql SELECT name, interests FROM my_contacts WHERE SUBSTRING_INDEX(intersts, ',', 1) = 'animals'; ``` 為了讓搜尋能夠較為適用,我們改為只要求第一個興趣相同即可(會產生些許問題,配對到的人無法足夠精準),而且需要透過 SUBSTRING_INDEX 去切割興趣欄位。 如果將只有一個 interests 改為 interest1, interest2, interest3? ||| |:---:|:---:| |contactId|873| |lastName|Suliivan| |firstName|Regies| |phone|5552311122| |me@kathieleeisaflake.com| |gender|M| |birthday|1955-03-20| |profession|Comdian| |city|Cambridge| |state|MA| |status|single| |interest1|animals| |interest2|trading cards| |interests|geocaching| |seeking|single F| ```sql SELECT name, interests FROM my_contacts WHERE (( interest1 = 'animals' OR interest2 = 'animals' OR interest3 = 'animals' ) AND ( interest1 = 'trading cards' OR interest2 = 'trading cards' OR interest3 = 'trading cards' ) AND ( interest1 = 'geocaching' OR interest2 = 'geocaching' OR interest3 = 'geocaching' ) ); ``` 省了 SUBSTRING_INDEX 的麻煩,也能保證結果精準,但顯得資料表更加多於冗長了。 因此根本問題在於資料表設計得太過冗雜,我們應該想辦法將這張大表格切割成幾張小表格,好簡化搜尋的難度。 將性質類似的欄位分割出來獨立成一張表格,並與原本資料使用一個簡單的 schema 來關聯。 例如:我們可以直接將 interests 取出來獨立成一張表, |interests| |:---:| |interestId| |interest| 接著,與原資料關聯。 || |:---:| |contactId| |lastName| |firstName| |phone| |me@kathieleeisaflake.com| |gender| |birthday| |profession| |city| |state| |status| |seeking| |interests| |:---:| |interestId| |interest| |lastName| |firstName| 現在我們可以透過兩次 SQL 執行來達成一些原本的工作。 不過目前仍然有個問題,如果有人同名同姓有不同的興趣時關聯會出現問題。 |interests| |:---:| |interestId| |interest| |contactId| 因此我們改用表中唯一的 Primary Key(contactId)做成 Foreign Key 來作關聯。 ### Foreign Key Foreign Key 用來幫助我們聯結其他表格,與 Primary Key 有些不同: - 可以為 NULL - 不必是唯一的(通常情況下常常不是唯一) - 聯結的兩個欄位必須是相同類型相同寬度的 Foreign Key 為 NULL 時,代表這筆資料並未與其他表格之資料產生聯結。 儘管 Foreign Key 可以為 NULL,但我們也可以透過 constrant 去限制必須與其他表格資料聯結。 我們可以要求 Foreign Key 必須為父表格中已存在的資料(關聯完整性)。 ```sql CREATE TABLE interests ( interestId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, interest VARCHAR(50) NOT NULL, contactId INT NOT NULL, CONSTRAINT myContacts_contactId_fk FOREIGN KEY(contactId) REFERENCES myContacts(contactId); ) ``` 資料表上的關聯通常有三種形式,主資料對副資料: - 一對一 - 一對多 - 多對多 當我們定義好這些資料的關聯方式後,我們就可以輕易的設計出我們需要的資料表,我們的資料欄位也會變得更加簡單明瞭。 ### 一對一關聯 |employeeId(PK)|firstName|lastName| |:---:|:---:|:---:| |1|Beyonce|knowles| |2|Shawn|Carler| |3|Shakira|Ripoll| |ssn(PK)|salaryLevel|employeeId(FK)| |:---:|:---:|:---:| |234567891|2|6| |345678912|5|35| |123456789|7|1| 這樣就屬於一個簡單的一對一關聯,一個員工(主資料)對應一個屬於他的薪水規定(子資料)。然而實際上我們並不常採用這樣的關聯方式,實際採用一對一關聯時多為: 1. 加快執行 SQL 的速度,當我們在這樣的例子中希望修改某一人特定的薪水規定時,只需要在 salary 中 SELECT 出正確的 ssn 就可以完成修改。 2. 透過這樣的分離方式,可以將員工與薪水孤立開來,保護限制薪水資料的存取。 3. 假設有較大資料的資料希望儲存(BLOB),通常也會將該檔案與主資料分隔開來。 ### 一對多關聯 |profession| |:---:| |professionId(PK)| |profession| |person| |:---:| |contactId(PK)| |lastName| |firstName| |phone| |me@kathieleeisaflake.com| |gender| |birthday| |professionId(FK)| |city| |state| |status| |seeking| 由於在 profession 表中 professionId 為 PK 因此整張表中同樣的 professionId 只能有一個,在 person 中 professionId 是 FK,並不需要是唯一的。因此可能有很多人共同擁有同一個 professionId。 具體表現形式就像是,一個 profession 可能可以被很多 person 擁有。 ### 多對多關聯 有些時候我們可能會發生一些複雜的關聯行為,例如上面 interest 的例子,一個人可能有很多種喜好,一種喜好可能會有人多人擁有。 這種情況會使狀況使資料的聯結變得複雜,我們很難去維護資料完整性,因此我們可能會透過一個中介資料表,取代多對多的關聯。 `A->B<-C` - A 對 B 是一對多的關聯 - C 對 B 也是一對多的關聯 |womanId(PK、FK)|woman| |:---:|:---:| |1|Carrie| |2|Samantha| |3|Charlotte| |4|Miranda| |womanId(FK)|shoeId(FK)| |:---:|:---:| |1|3| |1|4| |2|1| |3|1| |3|2| |3|3| |3|4| |4|1| |4|3| |4|4| |shoeId(PK、FK)|shoeName| |:---:|:---:| |1|Manolo Strappies| |2|Crocs Clogs| |3|Old Navy Flops| |4|Prada boots| ### First Normal Form(1NF) 1. 欄位資料屬於最小原子 2. 沒有重複的資料欄位 3. 有一個 PK,且其他欄位功能相依於 PK |toyId|toy|colors| |:---:|:---:|:---:| |5|whiffleball|white, yellow, blue| |6|frisbee|green, yellow| |9|kite|red, blue, green| |12|yoyo|white, yellow| 資料不屬於最小原子 |toyId|toy|color1|color2|color3| |:---:|:---:|:---:|:---:|:---:| |5|whiffleball|white|yellow|blue| |6|frisbee|green|yellow|| |9|kite|red|blue|green| |12|yoyo|white|yellow|| 有重複的資料欄位 |toyId(PK)|toy| |:---:|:---:| |5|whiffleball| |6|frisbee| |9|kite| |12|yoyo| |toyId(FK、PK)|color(PK)| |:---:|:---:| |5|white| |5|yellow| |5|blue| |6|green| |6|yellow| |9|red| |9|blue| |9|green| |12|white| |12|yellow| 採用複合 PK 讓 color 表每個欄位都有唯一的 PK。 ### Functional Dependency 功能相依:描述表中屬性的相依關係,是關聯資料庫的基礎,1NF 就是要找出這些相依性。 找出屬性間的功能相依,可以幫助我們找出重複資料以及 PK(若表中所有屬性都功能相依於某個屬性那該屬性就是 PK、所有屬性都功能相依於某組屬性,那組屬性就是候選鍵) 若關聯表 R 有屬性 A 與 B,A 與 B 可以是複合屬性。 如果 B 功能相依於 A,或稱 A 功能決定 B,則寫成:R.A→R.B,或簡寫成:A→B A→B 表示知道 A 的值就可以決定 B 的值。 功能相依又可以分為: - 完全相依:A 和 B 是關聯表屬性,A 或 B 可能是複合屬性,B 功能相依於 A,而且 B 沒有功能相依於任何 A 的子集。 - 部分相依:A(複合屬性,例如 a1+a2)和 B 是關聯表的屬性,如果 B 是功能相依於 A(A→B),而且 B 又功能相依於 A 的部分屬性(a1→B 或 a2→B) - 遷移相依:A、B 和 C 是關聯表的屬性,如果 A→B 且 B→C,則稱 C 是遞移相依於 A,A→C ### Second Normal Form(2NF) 1. 完成 1NF 2. 沒有部分相依的欄位 |toyId(PK)|toy| |:---:|:---:| |5|whiffleball| |6|frisbee| |9|kite| |12|yoyo| |toyId(FK、PK)|storeId(PK)|color|inventory|storeAddress| |:---:|:---:|:---:|:---:|:---:| |5|1|white|34|23 Maple| |5|3|yellow|12|100 E.North St.| |5|1|blue|5|23 Maple| |6|2|green|10|1902 Amber Ln.| |6|4|yellow|24|17 Engliside| |9|1|red|50|23 Maple| |9|2|blue|2|1902 Amber Ln.| |9|2|green|18|1902 Amber Ln.| |12|4|white|28|17 Engliside| |12|4|yellow|11|17 Engliside| 完成 2NF 後會變成三張表,某個玩具(包含顏色等資訊)在某個倉庫有多少庫存、玩具、倉庫。 |toyId|storeId|inventory| |:---:|:---:|:---:| |5|1|34| |5|3|12| |5|1|5| |6|2|10| |6|4|24| |9|1|50| |9|2|2| |9|2|18| |12|4|28| |12|4|11| |storeId|address| |:---:|:---:| |1|23 Maple| |2|1902 Amber Ln.| |3|100 E.North St.| |4|17 Engliside| |toyId|toy|color| |:---:|:---:|:---:| |5|whiffleball|white| |5|whiffleball|yellow| |5|whiffleball|blue| |6|frisbee|green| |6|frisbee|yellow| |9|kite|red| |9|kite|blue| |9|kite|green| |12|yoyo|white| |12|yoyo|yellow| 補上一些新的欄位。 |toyId|storeId|inventory| |:---:|:---:|:---:| |5|1|34| |5|3|12| |5|1|5| |6|2|10| |6|4|24| |9|1|50| |9|2|2| |9|2|18| |12|4|28| |12|4|11| |storeId|address|phone|manager| |:---:|:---:|:---:|:---:| |1|23 Maple|555-6712|Joe| |2|1902 Amber Ln.|555-3478|Susan| |3|100 E.North St.|555-0987|Tara| |4|17 Engliside|555-6554|Gordon| |id|toyId|toy|color|cast|weight| |:---:|:---:|:---:|:---:|:---:|:---:| |1|5|whiffleball|white|1.95|0.3| |2|5|whiffleball|yellow|2.20|0.4| |3|5|whiffleball|blue|1.95|0.3| |4|6|frisbee|green|3.50|0.5| |5|6|frisbee|yellow|1.50|0.2| |6|9|kite|red|5.75|1.2| |7|9|kite|blue|5.75|1.2| |8|9|kite|green|3.15|0.8| |9|12|yoyo|white|4.25|0.4| |10|12|yoyo|yellow|1.50|0.2| 經過 2NF 後表中欄位間不再部分相依。 ### Third Normal Form(3NF) 1. 完成 2NF 2. 沒有遷移相依的欄位 遷移相依意味著有非鍵值屬性相依於其他非鍵值屬性,造成改動一筆資料的非鍵值屬性很可能需要同時去影響該資料的另一非鍵值屬性。 ## JOIN 在完成了這麼多動作後,我們會發現原本一張完整的資料表,被我們逐漸切割成了許多更單純的表,這幫助了我們避免出現太多重複資料也順利的節省空間。 然而現在搜尋時需要取得完整資料,我們卻得同時搜尋好幾張表格,變得十分麻煩。 ### Cartesian Join(Cross Join) 也叫做交叉連線。 | LastName | DepartmentID | | --- | --- | | Rafferty | 31 | | Jones | 33 | | Steinberg | 33 | | Robinson | 34 | | Smith | 34 | | Jasper | _NULL_ | | DepartmentID | DepartmentName | | --- | --- | | 31 | 銷售部 | | 33 | 工程部 | | 34 | 秘書 | | 35 | 市場部 | ```sql SELECT * FROM employee CROSS JOIN department; SELECT * FROM employee, department; ``` | Employee.LastName | Employee.DepartmentID | Department.DepartmentName | Department.DepartmentID | | --- | --- | --- | --- | | Rafferty | 31 | 銷售部 | 31 | | Jones | 33 | 銷售部 | 31 | | Steinberg | 33 | 銷售部 | 31 | | Smith | 34 | 銷售部 | 31 | | Robinson | 34 | 銷售部 | 31 | | Jasper | _NULL_ | 銷售部 | 31 | | Rafferty | 31 | 工程部 | 33 | | Jones | 33 | 工程部 | 33 | | Steinberg | 33 | 工程部 | 33 | | Smith | 34 | 工程部 | 33 | | Robinson | 34 | 工程部 | 33 | | Jasper | _NULL_ | 工程部 | 33 | | Rafferty | 31 | 秘書 | 34 | | Jones | 33 | 秘書 | 34 | | Steinberg | 33 | 秘書 | 34 | | Smith | 34 | 秘書 | 34 | | Robinson | 34 | 秘書 | 34 | | Jasper | _NULL_ | 秘書 | 34 | | Rafferty | 31 | 市場部 | 35 | | Jones | 33 | 市場部 | 35 | | Steinberg | 33 | 市場部 | 35 | | Smith | 34 | 市場部 | 35 | | Robinson | 34 | 市場部 | 35 | | Jasper | _NULL_ | 市場部 | 35 | 將兩張表所有資料交叉連結後產生,如 A 表有五筆資料 B 表有六筆資料,那麼 AB 交叉連線後會產生 30 筆資料。 ### Inner Join 很多時候我們可以透過 Inner Join 來幫助我們把關聯的資料表聯結在一起,只要 Cross Join 後,利用 ON 將 foreign key 相等的對應資料取出即可。 ```sql SELECT * FROM employee INNER JOIN department ON employee.DepartmentID = department.DepartmentID; ``` | Employee.LastName | Employee.DepartmentID | Department.DepartmentName | Department.DepartmentID | | --- | --- | --- | --- | | Robinson | 34 | 秘書 | 34 | | Jones | 33 | 工程部 | 33 | | Smith | 34 | 秘書 | 34 | | Steinberg | 33 | 工程部 | 33 | | Rafferty | 31 | 銷售部 | 31 | 某些情況我們也可以做些使用上的轉換,想像有兩張表一張紀錄顧客購買紀錄,另一張紀錄商品。 透過 Inner Join ON 的條件設為不相等,我們就可以找出某位顧客仍未購買過什麼產品。 ```sql SELECT c.name, p.name FROM custumer AS c INNER JOIN product AS p ON c.productName <> p.name; ``` ### Natural Join 其實就是 Inner Join 自動比對相同名稱欄位 ```sql SELECT * FROM employee NATURAL JOIN department; ``` | DepartmentID | Employee.LastName | Department.DepartmentName | | --- | --- | --- | | 34 | Smith | 秘書 | | 33 | Jones | 工程部 | | 34 | Robinson | 秘書 | | 33 | Steinberg | 工程部 | | 31 | Rafferty | 銷售部 | ### Outer Join Inner Join 要求在兩張表中都必須有值,Outer Join 則不受此限,無值的部分會以 NULL 取代顯示。 外連線又可分為: 1. 左外連線:以左側資料為主顯示,右表無資料則以 NULL 呈現。 ```sql SELECT * FROM employee LEFT OUTER JOIN department ON employee.DepartmentID = department.DepartmentID; ``` | Employee.LastName | Employee.DepartmentID | Department.DepartmentName | Department.DepartmentID | | --- | --- | --- | --- | | Jones | 33 | Engineering | 33 | | Rafferty | 31 | Sales | 31 | | Robinson | 34 | Clerical | 34 | | Smith | 34 | Clerical | 34 | | _Jasper_ | _NULL_ | _NULL_ | _NULL_ | | Steinberg | 33 | Engineering | 33 | 2. 右外連線:與左外連線一樣概念,只是順序相反,保留右側的。 3. 全連線:一些資料庫系統(如 MySQL)並不直接支援全連線, 但它們可以通過左右外連線的並集(參考:[union](https://zh.wikipedia.org/w/index.php?title=Union_(SQL)&action=edit&redlink=1 "Union (SQL)(頁面不存在)"))來模擬實現。 ```sql SELECT * FROM employee FULL OUTER JOIN department ON employee.DepartmentID = department.DepartmentID; ``` | Employee.LastName | Employee.DepartmentID | Department.DepartmentName | Department.DepartmentID | | --- | --- | --- | --- | | Smith | 34 | Clerical | 34 | | Jones | 33 | Engineering | 33 | | Robinson | 34 | Clerical | 34 | | _Jasper_ | _NULL_ | _NULL_ | _NULL_ | | Steinberg | 33 | Engineering | 33 | | Rafferty | 31 | Sales | 31 | | _NULL_ | _NULL_ | _Marketing_ | _35_ | ## Subqueries 解決了關聯資料表的問題後,有些時候為了取得更精確的資料,我們不得不依序輸入幾條不同的 Query 去逐步縮減結果。 ```sql SELECT title FROM job_listings GROUP BY title ORDER BY title; SELECT mc.first_name, mc.last_name, mc.phone, jc.title FROM job_current AS jc INNER JOIN my_contacts AS mc ON jc.current_id = mc.current_id WHERE jc.title IN ('Cook', 'Hairdresser', 'Waiter', 'Web Designer', 'Web Developer'); ``` 第一條 Query 取得 Cook, Hairdresser, Waiter, Web Designer, Web Developer 後將這些做為條件輸入第二條 Query。 這樣就顯得十分不方便,因此 SQL 提供了 Subqueries 幫助我們整合 Query。 以這個例子:第一條 Query 被稱為 Inner Query 他會先取得符合的資料並套入 Outer Query 也就是第二條 Query。 而 Inner Query + Outer Query = Subqueries。 ```sql SELECT mc.first_name, mc.last_name, mc.phone, jc.title FROM job_current AS jc INNER JOIN my_contacts AS mc ON jc.current_id = mc.current_id WHERE jc.title IN (SELECT title FROM job_listings GROUP BY title ORDER BY title ); ``` 透過 Subqueries 我們也能做到與 join 相同的效果。 ### 子查詢的使用範圍 - SELECT: ```sql SELECT * FROM CUSTOMERS WHERE ID IN (SELECT ID FROM CUSTOMERS WHERE SALARY > 4500 ); ``` - INSERT:可以使用從子查詢收到的資料插入到表中。 ```sql INSERT INTO CUSTOMERS_BKP SELECT * FROM CUSTOMERS WHERE ID IN (SELECT ID FROM CUSTOMERS); ``` - UPDATE:可以使用從子查詢收到的資料插入到表中 ```sql UPDATE CUSTOMERS SET SALARY = SALARY * 0.25 WHERE AGE IN (SELECT AGE FROM CUSTOMERS_BKP WHERE AGE >= 27 ); ``` - DELETE:可以使用從子查詢收到的資料做為刪除標準。 ```sql DELETE FROM CUSTOMERS WHERE AGE IN (SELECT AGE FROM CUSTOMERS_BKP WHERE AGE > 27 ); ``` ### 關聯子查詢 與 非關聯子查詢 - 非關聯子查詢:子查詢與主查詢無關。 ```sql SELECT p.ProductID, p.ProductName, p.UnitPrice FROM Products AS p WHERE p.UnitPrice > (SELECT AVG(P2.UnitPrice) FROM Products P2); ``` - 關聯子查詢:子查詢會用到主查詢的結果。 ```sql SELECT p.ProductID, p.ProductName, p.UnitPrice FROM Products AS p WHERE Exists (SELECT ProductID FROM OrderDetails OD WHERE OD.ProductID = p.ProductID ); ``` 在子查詢中用到了主查詢的 p.ProductID,這種情況下因為會需要不斷重複比對主查詢子查詢,當資料龐大時效能影響會十分嚴重。 ## 資料集的操作 必須是同樣類型的資料集。 ### UNION、UNION ALL UNION:聯集,可以將結果合併起來。 ```sql SELECT * FROM A UNION SELECT * FROM B; ``` A 與 B 的結果會合併在一起出來,重複的行只會出現一次。 UNION ALL:可以將結果合併起來。 ```sql SELECT * FROM A UNION ALL SELECT * FROM B; ``` A 與 B 的結果會合併在一起出來,重複的行會被保留。 ### EXCEPT 差集:去除掉一部分結果。 ```sql SELECT * FROM A EXCEPT SELECT * FROM B; ``` 顯示只出現在 A 中沒出現在 B 的結果。 ### INTERSECT 交集:共同出現的結果。 ```sql SELECT * FROM A INTERSECT SELECT * FROM B; ``` A 與 B 共有的結果。 ### JOIN & SUBQUERIES 一個 SUBQUERIES 與 JOIN 之間通常可以互相轉換。 ```sql SELECT c1.name, c2.name AS boss FROM clown_info AS c1 INNER JOIN clown_info AS c2 ON c1.boss_id = c2.id; ``` ```sql SELECT c1.name , (SELECT name FROM clown_info WHERE c1.boss_id = id) AS BOSS FROM clown_info AS c1; ``` ## 維護資料庫 當資料庫越來越龐大、使用者與有能力存取資料庫的人越來越多,漸漸的就會有些問題發生。 ### 欄位資料規則 也許某些欄位你已經假定只會出現某幾種值,例如:性別:F、M 或是年齡:0~120。 我們最好要透過一些方式去明確限制這點,使人在輸入不正確資料時會被資料庫拒絕。 ```sql CREATE TABLE mystery_table ( column1 INT CHECK(column1 >200), column2 CHAR(1) CHECK(column2 NOT IN('x', 'y', 'z')), column3 VARCHAR(3) CHECK('A' = SUBSTRING(column3, 1, 1)) ) ``` 例如我們可以透過 CHECK,要求該欄位在被 INSERT 或 UPDATE 時檢查是否符合狀況。 ### VIEW ```sql CREATE VIEW web_design AS SELECT mc.first_name, mc.last_name, mc.phone, mc.email FROM my_contacts AS mc NATURAL JOIN job_desired AS jd WHERE jd.title = 'Web Design'; ``` 透過 CREATE VIEW 我們建立一個常用的 SELECT,日後可以直接去查看這個 VIEW 以取得資料。 ```sql SELECT * FROM web_design; ``` VIEW 就像是一個虛擬的 TABLE。 使用 VIEW 可以: 1. 讓搜尋變得簡單,一些複雜的 SELECT 指令,我們可以事先儲存好。 2. 使用者透過 VIEW 來作搜尋,而不是直接查表,可以讓資料庫多一層保護,也更方便日後 TABLE 的異動。 除此之外,如果 VIEW 所選取的欄位包含該表所有非 NULL 欄位的話,我們也可以對其作 INSERT、UPDATE、DELETE(不會影響超過一個基底資料表、且通過 CHECK OPTION) ### TRANSACTION - Atomicity:單獨性.比如,一個 Transaction 裡有一個 Update command,一個 Delete command.如果 Update command 成功了而 Delete command 失敗了,則這個 Transaction 便是失敗的,所以 Update command 必須回復修改過的資料。 - Consistency:一致性在 Transaction 執行前後,資料皆處於合乎所有規則的狀態.例如,某個欄位具有 foreign key 的關係在執行後,這樣的關係必需持續下去。 - Isolation:不同的 Transaction 之間執行時不能被彼此干擾.假設有兩個 Transaction 在同一時間對相同的一百筆資料進行更新動作,而其中一個 Transaction 在更新到一半時產生錯誤,於是進行資料回復.然而,另一個 Transaction 是完成的.可想而知最後的資料狀態一定是無法預測,因為不清楚那些資料是失敗的 Transaction 做資料回復的,還是成功的 Transaction 所更新完成的。 - Durability:資料庫引擎必須要保證一旦 Transaction 完成後,不論發生任何事情,如系統當機或電力中斷等,運作後的資料都必須存在於儲存媒介中,也就是在硬碟裡。 有時候我們會對資料庫進行一系列操作(邏輯上是同一件事情的各項小操作),例如:銀行轉帳,必須先確認 1. 帳號餘額是否足夠(SELECT) 2. 對帳號進行扣款(UPDATE) 3. 對轉帳帳號進行入帳(UPDATE) 這三件事情必須一起完成才是一個合理的轉帳操作,如果扣款成功了卻沒對轉帳對象入帳,就會出問題。 TRANSACTION 幫助我們解決了這個問題,他可以將一連串的操作包裹起來,若有任一動作執行未成功可以 rollback 回正常狀態;所有動作都執行成功時才正確將操作改變寫入資料。 有些 TABLE TYPE 不支援 TRANSACTION,因此預計需要保證安全性的資料表,會使用 TRANSACTION 的資料表應進行確認。(MySQL 中:BDB、InnoDB) ```sql START TRANSACTION; SELECT * FROM piggy_bank; UPDATE piggy_bank SET coin = 'Q' WHERE coin = 'P'; SELECT * FROM piggy_bank; COMMIT(ROLLBACK); ``` COMMIT 代表確認這些操作執行;ROLLBACK 則代表還原這些操作。 Microsoft SQL Server 使用 BEGIN TRANSACTION;來開始。 ### 權限設定 在一開始,我們的資料庫只有一位 root 使用者且沒有密碼,為了保證安全我們需要對這樣的狀況做些處理。 設定密碼(MySQL) ```sql SET PASSWORD FOR 'root'@'localhost' = PASSWORD('aB2Psad78'); ``` 新增其他使用者(MySQL) ```sql CREATE USER elsie IDENTIFIED BY 'elsiePassword'; ``` 發放權限 (給予 elsie 在 clown_info 使用 SELECT 的權限) (給予 elsie 在 clown_info 使用 ALL(SELECT、DELETE、UPDATE、INSERT)的權限) (給予 elsie 在 TODO 資料庫中所有表使用 SELECT 的權限) (給予 elsie 在 clown_info 使用 ALL(SELECT、DELETE、UPDATE、INSERT)的權限,並允許他給予其他人同樣權限) ```sql GRANT SELECT ON clown_info TO elsie; GRANT ALL ON clown_info TO elsie; GRANT SELECT ON TODO.* TO elsie; GRANT ALL ON clown_info TO elsie WITH GRANT OPTION; ``` 撤銷權限 (CASCADE:撤銷被 elsie 所給予的人權限 RESTRICT:若撤銷 elsie 權限會影響到其他人則回傳 ERROR) ```sql REVOKE SELECT ON clown_info FROM elsie; REVOKE SELECT ON clown_info FROM elsie CASCADE; REVOKE SELECT ON clown_info FROM elsie RESTRICT; ``` ### ROLE 除了針對各個使用者設定權限,也可以設置一些群組,透過給予群組權限再將各個使用者分發到群組中,以便管理。 ```sql CREATE ROLE data_entry; GRANT SELECT ON clown_info TO data_entry; GRANT data_entry TO elsie; ``` 與 GRANT OPTION 類似,ROLE 也有 ADMIN OPTION。 擁有 ROLE ADMIN OPTION 的人,可以將其他人編入此 ROLE。 ### 合併 CREATE USER 以及 GRANT ```sql GRANT SELECT ON clown_info TO elsie IDENTIFIED BY 'elsiePassword'; ``` ## Stored Procedure、Function - 更彈性的 SQL 語法 - 錯誤處理能力 - 合於 SQL 標準 - 程式碼封裝 - 可重覆利用 - 分離程式邏輯 - 易於維護 - 減少所需的網路頻寬 - Server 端的 upgrade 也可以嘉惠 Client 端 - 更好的安全性 ### 差異 - Procedure 的 Parameter 可以定義為 IN, OUT, INOUT,而 Function 的 Parameter 則必定是 IN(系統預設,不能自行指定)。 - Function 必定有回傳值,因此必須包含一個 RETURNS 子句來定義傳回值的資料型態。 - Procedure 可以直接產生單一或多個 Result Set,但 Function 不行。 ```sql CREATE FUNCTION teamData( @teamNumber CHAR(8) ) RETURNS @TABLE TABLE ( teamName VARCHAR(20), coachName VARCHAR(20), memberName VARCHAR(20), memberBackNumber INT ) AS BEGIN INSERT INTO @TABLE (teamName, coachName, memberName, memberBackNumber) SELECT team.teamName, coach.coachName, member.memberName, member.memberBackNumber FROM team INNER JOIN coach ON team.teamNumber = coach.teamNumber INNER JOIN member ON team.teamNumber = member.teamNumber WHERE team.teamNumber = @teamNumber; RETURN (@TABLE); END ``` ### stored procedure ```sql CREATE PROCEDURE insertMember ( @number CHAR(8), @memberNumber CHAR(6), @memberName VARCHAR(20), @memberBackNumber INT ) AS BEGIN INSERT INTO member VALUES(@number, @memberNumber, @memberName, @memberBackNumber); END ``` ### Trigger ```sql CREATE TRIGGER trg_tblGrade ON tblGrade INSTEAD OF INSERT AS INSERT INTO tblGrade2( StudentNo, ClassID, Grade ) SELECT StudentNo, ClassID, Grade FROM inserted ``` ```sql CREATE TRIGGER trg_tblGrade_after ON tblGrade AFTER INSERT, UPDATE, DELETE AS DECLARE @OriginalData XML; DECLARE @NewData XML; DECLARE @Operation char(1); SET @OriginalData=''; SET @NewData=''; --Update ( deleted + inserted ) IF EXISTS(SELECT 1 FROM deleted) AND EXISTS(SELECT 1 FROM inserted) BEGIN SET @OriginalData = ( SELECT * FROM deleted FOR XML RAW('GradeTable') ) SET @NewData = ( SELECT * FROM inserted FOR XML RAW('GradeTable') ) END --Insert ( inserted ) IF EXISTS(SELECT 1 FROM inserted) AND NOT EXISTS(SELECT 1 FROM deleted) BEGIN SET @NewData = ( SELECT * FROM inserted FOR XML RAW('GradeTable') ) END --Delete ( deleted ) IF EXISTS(SELECT 1 FROM deleted) AND NOT EXISTS(SELECT 1 FROM inserted) BEGIN SET @OriginalData = ( SELECT * FROM deleted FOR XML RAW('GradeTable') ) END INSERT INTO Log(TableName, OriginalData, NewData) VALUES ('GradeTable', @OriginalData, @NewData) ```