diff --git a/docs/notes/SQL 练习.md b/docs/notes/SQL 练习.md
new file mode 100644
index 00000000..92f90ae6
--- /dev/null
+++ b/docs/notes/SQL 练习.md
@@ -0,0 +1,1143 @@
+
+* [595. Big Countries](#595-big-countries)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [627. Swap Salary](#627-swap-salary)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [620. Not Boring Movies](#620-not-boring-movies)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [596. Classes More Than 5 Students](#596-classes-more-than-5-students)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [182. Duplicate Emails](#182-duplicate-emails)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [196. Delete Duplicate Emails](#196-delete-duplicate-emails)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [175. Combine Two Tables](#175-combine-two-tables)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [181. Employees Earning More Than Their Managers](#181-employees-earning-more-than-their-managers)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [183. Customers Who Never Order](#183-customers-who-never-order)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [184. Department Highest Salary](#184-department-highest-salary)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [176. Second Highest Salary](#176-second-highest-salary)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [177. Nth Highest Salary](#177-nth-highest-salary)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [178. Rank Scores](#178-rank-scores)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [180. Consecutive Numbers](#180-consecutive-numbers)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [626. Exchange Seats](#626-exchange-seats)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+
+
+
+# 595. Big Countries
+
+https://leetcode.com/problems/big-countries/description/
+
+## Description
+
+```html
++-----------------+------------+------------+--------------+---------------+
+| name | continent | area | population | gdp |
++-----------------+------------+------------+--------------+---------------+
+| Afghanistan | Asia | 652230 | 25500100 | 20343000 |
+| Albania | Europe | 28748 | 2831741 | 12960000 |
+| Algeria | Africa | 2381741 | 37100000 | 188681000 |
+| Andorra | Europe | 468 | 78115 | 3712000 |
+| Angola | Africa | 1246700 | 20609294 | 100990000 |
++-----------------+------------+------------+--------------+---------------+
+```
+
+查找面积超过 3,000,000 或者人口数超过 25,000,000 的国家。
+
+```html
++--------------+-------------+--------------+
+| name | population | area |
++--------------+-------------+--------------+
+| Afghanistan | 25500100 | 652230 |
+| Algeria | 37100000 | 2381741 |
++--------------+-------------+--------------+
+```
+
+## Solution
+
+```sql
+SELECT name,
+ population,
+ area
+FROM
+ World
+WHERE
+ area > 3000000
+ OR population > 25000000;
+```
+
+## SQL Schema
+
+SQL Schema 用于在本地环境下创建表结构并导入数据,从而方便在本地环境调试。
+
+```sql
+DROP TABLE
+IF
+ EXISTS World;
+CREATE TABLE World ( NAME VARCHAR ( 255 ), continent VARCHAR ( 255 ), area INT, population INT, gdp INT );
+INSERT INTO World ( NAME, continent, area, population, gdp )
+VALUES
+ ( 'Afghanistan', 'Asia', '652230', '25500100', '203430000' ),
+ ( 'Albania', 'Europe', '28748', '2831741', '129600000' ),
+ ( 'Algeria', 'Africa', '2381741', '37100000', '1886810000' ),
+ ( 'Andorra', 'Europe', '468', '78115', '37120000' ),
+ ( 'Angola', 'Africa', '1246700', '20609294', '1009900000' );
+```
+
+# 627. Swap Salary
+
+https://leetcode.com/problems/swap-salary/description/
+
+## Description
+
+```html
+| id | name | sex | salary |
+|----|------|-----|--------|
+| 1 | A | m | 2500 |
+| 2 | B | f | 1500 |
+| 3 | C | m | 5500 |
+| 4 | D | f | 500 |
+```
+
+只用一个 SQL 查询,将 sex 字段反转。
+
+```html
+| id | name | sex | salary |
+|----|------|-----|--------|
+| 1 | A | f | 2500 |
+| 2 | B | m | 1500 |
+| 3 | C | f | 5500 |
+| 4 | D | m | 500 |
+```
+
+## Solution
+
+两个相等的数异或的结果为 0,而 0 与任何一个数异或的结果为这个数。
+
+sex 字段只有两个取值:'f' 和 'm',并且有以下规律:
+
+```
+'f' ^ ('m' ^ 'f') = 'm' ^ ('f' ^ 'f') = 'm'
+'m' ^ ('m' ^ 'f') = 'f' ^ ('m' ^ 'm') = 'f'
+```
+
+因此将 sex 字段和 'm' ^ 'f' 进行异或操作,最后就能反转 sex 字段。
+
+```sql
+UPDATE salary
+SET sex = CHAR ( ASCII(sex) ^ ASCII( 'm' ) ^ ASCII( 'f' ) );
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS salary;
+CREATE TABLE salary ( id INT, NAME VARCHAR ( 100 ), sex CHAR ( 1 ), salary INT );
+INSERT INTO salary ( id, NAME, sex, salary )
+VALUES
+ ( '1', 'A', 'm', '2500' ),
+ ( '2', 'B', 'f', '1500' ),
+ ( '3', 'C', 'm', '5500' ),
+ ( '4', 'D', 'f', '500' );
+```
+
+# 620. Not Boring Movies
+
+https://leetcode.com/problems/not-boring-movies/description/
+
+## Description
+
+
+```html
++---------+-----------+--------------+-----------+
+| id | movie | description | rating |
++---------+-----------+--------------+-----------+
+| 1 | War | great 3D | 8.9 |
+| 2 | Science | fiction | 8.5 |
+| 3 | irish | boring | 6.2 |
+| 4 | Ice song | Fantacy | 8.6 |
+| 5 | House card| Interesting| 9.1 |
++---------+-----------+--------------+-----------+
+```
+
+查找 id 为奇数,并且 description 不是 boring 的电影,按 rating 降序。
+
+```html
++---------+-----------+--------------+-----------+
+| id | movie | description | rating |
++---------+-----------+--------------+-----------+
+| 5 | House card| Interesting| 9.1 |
+| 1 | War | great 3D | 8.9 |
++---------+-----------+--------------+-----------+
+```
+
+## Solution
+
+```sql
+SELECT
+ *
+FROM
+ cinema
+WHERE
+ id % 2 = 1
+ AND description != 'boring'
+ORDER BY
+ rating DESC;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS cinema;
+CREATE TABLE cinema ( id INT, movie VARCHAR ( 255 ), description VARCHAR ( 255 ), rating FLOAT ( 2, 1 ) );
+INSERT INTO cinema ( id, movie, description, rating )
+VALUES
+ ( 1, 'War', 'great 3D', 8.9 ),
+ ( 2, 'Science', 'fiction', 8.5 ),
+ ( 3, 'irish', 'boring', 6.2 ),
+ ( 4, 'Ice song', 'Fantacy', 8.6 ),
+ ( 5, 'House card', 'Interesting', 9.1 );
+```
+
+# 596. Classes More Than 5 Students
+
+https://leetcode.com/problems/classes-more-than-5-students/description/
+
+## Description
+
+```html
++---------+------------+
+| student | class |
++---------+------------+
+| A | Math |
+| B | English |
+| C | Math |
+| D | Biology |
+| E | Math |
+| F | Computer |
+| G | Math |
+| H | Math |
+| I | Math |
++---------+------------+
+```
+
+查找有五名及以上 student 的 class。
+
+```html
++---------+
+| class |
++---------+
+| Math |
++---------+
+```
+
+## Solution
+
+对 class 列进行分组之后,再使用 count 汇总函数统计每个分组的记录个数,之后使用 HAVING 进行筛选。HAVING 针对分组进行筛选,而 WHERE 针对每个记录(行)进行筛选。
+
+```sql
+SELECT
+ class
+FROM
+ courses
+GROUP BY
+ class
+HAVING
+ count( DISTINCT student ) >= 5;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS courses;
+CREATE TABLE courses ( student VARCHAR ( 255 ), class VARCHAR ( 255 ) );
+INSERT INTO courses ( student, class )
+VALUES
+ ( 'A', 'Math' ),
+ ( 'B', 'English' ),
+ ( 'C', 'Math' ),
+ ( 'D', 'Biology' ),
+ ( 'E', 'Math' ),
+ ( 'F', 'Computer' ),
+ ( 'G', 'Math' ),
+ ( 'H', 'Math' ),
+ ( 'I', 'Math' );
+```
+
+# 182. Duplicate Emails
+
+https://leetcode.com/problems/duplicate-emails/description/
+
+## Description
+
+邮件地址表:
+
+```html
++----+---------+
+| Id | Email |
++----+---------+
+| 1 | a@b.com |
+| 2 | c@d.com |
+| 3 | a@b.com |
++----+---------+
+```
+
+查找重复的邮件地址:
+
+```html
++---------+
+| Email |
++---------+
+| a@b.com |
++---------+
+```
+
+## Solution
+
+对 Email 进行分组,如果并使用 COUNT 进行计数统计,结果大于等于 2 的表示 Email 重复。
+
+```sql
+SELECT
+ Email
+FROM
+ Person
+GROUP BY
+ Email
+HAVING
+ COUNT( * ) >= 2;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Person;
+CREATE TABLE Person ( Id INT, Email VARCHAR ( 255 ) );
+INSERT INTO Person ( Id, Email )
+VALUES
+ ( 1, 'a@b.com' ),
+ ( 2, 'c@d.com' ),
+ ( 3, 'a@b.com' );
+```
+
+
+# 196. Delete Duplicate Emails
+
+https://leetcode.com/problems/delete-duplicate-emails/description/
+
+## Description
+
+邮件地址表:
+
+```html
++----+---------+
+| Id | Email |
++----+---------+
+| 1 | john@example.com |
+| 2 | bob@example.com |
+| 3 | john@example.com |
++----+---------+
+```
+
+删除重复的邮件地址:
+
+```html
++----+------------------+
+| Id | Email |
++----+------------------+
+| 1 | john@example.com |
+| 2 | bob@example.com |
++----+------------------+
+```
+
+## Solution
+
+只保留相同 Email 中 Id 最小的那一个,然后删除其它的。
+
+连接查询:
+
+```sql
+DELETE p1
+FROM
+ Person p1,
+ Person p2
+WHERE
+ p1.Email = p2.Email
+ AND p1.Id > p2.Id
+```
+
+子查询:
+
+```sql
+DELETE
+FROM
+ Person
+WHERE
+ id NOT IN (
+ SELECT id
+ FROM (
+ SELECT min( id ) AS id
+ FROM Person
+ GROUP BY email
+ ) AS m
+ );
+```
+
+应该注意的是上述解法额外嵌套了一个 SELECT 语句,如果不这么做,会出现错误:You can't specify target table 'Person' for update in FROM clause。以下演示了这种错误解法。
+
+```sql
+DELETE
+FROM
+ Person
+WHERE
+ id NOT IN (
+ SELECT min( id ) AS id
+ FROM Person
+ GROUP BY email
+ );
+```
+
+参考:[pMySQL Error 1093 - Can't specify target table for update in FROM clause](https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause)
+
+## SQL Schema
+
+与 182 相同。
+
+# 175. Combine Two Tables
+
+https://leetcode.com/problems/combine-two-tables/description/
+
+## Description
+
+Person 表:
+
+```html
++-------------+---------+
+| Column Name | Type |
++-------------+---------+
+| PersonId | int |
+| FirstName | varchar |
+| LastName | varchar |
++-------------+---------+
+PersonId is the primary key column for this table.
+```
+
+Address 表:
+
+```html
++-------------+---------+
+| Column Name | Type |
++-------------+---------+
+| AddressId | int |
+| PersonId | int |
+| City | varchar |
+| State | varchar |
++-------------+---------+
+AddressId is the primary key column for this table.
+```
+
+查找 FirstName, LastName, City, State 数据,而不管一个用户有没有填地址信息。
+
+## Solution
+
+涉及到 Person 和 Address 两个表,在对这两个表执行连接操作时,因为要保留 Person 表中的信息,即使在 Address 表中没有关联的信息也要保留。此时可以用左外连接,将 Person 表放在 LEFT JOIN 的左边。
+
+```sql
+SELECT
+ FirstName,
+ LastName,
+ City,
+ State
+FROM
+ Person P
+ LEFT JOIN Address A
+ ON P.PersonId = A.PersonId;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Person;
+CREATE TABLE Person ( PersonId INT, FirstName VARCHAR ( 255 ), LastName VARCHAR ( 255 ) );
+DROP TABLE
+IF
+ EXISTS Address;
+CREATE TABLE Address ( AddressId INT, PersonId INT, City VARCHAR ( 255 ), State VARCHAR ( 255 ) );
+INSERT INTO Person ( PersonId, LastName, FirstName )
+VALUES
+ ( 1, 'Wang', 'Allen' );
+INSERT INTO Address ( AddressId, PersonId, City, State )
+VALUES
+ ( 1, 2, 'New York City', 'New York' );
+```
+
+# 181. Employees Earning More Than Their Managers
+
+https://leetcode.com/problems/employees-earning-more-than-their-managers/description/
+
+## Description
+
+Employee 表:
+
+```html
++----+-------+--------+-----------+
+| Id | Name | Salary | ManagerId |
++----+-------+--------+-----------+
+| 1 | Joe | 70000 | 3 |
+| 2 | Henry | 80000 | 4 |
+| 3 | Sam | 60000 | NULL |
+| 4 | Max | 90000 | NULL |
++----+-------+--------+-----------+
+```
+
+查找薪资大于其经理薪资的员工信息。
+
+## Solution
+
+```sql
+SELECT
+ E1.NAME AS Employee
+FROM
+ Employee E1
+ INNER JOIN Employee E2
+ ON E1.ManagerId = E2.Id
+ AND E1.Salary > E2.Salary;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Employee;
+CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, ManagerId INT );
+INSERT INTO Employee ( Id, NAME, Salary, ManagerId )
+VALUES
+ ( 1, 'Joe', 70000, 3 ),
+ ( 2, 'Henry', 80000, 4 ),
+ ( 3, 'Sam', 60000, NULL ),
+ ( 4, 'Max', 90000, NULL );
+```
+
+# 183. Customers Who Never Order
+
+https://leetcode.com/problems/customers-who-never-order/description/
+
+## Description
+
+Customers 表:
+
+```html
++----+-------+
+| Id | Name |
++----+-------+
+| 1 | Joe |
+| 2 | Henry |
+| 3 | Sam |
+| 4 | Max |
++----+-------+
+```
+
+Orders 表:
+
+```html
++----+------------+
+| Id | CustomerId |
++----+------------+
+| 1 | 3 |
+| 2 | 1 |
++----+------------+
+```
+
+查找没有订单的顾客信息:
+
+```html
++-----------+
+| Customers |
++-----------+
+| Henry |
+| Max |
++-----------+
+```
+
+## Solution
+
+左外链接
+
+```sql
+SELECT
+ C.Name AS Customers
+FROM
+ Customers C
+ LEFT JOIN Orders O
+ ON C.Id = O.CustomerId
+WHERE
+ O.CustomerId IS NULL;
+```
+
+子查询
+
+```sql
+SELECT
+ Name AS Customers
+FROM
+ Customers
+WHERE
+ Id NOT IN (
+ SELECT CustomerId
+ FROM Orders
+ );
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Customers;
+CREATE TABLE Customers ( Id INT, NAME VARCHAR ( 255 ) );
+DROP TABLE
+IF
+ EXISTS Orders;
+CREATE TABLE Orders ( Id INT, CustomerId INT );
+INSERT INTO Customers ( Id, NAME )
+VALUES
+ ( 1, 'Joe' ),
+ ( 2, 'Henry' ),
+ ( 3, 'Sam' ),
+ ( 4, 'Max' );
+INSERT INTO Orders ( Id, CustomerId )
+VALUES
+ ( 1, 3 ),
+ ( 2, 1 );
+```
+
+# 184. Department Highest Salary
+
+https://leetcode.com/problems/department-highest-salary/description/
+
+## Description
+
+Employee 表:
+
+```html
++----+-------+--------+--------------+
+| Id | Name | Salary | DepartmentId |
++----+-------+--------+--------------+
+| 1 | Joe | 70000 | 1 |
+| 2 | Henry | 80000 | 2 |
+| 3 | Sam | 60000 | 2 |
+| 4 | Max | 90000 | 1 |
++----+-------+--------+--------------+
+```
+
+Department 表:
+
+```html
++----+----------+
+| Id | Name |
++----+----------+
+| 1 | IT |
+| 2 | Sales |
++----+----------+
+```
+
+查找一个 Department 中收入最高者的信息:
+
+```html
++------------+----------+--------+
+| Department | Employee | Salary |
++------------+----------+--------+
+| IT | Max | 90000 |
+| Sales | Henry | 80000 |
++------------+----------+--------+
+```
+
+## Solution
+
+创建一个临时表,包含了部门员工的最大薪资。可以对部门进行分组,然后使用 MAX() 汇总函数取得最大薪资。
+
+之后使用连接找到一个部门中薪资等于临时表中最大薪资的员工。
+
+```sql
+SELECT
+ D.NAME Department,
+ E.NAME Employee,
+ E.Salary
+FROM
+ Employee E,
+ Department D,
+ ( SELECT DepartmentId, MAX( Salary ) Salary
+ FROM Employee
+ GROUP BY DepartmentId ) M
+WHERE
+ E.DepartmentId = D.Id
+ AND E.DepartmentId = M.DepartmentId
+ AND E.Salary = M.Salary;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE IF EXISTS Employee;
+CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, DepartmentId INT );
+DROP TABLE IF EXISTS Department;
+CREATE TABLE Department ( Id INT, NAME VARCHAR ( 255 ) );
+INSERT INTO Employee ( Id, NAME, Salary, DepartmentId )
+VALUES
+ ( 1, 'Joe', 70000, 1 ),
+ ( 2, 'Henry', 80000, 2 ),
+ ( 3, 'Sam', 60000, 2 ),
+ ( 4, 'Max', 90000, 1 );
+INSERT INTO Department ( Id, NAME )
+VALUES
+ ( 1, 'IT' ),
+ ( 2, 'Sales' );
+```
+
+
+# 176. Second Highest Salary
+
+https://leetcode.com/problems/second-highest-salary/description/
+
+## Description
+
+```html
++----+--------+
+| Id | Salary |
++----+--------+
+| 1 | 100 |
+| 2 | 200 |
+| 3 | 300 |
++----+--------+
+```
+
+查找工资第二高的员工。
+
+```html
++---------------------+
+| SecondHighestSalary |
++---------------------+
+| 200 |
++---------------------+
+```
+
+没有找到返回 null 而不是不返回数据。
+
+## Solution
+
+为了在没有查找到数据时返回 null,需要在查询结果外面再套一层 SELECT。
+
+```sql
+SELECT
+ ( SELECT DISTINCT Salary
+ FROM Employee
+ ORDER BY Salary DESC
+ LIMIT 1, 1 ) SecondHighestSalary;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Employee;
+CREATE TABLE Employee ( Id INT, Salary INT );
+INSERT INTO Employee ( Id, Salary )
+VALUES
+ ( 1, 100 ),
+ ( 2, 200 ),
+ ( 3, 300 );
+```
+
+# 177. Nth Highest Salary
+
+## Description
+
+查找工资第 N 高的员工。
+
+## Solution
+
+```sql
+CREATE FUNCTION getNthHighestSalary ( N INT ) RETURNS INT BEGIN
+
+SET N = N - 1;
+RETURN (
+ SELECT (
+ SELECT DISTINCT Salary
+ FROM Employee
+ ORDER BY Salary DESC
+ LIMIT N, 1
+ )
+);
+
+END
+```
+
+## SQL Schema
+
+同 176。
+
+
+# 178. Rank Scores
+
+https://leetcode.com/problems/rank-scores/description/
+
+## Description
+
+得分表:
+
+```html
++----+-------+
+| Id | Score |
++----+-------+
+| 1 | 3.50 |
+| 2 | 3.65 |
+| 3 | 4.00 |
+| 4 | 3.85 |
+| 5 | 4.00 |
+| 6 | 3.65 |
++----+-------+
+```
+
+将得分排序,并统计排名。
+
+```html
++-------+------+
+| Score | Rank |
++-------+------+
+| 4.00 | 1 |
+| 4.00 | 1 |
+| 3.85 | 2 |
+| 3.65 | 3 |
+| 3.65 | 3 |
+| 3.50 | 4 |
++-------+------+
+```
+
+## Solution
+
+要统计某个 score 的排名,只要统计大于等于该 score 的 score 数量。
+
+| Id | score | 大于等于该 score 的 score 数量 | 排名 |
+| :---: | :---: | :---: | :---: |
+| 1 | 4.1 | 3 | 3 |
+| 2 | 4.2 | 2 | 2 |
+| 3 | 4.3 | 1 | 1 |
+
+使用连接操作找到某个 score 对应的大于等于其值的记录:
+
+```sql
+SELECT
+ *
+FROM
+ Scores S1
+ INNER JOIN Scores S2
+ ON S1.score <= S2.score
+ORDER BY
+ S1.score DESC, S1.Id;
+```
+
+| S1.Id | S1.score | S2.Id | S2.score |
+| :---: | :---: | :---: | :---: |
+|3| 4.3| 3 |4.3|
+|2| 4.2| 2| 4.2|
+|2| 4.2 |3 |4.3|
+|1| 4.1 |1| 4.1|
+|1| 4.1 |2| 4.2|
+|1| 4.1 |3| 4.3|
+
+可以看到每个 S1.score 都有对应好几条记录,我们再进行分组,并统计每个分组的数量作为 'Rank'
+
+```sql
+SELECT
+ S1.score 'Score',
+ COUNT(*) 'Rank'
+FROM
+ Scores S1
+ INNER JOIN Scores S2
+ ON S1.score <= S2.score
+GROUP BY
+ S1.id, S1.score
+ORDER BY
+ S1.score DESC, S1.Id;
+```
+
+| score | Rank |
+| :---: | :---: |
+| 4.3 | 1 |
+| 4.2 | 2 |
+| 4.1 | 3 |
+
+上面的解法看似没问题,但是对于以下数据,它却得到了错误的结果:
+
+| Id | score |
+| :---: | :---: |
+| 1 | 4.1 |
+| 2 | 4.2 |
+| 3 | 4.2 |
+
+| score | Rank |
+| :---: | :--: |
+| 4.2 | 2 |
+| 4.2 | 2 |
+| 4.1 | 3 |
+
+而我们希望的结果为:
+
+| score | Rank |
+| :---: | :--: |
+| 4.2 | 1 |
+| 4.2 | 1 |
+| 4.1 | 2 |
+
+连接情况如下:
+
+| S1.Id | S1.score | S2.Id | S2.score |
+| :---: | :------: | :---: | :------: |
+| 2 | 4.2 | 3 | 4.2 |
+| 2 | 4.2 | 2 | 4.2 |
+| 3 | 4.2 | 3 | 4.2 |
+| 3 | 4.2 | 2 | 4.1 |
+| 1 | 4.1 | 3 | 4.2 |
+| 1 | 4.1 | 2 | 4.2 |
+| 1 | 4.1 | 1 | 4.1 |
+
+我们想要的结果是,把分数相同的放在同一个排名,并且相同分数只占一个位置,例如上面的分数,Id=2 和 Id=3 的记录都有相同的分数,并且最高,他们并列第一。而 Id=1 的记录应该排第二名,而不是第三名。所以在进行 COUNT 计数统计时,我们需要使用 COUNT( DISTINCT S2.score ) 从而只统计一次相同的分数。
+
+```sql
+SELECT
+ S1.score 'Score',
+ COUNT( DISTINCT S2.score ) 'Rank'
+FROM
+ Scores S1
+ INNER JOIN Scores S2
+ ON S1.score <= S2.score
+GROUP BY
+ S1.id, S1.score
+ORDER BY
+ S1.score DESC;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Scores;
+CREATE TABLE Scores ( Id INT, Score DECIMAL ( 3, 2 ) );
+INSERT INTO Scores ( Id, Score )
+VALUES
+ ( 1, 4.1 ),
+ ( 2, 4.1 ),
+ ( 3, 4.2 ),
+ ( 4, 4.2 ),
+ ( 5, 4.3 ),
+ ( 6, 4.3 );
+```
+
+# 180. Consecutive Numbers
+
+https://leetcode.com/problems/consecutive-numbers/description/
+
+## Description
+
+数字表:
+
+```html
++----+-----+
+| Id | Num |
++----+-----+
+| 1 | 1 |
+| 2 | 1 |
+| 3 | 1 |
+| 4 | 2 |
+| 5 | 1 |
+| 6 | 2 |
+| 7 | 2 |
++----+-----+
+```
+
+查找连续出现三次的数字。
+
+```html
++-----------------+
+| ConsecutiveNums |
++-----------------+
+| 1 |
++-----------------+
+```
+
+## Solution
+
+```sql
+SELECT
+ DISTINCT L1.num ConsecutiveNums
+FROM
+ Logs L1,
+ Logs L2,
+ Logs L3
+WHERE L1.id = l2.id - 1
+ AND L2.id = L3.id - 1
+ AND L1.num = L2.num
+ AND l2.num = l3.num;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS LOGS;
+CREATE TABLE LOGS ( Id INT, Num INT );
+INSERT INTO LOGS ( Id, Num )
+VALUES
+ ( 1, 1 ),
+ ( 2, 1 ),
+ ( 3, 1 ),
+ ( 4, 2 ),
+ ( 5, 1 ),
+ ( 6, 2 ),
+ ( 7, 2 );
+```
+
+# 626. Exchange Seats
+
+https://leetcode.com/problems/exchange-seats/description/
+
+## Description
+
+seat 表存储着座位对应的学生。
+
+```html
++---------+---------+
+| id | student |
++---------+---------+
+| 1 | Abbot |
+| 2 | Doris |
+| 3 | Emerson |
+| 4 | Green |
+| 5 | Jeames |
++---------+---------+
+```
+
+要求交换相邻座位的两个学生,如果最后一个座位是奇数,那么不交换这个座位上的学生。
+
+```html
++---------+---------+
+| id | student |
++---------+---------+
+| 1 | Doris |
+| 2 | Abbot |
+| 3 | Green |
+| 4 | Emerson |
+| 5 | Jeames |
++---------+---------+
+```
+
+## Solution
+
+使用多个 union。
+
+```sql
+# 处理偶数 id,让 id 减 1
+# 例如 2,4,6,... 变成 1,3,5,...
+SELECT
+ s1.id - 1 AS id,
+ s1.student
+FROM
+ seat s1
+WHERE
+ s1.id MOD 2 = 0 UNION
+# 处理奇数 id,让 id 加 1。但是如果最大的 id 为奇数,则不做处理
+# 例如 1,3,5,... 变成 2,4,6,...
+SELECT
+ s2.id + 1 AS id,
+ s2.student
+FROM
+ seat s2
+WHERE
+ s2.id MOD 2 = 1
+ AND s2.id != ( SELECT max( s3.id ) FROM seat s3 ) UNION
+# 如果最大的 id 为奇数,单独取出这个数
+SELECT
+ s4.id AS id,
+ s4.student
+FROM
+ seat s4
+WHERE
+ s4.id MOD 2 = 1
+ AND s4.id = ( SELECT max( s5.id ) FROM seat s5 )
+ORDER BY
+ id;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS seat;
+CREATE TABLE seat ( id INT, student VARCHAR ( 255 ) );
+INSERT INTO seat ( id, student )
+VALUES
+ ( '1', 'Abbot' ),
+ ( '2', 'Doris' ),
+ ( '3', 'Emerson' ),
+ ( '4', 'Green' ),
+ ( '5', 'Jeames' );
+```
+
+
+
+
+
+
+
diff --git a/docs/notes/SQL 语法.md b/docs/notes/SQL 语法.md
new file mode 100644
index 00000000..9e7adc04
--- /dev/null
+++ b/docs/notes/SQL 语法.md
@@ -0,0 +1,788 @@
+
+* [一、基础](#一基础)
+* [二、创建表](#二创建表)
+* [三、修改表](#三修改表)
+* [四、插入](#四插入)
+* [五、更新](#五更新)
+* [六、删除](#六删除)
+* [七、查询](#七查询)
+ * [DISTINCT](#distinct)
+ * [LIMIT](#limit)
+* [八、排序](#八排序)
+* [九、过滤](#九过滤)
+* [十、通配符](#十通配符)
+* [十一、计算字段](#十一计算字段)
+* [十二、函数](#十二函数)
+ * [汇总](#汇总)
+ * [文本处理](#文本处理)
+ * [日期和时间处理](#日期和时间处理)
+ * [数值处理](#数值处理)
+* [十三、分组](#十三分组)
+* [十四、子查询](#十四子查询)
+* [十五、连接](#十五连接)
+ * [内连接](#内连接)
+ * [自连接](#自连接)
+ * [自然连接](#自然连接)
+ * [外连接](#外连接)
+* [十六、组合查询](#十六组合查询)
+* [十七、视图](#十七视图)
+* [十八、存储过程](#十八存储过程)
+* [十九、游标](#十九游标)
+* [二十、触发器](#二十触发器)
+* [二十一、事务管理](#二十一事务管理)
+* [二十二、字符集](#二十二字符集)
+* [二十三、权限管理](#二十三权限管理)
+* [参考资料](#参考资料)
+
+
+
+# 一、基础
+
+模式定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。
+
+主键的值不允许修改,也不允许复用(不能将已经删除的主键值赋给新数据行的主键)。
+
+SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。
+
+SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。
+
+SQL 支持以下三种注释:
+
+```sql
+# 注释
+SELECT *
+FROM mytable; -- 注释
+/* 注释1
+ 注释2 */
+```
+
+数据库创建与使用:
+
+```sql
+CREATE DATABASE test;
+USE test;
+```
+
+# 二、创建表
+
+```sql
+CREATE TABLE mytable (
+ # int 类型,不为空,自增
+ id INT NOT NULL AUTO_INCREMENT,
+ # int 类型,不可为空,默认值为 1,不为空
+ col1 INT NOT NULL DEFAULT 1,
+ # 变长字符串类型,最长为 45 个字符,可以为空
+ col2 VARCHAR(45) NULL,
+ # 日期类型,可为空
+ col3 DATE NULL,
+ # 设置主键为 id
+ PRIMARY KEY (`id`));
+```
+
+# 三、修改表
+
+添加列
+
+```sql
+ALTER TABLE mytable
+ADD col CHAR(20);
+```
+
+删除列
+
+```sql
+ALTER TABLE mytable
+DROP COLUMN col;
+```
+
+删除表
+
+```sql
+DROP TABLE mytable;
+```
+
+# 四、插入
+
+普通插入
+
+```sql
+INSERT INTO mytable(col1, col2)
+VALUES(val1, val2);
+```
+
+插入检索出来的数据
+
+```sql
+INSERT INTO mytable1(col1, col2)
+SELECT col1, col2
+FROM mytable2;
+```
+
+将一个表的内容插入到一个新表
+
+```sql
+CREATE TABLE newtable AS
+SELECT * FROM mytable;
+```
+
+# 五、更新
+
+```sql
+UPDATE mytable
+SET col = val
+WHERE id = 1;
+```
+
+# 六、删除
+
+```sql
+DELETE FROM mytable
+WHERE id = 1;
+```
+
+**TRUNCATE TABLE** 可以清空表,也就是删除所有行。
+
+```sql
+TRUNCATE TABLE mytable;
+```
+
+使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。
+
+# 七、查询
+
+## DISTINCT
+
+相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。
+
+```sql
+SELECT DISTINCT col1, col2
+FROM mytable;
+```
+
+## LIMIT
+
+限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。
+
+返回前 5 行:
+
+```sql
+SELECT *
+FROM mytable
+LIMIT 5;
+```
+
+```sql
+SELECT *
+FROM mytable
+LIMIT 0, 5;
+```
+
+返回第 3 \~ 5 行:
+
+```sql
+SELECT *
+FROM mytable
+LIMIT 2, 3;
+```
+
+# 八、排序
+
+- **ASC** :升序(默认)
+- **DESC** :降序
+
+可以按多个列进行排序,并且为每个列指定不同的排序方式:
+
+```sql
+SELECT *
+FROM mytable
+ORDER BY col1 DESC, col2 ASC;
+```
+
+# 九、过滤
+
+不进行过滤的数据非常大,导致通过网络传输了多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。
+
+```sql
+SELECT *
+FROM mytable
+WHERE col IS NULL;
+```
+
+下表显示了 WHERE 子句可用的操作符
+
+| 操作符 | 说明 |
+| :---: | :---: |
+| = | 等于 |
+| < | 小于 |
+| > | 大于 |
+| <> != | 不等于 |
+| <= !> | 小于等于 |
+| >= !< | 大于等于 |
+| BETWEEN | 在两个值之间 |
+| IS NULL | 为 NULL 值 |
+
+应该注意到,NULL 与 0、空字符串都不同。
+
+**AND 和 OR** 用于连接多个过滤条件。优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。
+
+**IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。
+
+**NOT** 操作符用于否定一个条件。
+
+# 十、通配符
+
+通配符也是用在过滤语句中,但它只能用于文本字段。
+
+- **%** 匹配 >=0 个任意字符;
+
+- **\_** 匹配 ==1 个任意字符;
+
+- **[ ]** 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。
+
+使用 Like 来进行通配符匹配。
+
+```sql
+SELECT *
+FROM mytable
+WHERE col LIKE '[^AB]%'; -- 不以 A 和 B 开头的任意文本
+```
+
+不要滥用通配符,通配符位于开头处匹配会非常慢。
+
+# 十一、计算字段
+
+在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。
+
+计算字段通常需要使用 **AS** 来取别名,否则输出的时候字段名为计算表达式。
+
+```sql
+SELECT col1 * col2 AS alias
+FROM mytable;
+```
+
+**CONCAT()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。
+
+```sql
+SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col
+FROM mytable;
+```
+
+# 十二、函数
+
+各个 DBMS 的函数都是不相同的,因此不可移植,以下主要是 MySQL 的函数。
+
+## 汇总
+
+|函 数 |说 明|
+| :---: | :---: |
+| AVG() | 返回某列的平均值 |
+| COUNT() | 返回某列的行数 |
+| MAX() | 返回某列的最大值 |
+| MIN() | 返回某列的最小值 |
+| SUM() |返回某列值之和 |
+
+AVG() 会忽略 NULL 行。
+
+使用 DISTINCT 可以汇总不同的值。
+
+```sql
+SELECT AVG(DISTINCT col1) AS avg_col
+FROM mytable;
+```
+
+## 文本处理
+
+| 函数 | 说明 |
+| :---: | :---: |
+| LEFT() | 左边的字符 |
+| RIGHT() | 右边的字符 |
+| LOWER() | 转换为小写字符 |
+| UPPER() | 转换为大写字符 |
+| LTRIM() | 去除左边的空格 |
+| RTRIM() | 去除右边的空格 |
+| LENGTH() | 长度 |
+| SOUNDEX() | 转换为语音值 |
+
+其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。
+
+```sql
+SELECT *
+FROM mytable
+WHERE SOUNDEX(col1) = SOUNDEX('apple')
+```
+
+## 日期和时间处理
+
+
+- 日期格式:YYYY-MM-DD
+- 时间格式:HH:MM:SS
+
+|函 数 | 说 明|
+| :---: | :---: |
+| ADDDATE() | 增加一个日期(天、周等)|
+| ADDTIME() | 增加一个时间(时、分等)|
+| CURDATE() | 返回当前日期 |
+| CURTIME() | 返回当前时间 |
+| DATE() |返回日期时间的日期部分|
+| DATEDIFF() |计算两个日期之差|
+| DATE_ADD() |高度灵活的日期运算函数|
+| DATE_FORMAT() |返回一个格式化的日期或时间串|
+| DAY()| 返回一个日期的天数部分|
+| DAYOFWEEK() |对于一个日期,返回对应的星期几|
+| HOUR() |返回一个时间的小时部分|
+| MINUTE() |返回一个时间的分钟部分|
+| MONTH() |返回一个日期的月份部分|
+| NOW() |返回当前日期和时间|
+| SECOND() |返回一个时间的秒部分|
+| TIME() |返回一个日期时间的时间部分|
+| YEAR() |返回一个日期的年份部分|
+
+```sql
+mysql> SELECT NOW();
+```
+
+```
+2018-4-14 20:25:11
+```
+
+## 数值处理
+
+| 函数 | 说明 |
+| :---: | :---: |
+| SIN() | 正弦 |
+| COS() | 余弦 |
+| TAN() | 正切 |
+| ABS() | 绝对值 |
+| SQRT() | 平方根 |
+| MOD() | 余数 |
+| EXP() | 指数 |
+| PI() | 圆周率 |
+| RAND() | 随机数 |
+
+# 十三、分组
+
+把具有相同的数据值的行放在同一组中。
+
+可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。
+
+指定的分组字段除了能按该字段进行分组,也会自动按该字段进行排序。
+
+```sql
+SELECT col, COUNT(*) AS num
+FROM mytable
+GROUP BY col;
+```
+
+GROUP BY 自动按分组字段进行排序,ORDER BY 也可以按汇总字段来进行排序。
+
+```sql
+SELECT col, COUNT(*) AS num
+FROM mytable
+GROUP BY col
+ORDER BY num;
+```
+
+WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。
+
+```sql
+SELECT col, COUNT(*) AS num
+FROM mytable
+WHERE col > 2
+GROUP BY col
+HAVING num >= 2;
+```
+
+分组规定:
+
+- GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前;
+- 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出;
+- NULL 的行会单独分为一组;
+- 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。
+
+# 十四、子查询
+
+子查询中只能返回一个字段的数据。
+
+可以将子查询的结果作为 WHRER 语句的过滤条件:
+
+```sql
+SELECT *
+FROM mytable1
+WHERE col1 IN (SELECT col2
+ FROM mytable2);
+```
+
+下面的语句可以检索出客户的订单数量,子查询语句会对第一个查询检索出的每个客户执行一次:
+
+```sql
+SELECT cust_name, (SELECT COUNT(*)
+ FROM Orders
+ WHERE Orders.cust_id = Customers.cust_id)
+ AS orders_num
+FROM Customers
+ORDER BY cust_name;
+```
+
+# 十五、连接
+
+连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。
+
+连接可以替换子查询,并且比子查询的效率一般会更快。
+
+可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。
+
+## 内连接
+
+内连接又称等值连接,使用 INNER JOIN 关键字。
+
+```sql
+SELECT A.value, B.value
+FROM tablea AS A INNER JOIN tableb AS B
+ON A.key = B.key;
+```
+
+可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。
+
+```sql
+SELECT A.value, B.value
+FROM tablea AS A, tableb AS B
+WHERE A.key = B.key;
+```
+
+## 自连接
+
+自连接可以看成内连接的一种,只是连接的表是自身而已。
+
+一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。
+
+子查询版本
+
+```sql
+SELECT name
+FROM employee
+WHERE department = (
+ SELECT department
+ FROM employee
+ WHERE name = "Jim");
+```
+
+自连接版本
+
+```sql
+SELECT e1.name
+FROM employee AS e1 INNER JOIN employee AS e2
+ON e1.department = e2.department
+ AND e2.name = "Jim";
+```
+
+## 自然连接
+
+自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。
+
+内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。
+
+```sql
+SELECT A.value, B.value
+FROM tablea AS A NATURAL JOIN tableb AS B;
+```
+
+## 外连接
+
+外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表没有关联的行。
+
+检索所有顾客的订单信息,包括还没有订单信息的顾客。
+
+```sql
+SELECT Customers.cust_id, Customer.cust_name, Orders.order_id
+FROM Customers LEFT OUTER JOIN Orders
+ON Customers.cust_id = Orders.cust_id;
+```
+
+customers 表:
+
+| cust_id | cust_name |
+| :---: | :---: |
+| 1 | a |
+| 2 | b |
+| 3 | c |
+
+orders 表:
+
+| order_id | cust_id |
+| :---: | :---: |
+|1 | 1 |
+|2 | 1 |
+|3 | 3 |
+|4 | 3 |
+
+结果:
+
+| cust_id | cust_name | order_id |
+| :---: | :---: | :---: |
+| 1 | a | 1 |
+| 1 | a | 2 |
+| 3 | c | 3 |
+| 3 | c | 4 |
+| 2 | b | Null |
+
+# 十六、组合查询
+
+使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。
+
+每个查询必须包含相同的列、表达式和聚集函数。
+
+默认会去除相同行,如果需要保留相同行,使用 UNION ALL。
+
+只能包含一个 ORDER BY 子句,并且必须位于语句的最后。
+
+```sql
+SELECT col
+FROM mytable
+WHERE col = 1
+UNION
+SELECT col
+FROM mytable
+WHERE col =2;
+```
+
+# 十七、视图
+
+视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。
+
+对视图的操作和对普通表的操作一样。
+
+视图具有如下好处:
+
+- 简化复杂的 SQL 操作,比如复杂的连接;
+- 只使用实际表的一部分数据;
+- 通过只给用户访问视图的权限,保证数据的安全性;
+- 更改数据格式和表示。
+
+```sql
+CREATE VIEW myview AS
+SELECT Concat(col1, col2) AS concat_col, col3*col4 AS compute_col
+FROM mytable
+WHERE col5 = val;
+```
+
+# 十八、存储过程
+
+存储过程可以看成是对一系列 SQL 操作的批处理。
+
+使用存储过程的好处:
+
+- 代码封装,保证了一定的安全性;
+- 代码复用;
+- 由于是预先编译,因此具有很高的性能。
+
+命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。
+
+包含 in、out 和 inout 三种参数。
+
+给变量赋值都需要用 select into 语句。
+
+每次只能给一个变量赋值,不支持集合的操作。
+
+```sql
+delimiter //
+
+create procedure myprocedure( out ret int )
+ begin
+ declare y int;
+ select sum(col1)
+ from mytable
+ into y;
+ select y*y into ret;
+ end //
+
+delimiter ;
+```
+
+```sql
+call myprocedure(@ret);
+select @ret;
+```
+
+# 十九、游标
+
+在存储过程中使用游标可以对一个结果集进行移动遍历。
+
+游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。
+
+使用游标的四个步骤:
+
+1. 声明游标,这个过程没有实际检索出数据;
+2. 打开游标;
+3. 取出数据;
+4. 关闭游标;
+
+```sql
+delimiter //
+create procedure myprocedure(out ret int)
+ begin
+ declare done boolean default 0;
+
+ declare mycursor cursor for
+ select col1 from mytable;
+ # 定义了一个 continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1
+ declare continue handler for sqlstate '02000' set done = 1;
+
+ open mycursor;
+
+ repeat
+ fetch mycursor into ret;
+ select ret;
+ until done end repeat;
+
+ close mycursor;
+ end //
+ delimiter ;
+```
+
+# 二十、触发器
+
+触发器会在某个表执行以下语句时而自动执行:DELETE、INSERT、UPDATE。
+
+触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化,AFTER 用于审计跟踪,将修改记录到另外一张表中。
+
+INSERT 触发器包含一个名为 NEW 的虚拟表。
+
+```sql
+CREATE TRIGGER mytrigger AFTER INSERT ON mytable
+FOR EACH ROW SELECT NEW.col into @result;
+
+SELECT @result; -- 获取结果
+```
+
+DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。
+
+UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改的,而 OLD 是只读的。
+
+MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。
+
+# 二十一、事务管理
+
+基本术语:
+
+- 事务(transaction)指一组 SQL 语句;
+- 回退(rollback)指撤销指定 SQL 语句的过程;
+- 提交(commit)指将未存储的 SQL 语句结果写入数据库表;
+- 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。
+
+不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。
+
+MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
+
+设置 autocommit 为 0 可以取消自动提交;autocommit 标记是针对每个连接而不是针对服务器的。
+
+如果没有设置保留点,ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。
+
+```sql
+START TRANSACTION
+// ...
+SAVEPOINT delete1
+// ...
+ROLLBACK TO delete1
+// ...
+COMMIT
+```
+
+# 二十二、字符集
+
+基本术语:
+
+- 字符集为字母和符号的集合;
+- 编码为某个字符集成员的内部表示;
+- 校对字符指定如何比较,主要用于排序和分组。
+
+除了给表指定字符集和校对外,也可以给列指定:
+
+```sql
+CREATE TABLE mytable
+(col VARCHAR(10) CHARACTER SET latin COLLATE latin1_general_ci )
+DEFAULT CHARACTER SET hebrew COLLATE hebrew_general_ci;
+```
+
+可以在排序、分组时指定校对:
+
+```sql
+SELECT *
+FROM mytable
+ORDER BY col COLLATE latin1_general_ci;
+```
+
+# 二十三、权限管理
+
+MySQL 的账户信息保存在 mysql 这个数据库中。
+
+```sql
+USE mysql;
+SELECT user FROM user;
+```
+
+**创建账户**
+
+新创建的账户没有任何权限。
+
+```sql
+CREATE USER myuser IDENTIFIED BY 'mypassword';
+```
+
+**修改账户名**
+
+```sql
+RENAME USER myuser TO newuser;
+```
+
+**删除账户**
+
+```sql
+DROP USER myuser;
+```
+
+**查看权限**
+
+```sql
+SHOW GRANTS FOR myuser;
+```
+
+**授予权限**
+
+账户用 username@host 的形式定义,username@% 使用的是默认主机名。
+
+```sql
+GRANT SELECT, INSERT ON mydatabase.* TO myuser;
+```
+
+**删除权限**
+
+GRANT 和 REVOKE 可在几个层次上控制访问权限:
+
+- 整个服务器,使用 GRANT ALL 和 REVOKE ALL;
+- 整个数据库,使用 ON database.\*;
+- 特定的表,使用 ON database.table;
+- 特定的列;
+- 特定的存储过程。
+
+```sql
+REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
+```
+
+**更改密码**
+
+必须使用 Password() 函数进行加密。
+
+```sql
+SET PASSWROD FOR myuser = Password('new_password');
+```
+
+# 参考资料
+
+- BenForta. SQL 必知必会 [M]. 人民邮电出版社, 2013.
+
+
+
+
+
+
+
diff --git a/notes/SQL 练习.md b/notes/SQL 练习.md
new file mode 100644
index 00000000..92f90ae6
--- /dev/null
+++ b/notes/SQL 练习.md
@@ -0,0 +1,1143 @@
+
+* [595. Big Countries](#595-big-countries)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [627. Swap Salary](#627-swap-salary)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [620. Not Boring Movies](#620-not-boring-movies)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [596. Classes More Than 5 Students](#596-classes-more-than-5-students)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [182. Duplicate Emails](#182-duplicate-emails)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [196. Delete Duplicate Emails](#196-delete-duplicate-emails)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [175. Combine Two Tables](#175-combine-two-tables)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [181. Employees Earning More Than Their Managers](#181-employees-earning-more-than-their-managers)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [183. Customers Who Never Order](#183-customers-who-never-order)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [184. Department Highest Salary](#184-department-highest-salary)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [176. Second Highest Salary](#176-second-highest-salary)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [177. Nth Highest Salary](#177-nth-highest-salary)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [178. Rank Scores](#178-rank-scores)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [180. Consecutive Numbers](#180-consecutive-numbers)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+* [626. Exchange Seats](#626-exchange-seats)
+ * [Description](#description)
+ * [Solution](#solution)
+ * [SQL Schema](#sql-schema)
+
+
+
+# 595. Big Countries
+
+https://leetcode.com/problems/big-countries/description/
+
+## Description
+
+```html
++-----------------+------------+------------+--------------+---------------+
+| name | continent | area | population | gdp |
++-----------------+------------+------------+--------------+---------------+
+| Afghanistan | Asia | 652230 | 25500100 | 20343000 |
+| Albania | Europe | 28748 | 2831741 | 12960000 |
+| Algeria | Africa | 2381741 | 37100000 | 188681000 |
+| Andorra | Europe | 468 | 78115 | 3712000 |
+| Angola | Africa | 1246700 | 20609294 | 100990000 |
++-----------------+------------+------------+--------------+---------------+
+```
+
+查找面积超过 3,000,000 或者人口数超过 25,000,000 的国家。
+
+```html
++--------------+-------------+--------------+
+| name | population | area |
++--------------+-------------+--------------+
+| Afghanistan | 25500100 | 652230 |
+| Algeria | 37100000 | 2381741 |
++--------------+-------------+--------------+
+```
+
+## Solution
+
+```sql
+SELECT name,
+ population,
+ area
+FROM
+ World
+WHERE
+ area > 3000000
+ OR population > 25000000;
+```
+
+## SQL Schema
+
+SQL Schema 用于在本地环境下创建表结构并导入数据,从而方便在本地环境调试。
+
+```sql
+DROP TABLE
+IF
+ EXISTS World;
+CREATE TABLE World ( NAME VARCHAR ( 255 ), continent VARCHAR ( 255 ), area INT, population INT, gdp INT );
+INSERT INTO World ( NAME, continent, area, population, gdp )
+VALUES
+ ( 'Afghanistan', 'Asia', '652230', '25500100', '203430000' ),
+ ( 'Albania', 'Europe', '28748', '2831741', '129600000' ),
+ ( 'Algeria', 'Africa', '2381741', '37100000', '1886810000' ),
+ ( 'Andorra', 'Europe', '468', '78115', '37120000' ),
+ ( 'Angola', 'Africa', '1246700', '20609294', '1009900000' );
+```
+
+# 627. Swap Salary
+
+https://leetcode.com/problems/swap-salary/description/
+
+## Description
+
+```html
+| id | name | sex | salary |
+|----|------|-----|--------|
+| 1 | A | m | 2500 |
+| 2 | B | f | 1500 |
+| 3 | C | m | 5500 |
+| 4 | D | f | 500 |
+```
+
+只用一个 SQL 查询,将 sex 字段反转。
+
+```html
+| id | name | sex | salary |
+|----|------|-----|--------|
+| 1 | A | f | 2500 |
+| 2 | B | m | 1500 |
+| 3 | C | f | 5500 |
+| 4 | D | m | 500 |
+```
+
+## Solution
+
+两个相等的数异或的结果为 0,而 0 与任何一个数异或的结果为这个数。
+
+sex 字段只有两个取值:'f' 和 'm',并且有以下规律:
+
+```
+'f' ^ ('m' ^ 'f') = 'm' ^ ('f' ^ 'f') = 'm'
+'m' ^ ('m' ^ 'f') = 'f' ^ ('m' ^ 'm') = 'f'
+```
+
+因此将 sex 字段和 'm' ^ 'f' 进行异或操作,最后就能反转 sex 字段。
+
+```sql
+UPDATE salary
+SET sex = CHAR ( ASCII(sex) ^ ASCII( 'm' ) ^ ASCII( 'f' ) );
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS salary;
+CREATE TABLE salary ( id INT, NAME VARCHAR ( 100 ), sex CHAR ( 1 ), salary INT );
+INSERT INTO salary ( id, NAME, sex, salary )
+VALUES
+ ( '1', 'A', 'm', '2500' ),
+ ( '2', 'B', 'f', '1500' ),
+ ( '3', 'C', 'm', '5500' ),
+ ( '4', 'D', 'f', '500' );
+```
+
+# 620. Not Boring Movies
+
+https://leetcode.com/problems/not-boring-movies/description/
+
+## Description
+
+
+```html
++---------+-----------+--------------+-----------+
+| id | movie | description | rating |
++---------+-----------+--------------+-----------+
+| 1 | War | great 3D | 8.9 |
+| 2 | Science | fiction | 8.5 |
+| 3 | irish | boring | 6.2 |
+| 4 | Ice song | Fantacy | 8.6 |
+| 5 | House card| Interesting| 9.1 |
++---------+-----------+--------------+-----------+
+```
+
+查找 id 为奇数,并且 description 不是 boring 的电影,按 rating 降序。
+
+```html
++---------+-----------+--------------+-----------+
+| id | movie | description | rating |
++---------+-----------+--------------+-----------+
+| 5 | House card| Interesting| 9.1 |
+| 1 | War | great 3D | 8.9 |
++---------+-----------+--------------+-----------+
+```
+
+## Solution
+
+```sql
+SELECT
+ *
+FROM
+ cinema
+WHERE
+ id % 2 = 1
+ AND description != 'boring'
+ORDER BY
+ rating DESC;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS cinema;
+CREATE TABLE cinema ( id INT, movie VARCHAR ( 255 ), description VARCHAR ( 255 ), rating FLOAT ( 2, 1 ) );
+INSERT INTO cinema ( id, movie, description, rating )
+VALUES
+ ( 1, 'War', 'great 3D', 8.9 ),
+ ( 2, 'Science', 'fiction', 8.5 ),
+ ( 3, 'irish', 'boring', 6.2 ),
+ ( 4, 'Ice song', 'Fantacy', 8.6 ),
+ ( 5, 'House card', 'Interesting', 9.1 );
+```
+
+# 596. Classes More Than 5 Students
+
+https://leetcode.com/problems/classes-more-than-5-students/description/
+
+## Description
+
+```html
++---------+------------+
+| student | class |
++---------+------------+
+| A | Math |
+| B | English |
+| C | Math |
+| D | Biology |
+| E | Math |
+| F | Computer |
+| G | Math |
+| H | Math |
+| I | Math |
++---------+------------+
+```
+
+查找有五名及以上 student 的 class。
+
+```html
++---------+
+| class |
++---------+
+| Math |
++---------+
+```
+
+## Solution
+
+对 class 列进行分组之后,再使用 count 汇总函数统计每个分组的记录个数,之后使用 HAVING 进行筛选。HAVING 针对分组进行筛选,而 WHERE 针对每个记录(行)进行筛选。
+
+```sql
+SELECT
+ class
+FROM
+ courses
+GROUP BY
+ class
+HAVING
+ count( DISTINCT student ) >= 5;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS courses;
+CREATE TABLE courses ( student VARCHAR ( 255 ), class VARCHAR ( 255 ) );
+INSERT INTO courses ( student, class )
+VALUES
+ ( 'A', 'Math' ),
+ ( 'B', 'English' ),
+ ( 'C', 'Math' ),
+ ( 'D', 'Biology' ),
+ ( 'E', 'Math' ),
+ ( 'F', 'Computer' ),
+ ( 'G', 'Math' ),
+ ( 'H', 'Math' ),
+ ( 'I', 'Math' );
+```
+
+# 182. Duplicate Emails
+
+https://leetcode.com/problems/duplicate-emails/description/
+
+## Description
+
+邮件地址表:
+
+```html
++----+---------+
+| Id | Email |
++----+---------+
+| 1 | a@b.com |
+| 2 | c@d.com |
+| 3 | a@b.com |
++----+---------+
+```
+
+查找重复的邮件地址:
+
+```html
++---------+
+| Email |
++---------+
+| a@b.com |
++---------+
+```
+
+## Solution
+
+对 Email 进行分组,如果并使用 COUNT 进行计数统计,结果大于等于 2 的表示 Email 重复。
+
+```sql
+SELECT
+ Email
+FROM
+ Person
+GROUP BY
+ Email
+HAVING
+ COUNT( * ) >= 2;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Person;
+CREATE TABLE Person ( Id INT, Email VARCHAR ( 255 ) );
+INSERT INTO Person ( Id, Email )
+VALUES
+ ( 1, 'a@b.com' ),
+ ( 2, 'c@d.com' ),
+ ( 3, 'a@b.com' );
+```
+
+
+# 196. Delete Duplicate Emails
+
+https://leetcode.com/problems/delete-duplicate-emails/description/
+
+## Description
+
+邮件地址表:
+
+```html
++----+---------+
+| Id | Email |
++----+---------+
+| 1 | john@example.com |
+| 2 | bob@example.com |
+| 3 | john@example.com |
++----+---------+
+```
+
+删除重复的邮件地址:
+
+```html
++----+------------------+
+| Id | Email |
++----+------------------+
+| 1 | john@example.com |
+| 2 | bob@example.com |
++----+------------------+
+```
+
+## Solution
+
+只保留相同 Email 中 Id 最小的那一个,然后删除其它的。
+
+连接查询:
+
+```sql
+DELETE p1
+FROM
+ Person p1,
+ Person p2
+WHERE
+ p1.Email = p2.Email
+ AND p1.Id > p2.Id
+```
+
+子查询:
+
+```sql
+DELETE
+FROM
+ Person
+WHERE
+ id NOT IN (
+ SELECT id
+ FROM (
+ SELECT min( id ) AS id
+ FROM Person
+ GROUP BY email
+ ) AS m
+ );
+```
+
+应该注意的是上述解法额外嵌套了一个 SELECT 语句,如果不这么做,会出现错误:You can't specify target table 'Person' for update in FROM clause。以下演示了这种错误解法。
+
+```sql
+DELETE
+FROM
+ Person
+WHERE
+ id NOT IN (
+ SELECT min( id ) AS id
+ FROM Person
+ GROUP BY email
+ );
+```
+
+参考:[pMySQL Error 1093 - Can't specify target table for update in FROM clause](https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause)
+
+## SQL Schema
+
+与 182 相同。
+
+# 175. Combine Two Tables
+
+https://leetcode.com/problems/combine-two-tables/description/
+
+## Description
+
+Person 表:
+
+```html
++-------------+---------+
+| Column Name | Type |
++-------------+---------+
+| PersonId | int |
+| FirstName | varchar |
+| LastName | varchar |
++-------------+---------+
+PersonId is the primary key column for this table.
+```
+
+Address 表:
+
+```html
++-------------+---------+
+| Column Name | Type |
++-------------+---------+
+| AddressId | int |
+| PersonId | int |
+| City | varchar |
+| State | varchar |
++-------------+---------+
+AddressId is the primary key column for this table.
+```
+
+查找 FirstName, LastName, City, State 数据,而不管一个用户有没有填地址信息。
+
+## Solution
+
+涉及到 Person 和 Address 两个表,在对这两个表执行连接操作时,因为要保留 Person 表中的信息,即使在 Address 表中没有关联的信息也要保留。此时可以用左外连接,将 Person 表放在 LEFT JOIN 的左边。
+
+```sql
+SELECT
+ FirstName,
+ LastName,
+ City,
+ State
+FROM
+ Person P
+ LEFT JOIN Address A
+ ON P.PersonId = A.PersonId;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Person;
+CREATE TABLE Person ( PersonId INT, FirstName VARCHAR ( 255 ), LastName VARCHAR ( 255 ) );
+DROP TABLE
+IF
+ EXISTS Address;
+CREATE TABLE Address ( AddressId INT, PersonId INT, City VARCHAR ( 255 ), State VARCHAR ( 255 ) );
+INSERT INTO Person ( PersonId, LastName, FirstName )
+VALUES
+ ( 1, 'Wang', 'Allen' );
+INSERT INTO Address ( AddressId, PersonId, City, State )
+VALUES
+ ( 1, 2, 'New York City', 'New York' );
+```
+
+# 181. Employees Earning More Than Their Managers
+
+https://leetcode.com/problems/employees-earning-more-than-their-managers/description/
+
+## Description
+
+Employee 表:
+
+```html
++----+-------+--------+-----------+
+| Id | Name | Salary | ManagerId |
++----+-------+--------+-----------+
+| 1 | Joe | 70000 | 3 |
+| 2 | Henry | 80000 | 4 |
+| 3 | Sam | 60000 | NULL |
+| 4 | Max | 90000 | NULL |
++----+-------+--------+-----------+
+```
+
+查找薪资大于其经理薪资的员工信息。
+
+## Solution
+
+```sql
+SELECT
+ E1.NAME AS Employee
+FROM
+ Employee E1
+ INNER JOIN Employee E2
+ ON E1.ManagerId = E2.Id
+ AND E1.Salary > E2.Salary;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Employee;
+CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, ManagerId INT );
+INSERT INTO Employee ( Id, NAME, Salary, ManagerId )
+VALUES
+ ( 1, 'Joe', 70000, 3 ),
+ ( 2, 'Henry', 80000, 4 ),
+ ( 3, 'Sam', 60000, NULL ),
+ ( 4, 'Max', 90000, NULL );
+```
+
+# 183. Customers Who Never Order
+
+https://leetcode.com/problems/customers-who-never-order/description/
+
+## Description
+
+Customers 表:
+
+```html
++----+-------+
+| Id | Name |
++----+-------+
+| 1 | Joe |
+| 2 | Henry |
+| 3 | Sam |
+| 4 | Max |
++----+-------+
+```
+
+Orders 表:
+
+```html
++----+------------+
+| Id | CustomerId |
++----+------------+
+| 1 | 3 |
+| 2 | 1 |
++----+------------+
+```
+
+查找没有订单的顾客信息:
+
+```html
++-----------+
+| Customers |
++-----------+
+| Henry |
+| Max |
++-----------+
+```
+
+## Solution
+
+左外链接
+
+```sql
+SELECT
+ C.Name AS Customers
+FROM
+ Customers C
+ LEFT JOIN Orders O
+ ON C.Id = O.CustomerId
+WHERE
+ O.CustomerId IS NULL;
+```
+
+子查询
+
+```sql
+SELECT
+ Name AS Customers
+FROM
+ Customers
+WHERE
+ Id NOT IN (
+ SELECT CustomerId
+ FROM Orders
+ );
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Customers;
+CREATE TABLE Customers ( Id INT, NAME VARCHAR ( 255 ) );
+DROP TABLE
+IF
+ EXISTS Orders;
+CREATE TABLE Orders ( Id INT, CustomerId INT );
+INSERT INTO Customers ( Id, NAME )
+VALUES
+ ( 1, 'Joe' ),
+ ( 2, 'Henry' ),
+ ( 3, 'Sam' ),
+ ( 4, 'Max' );
+INSERT INTO Orders ( Id, CustomerId )
+VALUES
+ ( 1, 3 ),
+ ( 2, 1 );
+```
+
+# 184. Department Highest Salary
+
+https://leetcode.com/problems/department-highest-salary/description/
+
+## Description
+
+Employee 表:
+
+```html
++----+-------+--------+--------------+
+| Id | Name | Salary | DepartmentId |
++----+-------+--------+--------------+
+| 1 | Joe | 70000 | 1 |
+| 2 | Henry | 80000 | 2 |
+| 3 | Sam | 60000 | 2 |
+| 4 | Max | 90000 | 1 |
++----+-------+--------+--------------+
+```
+
+Department 表:
+
+```html
++----+----------+
+| Id | Name |
++----+----------+
+| 1 | IT |
+| 2 | Sales |
++----+----------+
+```
+
+查找一个 Department 中收入最高者的信息:
+
+```html
++------------+----------+--------+
+| Department | Employee | Salary |
++------------+----------+--------+
+| IT | Max | 90000 |
+| Sales | Henry | 80000 |
++------------+----------+--------+
+```
+
+## Solution
+
+创建一个临时表,包含了部门员工的最大薪资。可以对部门进行分组,然后使用 MAX() 汇总函数取得最大薪资。
+
+之后使用连接找到一个部门中薪资等于临时表中最大薪资的员工。
+
+```sql
+SELECT
+ D.NAME Department,
+ E.NAME Employee,
+ E.Salary
+FROM
+ Employee E,
+ Department D,
+ ( SELECT DepartmentId, MAX( Salary ) Salary
+ FROM Employee
+ GROUP BY DepartmentId ) M
+WHERE
+ E.DepartmentId = D.Id
+ AND E.DepartmentId = M.DepartmentId
+ AND E.Salary = M.Salary;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE IF EXISTS Employee;
+CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, DepartmentId INT );
+DROP TABLE IF EXISTS Department;
+CREATE TABLE Department ( Id INT, NAME VARCHAR ( 255 ) );
+INSERT INTO Employee ( Id, NAME, Salary, DepartmentId )
+VALUES
+ ( 1, 'Joe', 70000, 1 ),
+ ( 2, 'Henry', 80000, 2 ),
+ ( 3, 'Sam', 60000, 2 ),
+ ( 4, 'Max', 90000, 1 );
+INSERT INTO Department ( Id, NAME )
+VALUES
+ ( 1, 'IT' ),
+ ( 2, 'Sales' );
+```
+
+
+# 176. Second Highest Salary
+
+https://leetcode.com/problems/second-highest-salary/description/
+
+## Description
+
+```html
++----+--------+
+| Id | Salary |
++----+--------+
+| 1 | 100 |
+| 2 | 200 |
+| 3 | 300 |
++----+--------+
+```
+
+查找工资第二高的员工。
+
+```html
++---------------------+
+| SecondHighestSalary |
++---------------------+
+| 200 |
++---------------------+
+```
+
+没有找到返回 null 而不是不返回数据。
+
+## Solution
+
+为了在没有查找到数据时返回 null,需要在查询结果外面再套一层 SELECT。
+
+```sql
+SELECT
+ ( SELECT DISTINCT Salary
+ FROM Employee
+ ORDER BY Salary DESC
+ LIMIT 1, 1 ) SecondHighestSalary;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Employee;
+CREATE TABLE Employee ( Id INT, Salary INT );
+INSERT INTO Employee ( Id, Salary )
+VALUES
+ ( 1, 100 ),
+ ( 2, 200 ),
+ ( 3, 300 );
+```
+
+# 177. Nth Highest Salary
+
+## Description
+
+查找工资第 N 高的员工。
+
+## Solution
+
+```sql
+CREATE FUNCTION getNthHighestSalary ( N INT ) RETURNS INT BEGIN
+
+SET N = N - 1;
+RETURN (
+ SELECT (
+ SELECT DISTINCT Salary
+ FROM Employee
+ ORDER BY Salary DESC
+ LIMIT N, 1
+ )
+);
+
+END
+```
+
+## SQL Schema
+
+同 176。
+
+
+# 178. Rank Scores
+
+https://leetcode.com/problems/rank-scores/description/
+
+## Description
+
+得分表:
+
+```html
++----+-------+
+| Id | Score |
++----+-------+
+| 1 | 3.50 |
+| 2 | 3.65 |
+| 3 | 4.00 |
+| 4 | 3.85 |
+| 5 | 4.00 |
+| 6 | 3.65 |
++----+-------+
+```
+
+将得分排序,并统计排名。
+
+```html
++-------+------+
+| Score | Rank |
++-------+------+
+| 4.00 | 1 |
+| 4.00 | 1 |
+| 3.85 | 2 |
+| 3.65 | 3 |
+| 3.65 | 3 |
+| 3.50 | 4 |
++-------+------+
+```
+
+## Solution
+
+要统计某个 score 的排名,只要统计大于等于该 score 的 score 数量。
+
+| Id | score | 大于等于该 score 的 score 数量 | 排名 |
+| :---: | :---: | :---: | :---: |
+| 1 | 4.1 | 3 | 3 |
+| 2 | 4.2 | 2 | 2 |
+| 3 | 4.3 | 1 | 1 |
+
+使用连接操作找到某个 score 对应的大于等于其值的记录:
+
+```sql
+SELECT
+ *
+FROM
+ Scores S1
+ INNER JOIN Scores S2
+ ON S1.score <= S2.score
+ORDER BY
+ S1.score DESC, S1.Id;
+```
+
+| S1.Id | S1.score | S2.Id | S2.score |
+| :---: | :---: | :---: | :---: |
+|3| 4.3| 3 |4.3|
+|2| 4.2| 2| 4.2|
+|2| 4.2 |3 |4.3|
+|1| 4.1 |1| 4.1|
+|1| 4.1 |2| 4.2|
+|1| 4.1 |3| 4.3|
+
+可以看到每个 S1.score 都有对应好几条记录,我们再进行分组,并统计每个分组的数量作为 'Rank'
+
+```sql
+SELECT
+ S1.score 'Score',
+ COUNT(*) 'Rank'
+FROM
+ Scores S1
+ INNER JOIN Scores S2
+ ON S1.score <= S2.score
+GROUP BY
+ S1.id, S1.score
+ORDER BY
+ S1.score DESC, S1.Id;
+```
+
+| score | Rank |
+| :---: | :---: |
+| 4.3 | 1 |
+| 4.2 | 2 |
+| 4.1 | 3 |
+
+上面的解法看似没问题,但是对于以下数据,它却得到了错误的结果:
+
+| Id | score |
+| :---: | :---: |
+| 1 | 4.1 |
+| 2 | 4.2 |
+| 3 | 4.2 |
+
+| score | Rank |
+| :---: | :--: |
+| 4.2 | 2 |
+| 4.2 | 2 |
+| 4.1 | 3 |
+
+而我们希望的结果为:
+
+| score | Rank |
+| :---: | :--: |
+| 4.2 | 1 |
+| 4.2 | 1 |
+| 4.1 | 2 |
+
+连接情况如下:
+
+| S1.Id | S1.score | S2.Id | S2.score |
+| :---: | :------: | :---: | :------: |
+| 2 | 4.2 | 3 | 4.2 |
+| 2 | 4.2 | 2 | 4.2 |
+| 3 | 4.2 | 3 | 4.2 |
+| 3 | 4.2 | 2 | 4.1 |
+| 1 | 4.1 | 3 | 4.2 |
+| 1 | 4.1 | 2 | 4.2 |
+| 1 | 4.1 | 1 | 4.1 |
+
+我们想要的结果是,把分数相同的放在同一个排名,并且相同分数只占一个位置,例如上面的分数,Id=2 和 Id=3 的记录都有相同的分数,并且最高,他们并列第一。而 Id=1 的记录应该排第二名,而不是第三名。所以在进行 COUNT 计数统计时,我们需要使用 COUNT( DISTINCT S2.score ) 从而只统计一次相同的分数。
+
+```sql
+SELECT
+ S1.score 'Score',
+ COUNT( DISTINCT S2.score ) 'Rank'
+FROM
+ Scores S1
+ INNER JOIN Scores S2
+ ON S1.score <= S2.score
+GROUP BY
+ S1.id, S1.score
+ORDER BY
+ S1.score DESC;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS Scores;
+CREATE TABLE Scores ( Id INT, Score DECIMAL ( 3, 2 ) );
+INSERT INTO Scores ( Id, Score )
+VALUES
+ ( 1, 4.1 ),
+ ( 2, 4.1 ),
+ ( 3, 4.2 ),
+ ( 4, 4.2 ),
+ ( 5, 4.3 ),
+ ( 6, 4.3 );
+```
+
+# 180. Consecutive Numbers
+
+https://leetcode.com/problems/consecutive-numbers/description/
+
+## Description
+
+数字表:
+
+```html
++----+-----+
+| Id | Num |
++----+-----+
+| 1 | 1 |
+| 2 | 1 |
+| 3 | 1 |
+| 4 | 2 |
+| 5 | 1 |
+| 6 | 2 |
+| 7 | 2 |
++----+-----+
+```
+
+查找连续出现三次的数字。
+
+```html
++-----------------+
+| ConsecutiveNums |
++-----------------+
+| 1 |
++-----------------+
+```
+
+## Solution
+
+```sql
+SELECT
+ DISTINCT L1.num ConsecutiveNums
+FROM
+ Logs L1,
+ Logs L2,
+ Logs L3
+WHERE L1.id = l2.id - 1
+ AND L2.id = L3.id - 1
+ AND L1.num = L2.num
+ AND l2.num = l3.num;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS LOGS;
+CREATE TABLE LOGS ( Id INT, Num INT );
+INSERT INTO LOGS ( Id, Num )
+VALUES
+ ( 1, 1 ),
+ ( 2, 1 ),
+ ( 3, 1 ),
+ ( 4, 2 ),
+ ( 5, 1 ),
+ ( 6, 2 ),
+ ( 7, 2 );
+```
+
+# 626. Exchange Seats
+
+https://leetcode.com/problems/exchange-seats/description/
+
+## Description
+
+seat 表存储着座位对应的学生。
+
+```html
++---------+---------+
+| id | student |
++---------+---------+
+| 1 | Abbot |
+| 2 | Doris |
+| 3 | Emerson |
+| 4 | Green |
+| 5 | Jeames |
++---------+---------+
+```
+
+要求交换相邻座位的两个学生,如果最后一个座位是奇数,那么不交换这个座位上的学生。
+
+```html
++---------+---------+
+| id | student |
++---------+---------+
+| 1 | Doris |
+| 2 | Abbot |
+| 3 | Green |
+| 4 | Emerson |
+| 5 | Jeames |
++---------+---------+
+```
+
+## Solution
+
+使用多个 union。
+
+```sql
+# 处理偶数 id,让 id 减 1
+# 例如 2,4,6,... 变成 1,3,5,...
+SELECT
+ s1.id - 1 AS id,
+ s1.student
+FROM
+ seat s1
+WHERE
+ s1.id MOD 2 = 0 UNION
+# 处理奇数 id,让 id 加 1。但是如果最大的 id 为奇数,则不做处理
+# 例如 1,3,5,... 变成 2,4,6,...
+SELECT
+ s2.id + 1 AS id,
+ s2.student
+FROM
+ seat s2
+WHERE
+ s2.id MOD 2 = 1
+ AND s2.id != ( SELECT max( s3.id ) FROM seat s3 ) UNION
+# 如果最大的 id 为奇数,单独取出这个数
+SELECT
+ s4.id AS id,
+ s4.student
+FROM
+ seat s4
+WHERE
+ s4.id MOD 2 = 1
+ AND s4.id = ( SELECT max( s5.id ) FROM seat s5 )
+ORDER BY
+ id;
+```
+
+## SQL Schema
+
+```sql
+DROP TABLE
+IF
+ EXISTS seat;
+CREATE TABLE seat ( id INT, student VARCHAR ( 255 ) );
+INSERT INTO seat ( id, student )
+VALUES
+ ( '1', 'Abbot' ),
+ ( '2', 'Doris' ),
+ ( '3', 'Emerson' ),
+ ( '4', 'Green' ),
+ ( '5', 'Jeames' );
+```
+
+
+
+
+
+
+
diff --git a/notes/SQL 语法.md b/notes/SQL 语法.md
new file mode 100644
index 00000000..9e7adc04
--- /dev/null
+++ b/notes/SQL 语法.md
@@ -0,0 +1,788 @@
+
+* [一、基础](#一基础)
+* [二、创建表](#二创建表)
+* [三、修改表](#三修改表)
+* [四、插入](#四插入)
+* [五、更新](#五更新)
+* [六、删除](#六删除)
+* [七、查询](#七查询)
+ * [DISTINCT](#distinct)
+ * [LIMIT](#limit)
+* [八、排序](#八排序)
+* [九、过滤](#九过滤)
+* [十、通配符](#十通配符)
+* [十一、计算字段](#十一计算字段)
+* [十二、函数](#十二函数)
+ * [汇总](#汇总)
+ * [文本处理](#文本处理)
+ * [日期和时间处理](#日期和时间处理)
+ * [数值处理](#数值处理)
+* [十三、分组](#十三分组)
+* [十四、子查询](#十四子查询)
+* [十五、连接](#十五连接)
+ * [内连接](#内连接)
+ * [自连接](#自连接)
+ * [自然连接](#自然连接)
+ * [外连接](#外连接)
+* [十六、组合查询](#十六组合查询)
+* [十七、视图](#十七视图)
+* [十八、存储过程](#十八存储过程)
+* [十九、游标](#十九游标)
+* [二十、触发器](#二十触发器)
+* [二十一、事务管理](#二十一事务管理)
+* [二十二、字符集](#二十二字符集)
+* [二十三、权限管理](#二十三权限管理)
+* [参考资料](#参考资料)
+
+
+
+# 一、基础
+
+模式定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。
+
+主键的值不允许修改,也不允许复用(不能将已经删除的主键值赋给新数据行的主键)。
+
+SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。
+
+SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。
+
+SQL 支持以下三种注释:
+
+```sql
+# 注释
+SELECT *
+FROM mytable; -- 注释
+/* 注释1
+ 注释2 */
+```
+
+数据库创建与使用:
+
+```sql
+CREATE DATABASE test;
+USE test;
+```
+
+# 二、创建表
+
+```sql
+CREATE TABLE mytable (
+ # int 类型,不为空,自增
+ id INT NOT NULL AUTO_INCREMENT,
+ # int 类型,不可为空,默认值为 1,不为空
+ col1 INT NOT NULL DEFAULT 1,
+ # 变长字符串类型,最长为 45 个字符,可以为空
+ col2 VARCHAR(45) NULL,
+ # 日期类型,可为空
+ col3 DATE NULL,
+ # 设置主键为 id
+ PRIMARY KEY (`id`));
+```
+
+# 三、修改表
+
+添加列
+
+```sql
+ALTER TABLE mytable
+ADD col CHAR(20);
+```
+
+删除列
+
+```sql
+ALTER TABLE mytable
+DROP COLUMN col;
+```
+
+删除表
+
+```sql
+DROP TABLE mytable;
+```
+
+# 四、插入
+
+普通插入
+
+```sql
+INSERT INTO mytable(col1, col2)
+VALUES(val1, val2);
+```
+
+插入检索出来的数据
+
+```sql
+INSERT INTO mytable1(col1, col2)
+SELECT col1, col2
+FROM mytable2;
+```
+
+将一个表的内容插入到一个新表
+
+```sql
+CREATE TABLE newtable AS
+SELECT * FROM mytable;
+```
+
+# 五、更新
+
+```sql
+UPDATE mytable
+SET col = val
+WHERE id = 1;
+```
+
+# 六、删除
+
+```sql
+DELETE FROM mytable
+WHERE id = 1;
+```
+
+**TRUNCATE TABLE** 可以清空表,也就是删除所有行。
+
+```sql
+TRUNCATE TABLE mytable;
+```
+
+使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。
+
+# 七、查询
+
+## DISTINCT
+
+相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。
+
+```sql
+SELECT DISTINCT col1, col2
+FROM mytable;
+```
+
+## LIMIT
+
+限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。
+
+返回前 5 行:
+
+```sql
+SELECT *
+FROM mytable
+LIMIT 5;
+```
+
+```sql
+SELECT *
+FROM mytable
+LIMIT 0, 5;
+```
+
+返回第 3 \~ 5 行:
+
+```sql
+SELECT *
+FROM mytable
+LIMIT 2, 3;
+```
+
+# 八、排序
+
+- **ASC** :升序(默认)
+- **DESC** :降序
+
+可以按多个列进行排序,并且为每个列指定不同的排序方式:
+
+```sql
+SELECT *
+FROM mytable
+ORDER BY col1 DESC, col2 ASC;
+```
+
+# 九、过滤
+
+不进行过滤的数据非常大,导致通过网络传输了多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。
+
+```sql
+SELECT *
+FROM mytable
+WHERE col IS NULL;
+```
+
+下表显示了 WHERE 子句可用的操作符
+
+| 操作符 | 说明 |
+| :---: | :---: |
+| = | 等于 |
+| < | 小于 |
+| > | 大于 |
+| <> != | 不等于 |
+| <= !> | 小于等于 |
+| >= !< | 大于等于 |
+| BETWEEN | 在两个值之间 |
+| IS NULL | 为 NULL 值 |
+
+应该注意到,NULL 与 0、空字符串都不同。
+
+**AND 和 OR** 用于连接多个过滤条件。优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。
+
+**IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。
+
+**NOT** 操作符用于否定一个条件。
+
+# 十、通配符
+
+通配符也是用在过滤语句中,但它只能用于文本字段。
+
+- **%** 匹配 >=0 个任意字符;
+
+- **\_** 匹配 ==1 个任意字符;
+
+- **[ ]** 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。
+
+使用 Like 来进行通配符匹配。
+
+```sql
+SELECT *
+FROM mytable
+WHERE col LIKE '[^AB]%'; -- 不以 A 和 B 开头的任意文本
+```
+
+不要滥用通配符,通配符位于开头处匹配会非常慢。
+
+# 十一、计算字段
+
+在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。
+
+计算字段通常需要使用 **AS** 来取别名,否则输出的时候字段名为计算表达式。
+
+```sql
+SELECT col1 * col2 AS alias
+FROM mytable;
+```
+
+**CONCAT()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。
+
+```sql
+SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col
+FROM mytable;
+```
+
+# 十二、函数
+
+各个 DBMS 的函数都是不相同的,因此不可移植,以下主要是 MySQL 的函数。
+
+## 汇总
+
+|函 数 |说 明|
+| :---: | :---: |
+| AVG() | 返回某列的平均值 |
+| COUNT() | 返回某列的行数 |
+| MAX() | 返回某列的最大值 |
+| MIN() | 返回某列的最小值 |
+| SUM() |返回某列值之和 |
+
+AVG() 会忽略 NULL 行。
+
+使用 DISTINCT 可以汇总不同的值。
+
+```sql
+SELECT AVG(DISTINCT col1) AS avg_col
+FROM mytable;
+```
+
+## 文本处理
+
+| 函数 | 说明 |
+| :---: | :---: |
+| LEFT() | 左边的字符 |
+| RIGHT() | 右边的字符 |
+| LOWER() | 转换为小写字符 |
+| UPPER() | 转换为大写字符 |
+| LTRIM() | 去除左边的空格 |
+| RTRIM() | 去除右边的空格 |
+| LENGTH() | 长度 |
+| SOUNDEX() | 转换为语音值 |
+
+其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。
+
+```sql
+SELECT *
+FROM mytable
+WHERE SOUNDEX(col1) = SOUNDEX('apple')
+```
+
+## 日期和时间处理
+
+
+- 日期格式:YYYY-MM-DD
+- 时间格式:HH:MM:SS
+
+|函 数 | 说 明|
+| :---: | :---: |
+| ADDDATE() | 增加一个日期(天、周等)|
+| ADDTIME() | 增加一个时间(时、分等)|
+| CURDATE() | 返回当前日期 |
+| CURTIME() | 返回当前时间 |
+| DATE() |返回日期时间的日期部分|
+| DATEDIFF() |计算两个日期之差|
+| DATE_ADD() |高度灵活的日期运算函数|
+| DATE_FORMAT() |返回一个格式化的日期或时间串|
+| DAY()| 返回一个日期的天数部分|
+| DAYOFWEEK() |对于一个日期,返回对应的星期几|
+| HOUR() |返回一个时间的小时部分|
+| MINUTE() |返回一个时间的分钟部分|
+| MONTH() |返回一个日期的月份部分|
+| NOW() |返回当前日期和时间|
+| SECOND() |返回一个时间的秒部分|
+| TIME() |返回一个日期时间的时间部分|
+| YEAR() |返回一个日期的年份部分|
+
+```sql
+mysql> SELECT NOW();
+```
+
+```
+2018-4-14 20:25:11
+```
+
+## 数值处理
+
+| 函数 | 说明 |
+| :---: | :---: |
+| SIN() | 正弦 |
+| COS() | 余弦 |
+| TAN() | 正切 |
+| ABS() | 绝对值 |
+| SQRT() | 平方根 |
+| MOD() | 余数 |
+| EXP() | 指数 |
+| PI() | 圆周率 |
+| RAND() | 随机数 |
+
+# 十三、分组
+
+把具有相同的数据值的行放在同一组中。
+
+可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。
+
+指定的分组字段除了能按该字段进行分组,也会自动按该字段进行排序。
+
+```sql
+SELECT col, COUNT(*) AS num
+FROM mytable
+GROUP BY col;
+```
+
+GROUP BY 自动按分组字段进行排序,ORDER BY 也可以按汇总字段来进行排序。
+
+```sql
+SELECT col, COUNT(*) AS num
+FROM mytable
+GROUP BY col
+ORDER BY num;
+```
+
+WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。
+
+```sql
+SELECT col, COUNT(*) AS num
+FROM mytable
+WHERE col > 2
+GROUP BY col
+HAVING num >= 2;
+```
+
+分组规定:
+
+- GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前;
+- 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出;
+- NULL 的行会单独分为一组;
+- 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。
+
+# 十四、子查询
+
+子查询中只能返回一个字段的数据。
+
+可以将子查询的结果作为 WHRER 语句的过滤条件:
+
+```sql
+SELECT *
+FROM mytable1
+WHERE col1 IN (SELECT col2
+ FROM mytable2);
+```
+
+下面的语句可以检索出客户的订单数量,子查询语句会对第一个查询检索出的每个客户执行一次:
+
+```sql
+SELECT cust_name, (SELECT COUNT(*)
+ FROM Orders
+ WHERE Orders.cust_id = Customers.cust_id)
+ AS orders_num
+FROM Customers
+ORDER BY cust_name;
+```
+
+# 十五、连接
+
+连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。
+
+连接可以替换子查询,并且比子查询的效率一般会更快。
+
+可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。
+
+## 内连接
+
+内连接又称等值连接,使用 INNER JOIN 关键字。
+
+```sql
+SELECT A.value, B.value
+FROM tablea AS A INNER JOIN tableb AS B
+ON A.key = B.key;
+```
+
+可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。
+
+```sql
+SELECT A.value, B.value
+FROM tablea AS A, tableb AS B
+WHERE A.key = B.key;
+```
+
+## 自连接
+
+自连接可以看成内连接的一种,只是连接的表是自身而已。
+
+一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。
+
+子查询版本
+
+```sql
+SELECT name
+FROM employee
+WHERE department = (
+ SELECT department
+ FROM employee
+ WHERE name = "Jim");
+```
+
+自连接版本
+
+```sql
+SELECT e1.name
+FROM employee AS e1 INNER JOIN employee AS e2
+ON e1.department = e2.department
+ AND e2.name = "Jim";
+```
+
+## 自然连接
+
+自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。
+
+内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。
+
+```sql
+SELECT A.value, B.value
+FROM tablea AS A NATURAL JOIN tableb AS B;
+```
+
+## 外连接
+
+外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表没有关联的行。
+
+检索所有顾客的订单信息,包括还没有订单信息的顾客。
+
+```sql
+SELECT Customers.cust_id, Customer.cust_name, Orders.order_id
+FROM Customers LEFT OUTER JOIN Orders
+ON Customers.cust_id = Orders.cust_id;
+```
+
+customers 表:
+
+| cust_id | cust_name |
+| :---: | :---: |
+| 1 | a |
+| 2 | b |
+| 3 | c |
+
+orders 表:
+
+| order_id | cust_id |
+| :---: | :---: |
+|1 | 1 |
+|2 | 1 |
+|3 | 3 |
+|4 | 3 |
+
+结果:
+
+| cust_id | cust_name | order_id |
+| :---: | :---: | :---: |
+| 1 | a | 1 |
+| 1 | a | 2 |
+| 3 | c | 3 |
+| 3 | c | 4 |
+| 2 | b | Null |
+
+# 十六、组合查询
+
+使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。
+
+每个查询必须包含相同的列、表达式和聚集函数。
+
+默认会去除相同行,如果需要保留相同行,使用 UNION ALL。
+
+只能包含一个 ORDER BY 子句,并且必须位于语句的最后。
+
+```sql
+SELECT col
+FROM mytable
+WHERE col = 1
+UNION
+SELECT col
+FROM mytable
+WHERE col =2;
+```
+
+# 十七、视图
+
+视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。
+
+对视图的操作和对普通表的操作一样。
+
+视图具有如下好处:
+
+- 简化复杂的 SQL 操作,比如复杂的连接;
+- 只使用实际表的一部分数据;
+- 通过只给用户访问视图的权限,保证数据的安全性;
+- 更改数据格式和表示。
+
+```sql
+CREATE VIEW myview AS
+SELECT Concat(col1, col2) AS concat_col, col3*col4 AS compute_col
+FROM mytable
+WHERE col5 = val;
+```
+
+# 十八、存储过程
+
+存储过程可以看成是对一系列 SQL 操作的批处理。
+
+使用存储过程的好处:
+
+- 代码封装,保证了一定的安全性;
+- 代码复用;
+- 由于是预先编译,因此具有很高的性能。
+
+命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。
+
+包含 in、out 和 inout 三种参数。
+
+给变量赋值都需要用 select into 语句。
+
+每次只能给一个变量赋值,不支持集合的操作。
+
+```sql
+delimiter //
+
+create procedure myprocedure( out ret int )
+ begin
+ declare y int;
+ select sum(col1)
+ from mytable
+ into y;
+ select y*y into ret;
+ end //
+
+delimiter ;
+```
+
+```sql
+call myprocedure(@ret);
+select @ret;
+```
+
+# 十九、游标
+
+在存储过程中使用游标可以对一个结果集进行移动遍历。
+
+游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。
+
+使用游标的四个步骤:
+
+1. 声明游标,这个过程没有实际检索出数据;
+2. 打开游标;
+3. 取出数据;
+4. 关闭游标;
+
+```sql
+delimiter //
+create procedure myprocedure(out ret int)
+ begin
+ declare done boolean default 0;
+
+ declare mycursor cursor for
+ select col1 from mytable;
+ # 定义了一个 continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1
+ declare continue handler for sqlstate '02000' set done = 1;
+
+ open mycursor;
+
+ repeat
+ fetch mycursor into ret;
+ select ret;
+ until done end repeat;
+
+ close mycursor;
+ end //
+ delimiter ;
+```
+
+# 二十、触发器
+
+触发器会在某个表执行以下语句时而自动执行:DELETE、INSERT、UPDATE。
+
+触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化,AFTER 用于审计跟踪,将修改记录到另外一张表中。
+
+INSERT 触发器包含一个名为 NEW 的虚拟表。
+
+```sql
+CREATE TRIGGER mytrigger AFTER INSERT ON mytable
+FOR EACH ROW SELECT NEW.col into @result;
+
+SELECT @result; -- 获取结果
+```
+
+DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。
+
+UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改的,而 OLD 是只读的。
+
+MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。
+
+# 二十一、事务管理
+
+基本术语:
+
+- 事务(transaction)指一组 SQL 语句;
+- 回退(rollback)指撤销指定 SQL 语句的过程;
+- 提交(commit)指将未存储的 SQL 语句结果写入数据库表;
+- 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。
+
+不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。
+
+MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
+
+设置 autocommit 为 0 可以取消自动提交;autocommit 标记是针对每个连接而不是针对服务器的。
+
+如果没有设置保留点,ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。
+
+```sql
+START TRANSACTION
+// ...
+SAVEPOINT delete1
+// ...
+ROLLBACK TO delete1
+// ...
+COMMIT
+```
+
+# 二十二、字符集
+
+基本术语:
+
+- 字符集为字母和符号的集合;
+- 编码为某个字符集成员的内部表示;
+- 校对字符指定如何比较,主要用于排序和分组。
+
+除了给表指定字符集和校对外,也可以给列指定:
+
+```sql
+CREATE TABLE mytable
+(col VARCHAR(10) CHARACTER SET latin COLLATE latin1_general_ci )
+DEFAULT CHARACTER SET hebrew COLLATE hebrew_general_ci;
+```
+
+可以在排序、分组时指定校对:
+
+```sql
+SELECT *
+FROM mytable
+ORDER BY col COLLATE latin1_general_ci;
+```
+
+# 二十三、权限管理
+
+MySQL 的账户信息保存在 mysql 这个数据库中。
+
+```sql
+USE mysql;
+SELECT user FROM user;
+```
+
+**创建账户**
+
+新创建的账户没有任何权限。
+
+```sql
+CREATE USER myuser IDENTIFIED BY 'mypassword';
+```
+
+**修改账户名**
+
+```sql
+RENAME USER myuser TO newuser;
+```
+
+**删除账户**
+
+```sql
+DROP USER myuser;
+```
+
+**查看权限**
+
+```sql
+SHOW GRANTS FOR myuser;
+```
+
+**授予权限**
+
+账户用 username@host 的形式定义,username@% 使用的是默认主机名。
+
+```sql
+GRANT SELECT, INSERT ON mydatabase.* TO myuser;
+```
+
+**删除权限**
+
+GRANT 和 REVOKE 可在几个层次上控制访问权限:
+
+- 整个服务器,使用 GRANT ALL 和 REVOKE ALL;
+- 整个数据库,使用 ON database.\*;
+- 特定的表,使用 ON database.table;
+- 特定的列;
+- 特定的存储过程。
+
+```sql
+REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
+```
+
+**更改密码**
+
+必须使用 Password() 函数进行加密。
+
+```sql
+SET PASSWROD FOR myuser = Password('new_password');
+```
+
+# 参考资料
+
+- BenForta. SQL 必知必会 [M]. 人民邮电出版社, 2013.
+
+
+
+
+
+
+