---
# System prepended metadata

title: SQL 筆記

---

# SQL 筆記

此筆記是基於 Mosh Hamedani 推出的的 SQL 課程 [Complete SQL Mastery](https://codewithmosh.com/p/complete-sql-mastery) 所撰寫。

# Getting started

## 什麼是 SQL？

SQL（Structured Query Language，結構化查詢語言）是專門用來處理（包括查詢和修改）關係型數據庫的標準語言。

## Database 數據庫或稱資料庫

- A DATABASE is a collection of data stored in a format that can easily be accessed
數據庫是一個以易訪問格式存儲的數據集合
- 為了管理我們的數據庫我們使用一種叫做數據庫管理系統（DBMS, Database Management System）的軟體。我們連接到一個DBMS 然後下達查詢或者修改數據的指令，DBMS 就會執行我們的指令並返回結果
- 有關係型和非關係型兩類數據庫，在更流行的關係型數據庫中，我們把數據存儲在**通過某些關係相互關聯的數據表中，每張表儲存特定的一類數據**，這正是關係型數據庫名稱的由來。（如：顧客表通過顧客id與訂單表相聯繫，訂單表又通過商品id與商品表相聯繫）
- 不同關係型數據庫管理系統語法略有不同，但都是基於標準SQL，本課使用最流行的開源關係型數據庫管理系統，MySQL。

# Retrieving Data From a Single Table

## The SELECT Statement

```sql
SELECT 
	name,
	unit_price,
	unit_price * 1.1 AS new_price -- 或 'new price'
FROM products
```

## The WHERE Clause

```sql
SELECT *
FROM customers
-- WHERE points > 3000  
WHERE state != 'va'  -- 'VA' 和 'va' 一樣結果
```

1. SQL 運算子：`>、<、=、>=、<=、!=、<>`

2. 可對日期格式或字串進行運算，注意 SQL 日期格式需要用引號包起來：
`WHERE birth_date > '1990-01-01'`



## The AND, OR and NOT Operators

```sql
SELECT *
FROM customers
WHERE birth_date > '1990-01-01' OR 
      (points > 1000 AND state = 'VA')  -- AND 優先於 OR，建議加括號，更清楚一些。
```

### NOT 用法

```sql
WHERE NOT (birth_date > '1990-01-01' OR points > 1000)
等於下面
WHERE birth_date <= '1990-01-01' AND points <= 1000
```

## The IN Operator

IN 可以找出所有符合的值

```sql
-- 選出符合 'va'、'fl'、'ga' 州的顧客
SELECT * FROM customers
WHERE state = 'va' OR state = 'fl' OR state = 'ga'
-- 可以改寫為下面醬：
WHERE state IN ('va', 'fl', 'ga')
-- 也可在加 NOT
WHERE state NOT IN ('va', 'fl', 'ga')
```

## The BETWEEN Operator

必須和 AND 一起用！

```sql
SELECT * FROM customers
WHERE points >= 1000 and points <= 3000
-- 等於下面醬
WHERE points BETWEEN 1000 AND 3000
```

## The LIKE Operator (已過時)

- % 代表任意數量的字元（包含 0 個）
- _ 代表一個字元

```sql
SELECT * FROM customers
WHERE last_name like 'brush%' OR
			last_name like 'b____y'
```

## The REGEXP Operator 正規表達式

```sql
SELECT * FROM customers
WHERE last_name like '%field%'
-- 等於下面
WHERE last_name REGEXP 'field'
```

```sql
WHERE last_name REGEXP '^mac|field$|rose' 
WHERE last_name REGEXP '[gi]e|e[fmq]' -- 查找含 ge/ie 或 ef/em/eq 的
WHERE last_name REGEXP '[a-h]e'
```

- ^ 開頭
- $ 結尾
- [abc] 包含 a 或 b 或 c
- [a-c] 包含 a 到 c
- |  或 logical or

## The IS NULL Operator

```sql
SELECT * FROM customers
WHERE phone IS NULL / IS NOT NULL
```

## The ORDER BY Clause

```sql
SELECT *
FROM customers
ORDER BY state, first_name 
-- 先依照 state 排序，若有兩個 state 相同再依照 first_name 排序
```

- Order By 後面可以接運算式跟沒有被 SELECT 到的欄位。

## The LIMIT Clause

```sql
SELECT * FROM customers
LIMIT 3 -- 只選三個
LIMIT 6, 3 -- 跳過六個之後選三個
```

# Retrieving Data From Multiple Tables

## Inner Joins

```sql
SELECT *
FROM orders
JOIN customers -- inner join 的 inner 可省略不寫
ON orders.customer_id = customers.customer_id
```

建議第一次可選 `SELECT *` 選取所有欄位後再選取想要的欄位名稱⬇️

```sql
SELECT order_id, first_name, last_name
FROM orders
JOIN customers
ON orders.customer_id = customers.customer_id
```

⚠️ 注意若我也想 `SELECT customer_id` 欄位：

```sql
-- 這一段會報錯，因為 SQL 不知道選 orders 還是 customers 的 customer_id
-- 雖然它們是一樣的
SELECT order_id, customer_id, first_name, last_name
FROM orders
JOIN customers
ON orders.customer_id = customers.customer_id

-- 你應在 customer_id 前加上資料表名稱（兩個都可以）：
SELECT order_id, orders.customer_id, first_name, last_name
FROM orders
JOIN customers
ON orders.customer_id = customers.customer_id
```

替資料表寫別名（語法糖）

```sql
-- 這邊取別名不用再加 AS
SELECT order_id, o.customer_id, first_name, last_name
FROM orders o
JOIN customers c
ON o.customer_id = c.customer_id
```

## Joining Across Databases (跨資料庫連接)

若要 JOIN 的資料表在另一個資料庫，就在該資料表前加上該資料庫前綴：

```sql
-- 這邊 sql_inventory 是另一個資料庫
select * from order_items oi
join sql_inventory.products p
    on oi.product_id = p.product_id
```

## Self Joins

利用 `inner join` 一個表與自己合併。常用於員工及員工主管或商品及商品分類的 table。

⚠️ 要注意的是因為是自己 join 自己，所以資料表必須要取別名：

```sql
SELECT * 
FROM employees e
JOIN employees m
ON e.reports_to = m.employee_id
```

![](https://i.imgur.com/LCfTphN.png)


讓 reports_to 欄位的值去關聯 employee_id，因為 reports_to 的值全部都是 37270 所以每一筆 SELECT 出來的資料都是 Yovonnda。需要注意的是第二筆資料的值是 NULL，所以 SELECT 出來時第二筆資料就不會顯示！

![](https://i.imgur.com/ltltcmB.png)

SELECT 出來的結果。每個員工的主管都是 Yovonnda，而 Yovonnda 本身的 reports_to 欄位值是 NULL （他沒有主管），在結果中就不會顯示他的資料。

承上，我們只 `SELECT` employee_id 跟 first_name 就好：

```sql
-- 這邊 first_name 出現兩次，可以將 m.first_name 另取別名為 manager 語意比較清楚
-- AS 在這邊是可以省略的
SELECT e.employee_id, e.first_name, m.first_name AS manager
FROM employees e
JOIN employees m
ON e.reports_to = m.employee_id
```

![](https://i.imgur.com/or37esn.png)

## Joining Multiple Tables

FROM 一個主要表 ，用多個 JOIN... ON... 去關聯多個不同資料表。

```sql
SELECT 
	o.order_id, 
    o.order_date,
    c.first_name,
    c.last_name,
    os.name AS status
FROM orders o
JOIN customers c
	ON o.customer_id = c.customer_id
JOIN order_statuses os
	ON o.status = os.order_status_id
```

## Compound Join Conditions 複合 join 條件

💡需使用 `AND` 連結：

```sql
SELECT * 
FROM 表A
JOIN 表B
    ON 關聯條件1
    AND 關聯條件2
```

像 order_items 這個資料表的 order_id 的值是有重複的，必須連 product_id 也一起看才能辨識唯一一筆紀錄：

![](https://i.imgur.com/PPvnXD2.png)


打開設計模式選像可以看到 order_id 和 product_id 都是 **Primary Key** ⬇️

![](https://i.imgur.com/UUf4qdH.png)


## Implicit Join Syntax 隱式關聯語法

就是將 FROM... JOIN... 寫法改成用 FROM 表1, 表2 WHERE... 🔽

```sql
SELECT * 
FROM orders o
JOIN customers c
    ON o.customer_id = c.customer_id

-- 下面是隱式關聯語法
SELECT * 
FROM orders o, customers c  
WHERE o.customer_id = c.customer_id
```

⚠️ 注意！**請盡量避免使用隱式關聯語法**，若忘記加上 `WHERE` 判斷會得到 cross join 的結果，若有 10 筆 order 和 10 筆 customer 就會撈出 100 筆資料。

## Outer Joins

- (INNER) JOIN 只會有兩個表交集的結果。
- LEFT / RIGHT (OUTER) JOIN 則會根據關聯條件顯示全部筆數的資料，沒有交集的話值就是 NULL。

這是 INNER JOIN 的用法，我們關聯 `c.customer_id = o.customer_id` 可以撈出有下訂單的顧客。

```sql
SELECT 
    c.customer_id,
    c.first_name,
    o.order_id
FROM customers c
JOIN orders o
    ON c.customer_id = o.customer_id
ORDER BY customer_id
```

![](https://i.imgur.com/7LqxNbG.png)


但若我們想撈出所有顧客時就須使用 OUTER JOIN 的方式（將所有 c.customer_id 都撈出來儘管有些人沒下訂單 order_id 為 NULL）

```sql
SELECT 
    c.customer_id,
    c.first_name,
    o.order_id
FROM customers c
LEFT JOIN orders o
    ON c.customer_id = o.customer_id
ORDER BY customer_id
```

![](https://i.imgur.com/NLCJAiS.png)

## Outer Join Between Multiple Tables 多個 outer join

```sql
SELECT 
    c.customer_id,
    c.first_name,
    o.order_id,
    sh.name AS shipper
FROM customers c
LEFT JOIN orders o
    ON c.customer_id = o.customer_id
LEFT JOIN shippers sh
    ON o.shipper_id = sh.shipper_id
ORDER BY customer_id
```

⚠️ 請一律使用 INNER JOIN 跟 LEDT JOIN 就好，不然很容易出錯看到眼花。

## The USING Clause

當使用 JOIN 時的關聯條件的欄位名稱相同可使用 USING：

```sql
SELECT
	o.order.id,
	c.first_name
FROM orders o
JOIN customers c
	-- ON o.customer_id = c.customer_id
	USING (customer_id)
```

複合關聯條件：

```sql
SELECT *
FROM order_items oi
JOIN order_item_notes oin

ON oi.order_id = oin.order_Id AND
   oi.product_id = oin.product_id
-- USING (order_id, product_id)
```

## Natural Joins

此語法會自動檢視同名的欄位作為關聯條件，基本上不要用這個，因為沒辦法掌握結果，知道有這個語法就好。

```sql
SELECT 
    o.order_id,
    c.first_name
FROM orders o
NATURAL JOIN customers c
```

## Cross Joins

交集兩個資料表所有欄位的組合。

```sql
SELECT 
    c.first_name AS customer,
    p.name AS product
FROM customers c
CROSS JOIN products p
ORDER BY c.first_name
```

隱式語法如下：

```sql
SELECT 
    c.first_name,
    p.name
FROM customers c, products p
ORDER BY c.first_name
```

盡量別用隱式的用法吧，語意比較不清楚。

## Unions

將不同次查詢資料上下串聯起來成一個結果。

- 不同次 SELECT 的欄位數量必須相同，否則會報錯。
- 執行結果的欄位名稱由第一個下 SELECT 語法的欄位名稱優先。

```sql
SELECT 
	order_id,
	order_date,
	'Active' AS status
FROM orders
WHERE order_date >= '2019-01-01'
UNION
SELECT 
	order_id,
	order_date,
	'Archived' AS status 
FROM orders
WHERE order_date < '2019-01-01';
```

![](https://i.imgur.com/UAJJzlH.png)


範例二：

```sql
SELECT 
	customer_id,
	first_name,
	points,
	'Bronze' AS type
FROM customers 
WHERE points < 2000
UNION
SELECT 
	customer_id,
	first_name,
	points,
	'Silver' AS type
FROM customers 
WHERE points BETWEEN 2000 and 3000
UNION
SELECT 
	customer_id,
	first_name,
	points,
	'Gold' AS type
FROM customers 
WHERE points > 3000

ORDER BY customer_id
```

![](https://i.imgur.com/Rhx5YVp.png)


# Inserting, Updating, and Deleting Data

varchar(50) VS char(50)

將一欄位屬性設為 varchar(50) 若只有 5可不照順序）

## Inserting a Row 插入一列

```sql
INSERT INTO 資料表 (欄位名稱） -- 欄位名稱是選填選項，盡量寫比較好！
VALUES (欄位值)
```

範例一（不填欄位名稱）

```sql
-- 若省略欄位名稱，值就必須照順序填，欄位屬性若是像 ID 有 AI 屬性，就要填 DEFAULT 值給它
INSERT INTO customers 
VALUES (
    DEFAULT,
    'Michael',
    'Jackson',
    '1958-08-29',  
    DEFAULT,
    '5225 Figueroa Mountain Rd', 
    'Los Olivos',
    'CA',
    DEFAULT
    );
```

範例二（有填欄位名稱，這種寫法較優，可跳過有預設值的欄位或可不照順序）

```sql
INSERT INTO customers (
    address,
    city,
    state,
    last_name,
    first_name,
    birth_date,
    )
VALUES (
    '5225 Figueroa Mountain Rd',
    'Los Olivos',
    'CA',
    'Jackson',
    'Michael',    
    '1958-08-29',  
    )
```

## Inserting Multiple Rows 插入多列

就是 VALUES 後用多個括號給值：

```sql
INSERT INTO products (name, quantity_in_stock, unit_price)
VALUES ('product1', 1, 10),
       ('product2', 2, 20),
       ('product3', 3, 30)
```

## Inserting Hierarchical Rows 插入層級列

本小節主要重點是可以利用 `LAST_INSERT_ID()` 來獲得上一筆插入資料的 ID，可以藉此將此 ID 再插入到另一張資料表裡面，通常是用在有父層子層關係的資料表中，如範例是一個 order 訂單中的一筆資料會有多個 order_item 項目（同一筆 order 可以有很多個 order_item，它們用 order_id 關聯）。

```sql
-- 先在 orders 插入一筆訂單資料
INSERT INTO orders (customer_id, order_date, status)
VALUES (1, '2019-01-01', 1);

-- 可以用 SELECT last_insert_id() 確認一下目前的序數

-- 然後這筆單的關聯條件就是last_insert_id()，包含什麼產品、數量跟單價一起寫入資料庫
INSERT INTO order_items 
VALUES 
    (last_insert_id(), 1, 2, 2.5),
    (last_insert_id(), 2, 5, 1.5)
```

<aside>
💡 **注意！`LAST_INSERT_ID()` 只能用在有設置 Auto Incrememt 屬性的欄位上！！**

</aside>

## Creating a Copy of a Table 拷貝一個資料表

### 方法一、`CREATE TABLE 資料表名稱 AS 子查詢語法`

```sql
-- 複製 orders 資料表到 orders_archived
CREATE TABLE orders_archived AS
    SELECT * FROM orders 
```

只拷貝部分欄位

```sql
CREATE TABLE orders_archived AS
    SELECT * FROM orders
    WHERE order_date < '2019-01-01'
```

### 方法二、`INSERT INTO 資料表名稱 AS 子查詢語法`

```sql
INSERT INTO orders_archived -- 這邊不用再輸入 (欄位名稱)，會直接使用子查詢後的欄位名
    SELECT * FROM orders  -- 這邊的子查詢對應的是 VALUES (...),(...),(...)
    WHERE order_date < '2019-01-01'
```

## Updating rows 更新欄位值

語法

```sql

UPDATE 資料表名稱 
SET 要更新的欄位名稱 = 值 / DEFAULT / NULL / 表達式
WHERE 條件

-- 要更新多筆資料就是增加 WHERE 的條件而已
```

範例

```sql
UPDATE invoices
SET 
    payment_total =  0.5 * invoice_total, 
    payment_date = NULL
WHERE invoice_id IN (3, 4)
```

## Using Subqueries in Updates 在 Update 中使用子查詢

```sql
UPDATE invoices
SET payment_total = 567, payment_date = due_date

WHERE client_id = 
		-- 子查詢用括號包起來確保可先執行
    (SELECT client_id 
    FROM clients
    WHERE name = 'Yadel');

-- 若子查詢返回多筆資料就要用 IN 而不是 =
WHERE client_id IN 
    (SELECT client_id 
    FROM clients
    WHERE state IN ('CA', 'NY'))
```

# Summarizing Data

## Aggregate Functions 聚合函數

輸入一系列的值並得到一個結果。

```sql
SELECT 
    MAX(invoice_total) AS highest,  
    MIN(invoice_total) AS lowest,
    AVG(invoice_total) AS average,
    SUM(invoice_total * 1.1) AS total, -- 聚合函數裡面也可放表達式
		COUNT(invoice_total) AS number_of_invoices, 
    COUNT(*) AS total_records, -- 聚合函數會忽略空值，想計算所有欄位可用 * 號
    COUNT(payment_date) AS number_of_payments, 
    COUNT(DISTINCT client_id) AS number_of_distinct_clients 
    -- DISTINCT client_id 篩選該欄位重複值，再用 COUNT 去計算數量
FROM invoices
WHERE invoice_date > '2019-07-01'  -- 用 WHERE 去限縮結果，只看下半年的
```

- 聚合函數會忽略空值
- 聚合函數可放表達式

Exercise

```sql
SELECT 
    '1st_half_of_2019' AS date_range,
    SUM(invoice_total) AS total_sales,
    SUM(payment_total) AS total_payments,
    SUM(invoice_total - payment_total) AS what_we_expect
FROM invoices
WHERE invoice_date BETWEEN '2019-01-01' AND '2019-06-30'

UNION
SELECT 
    '2st_half_of_2019' AS date_range,
    SUM(invoice_total) AS total_sales,
    SUM(payment_total) AS total_payments,
    SUM(invoice_total - payment_total) AS what_we_expect
FROM invoices
WHERE invoice_date BETWEEN '2019-07-01' AND '2019-12-31'

UNION
SELECT 
	  'Total' AS date_range,
	  SUM(invoice_total) AS total_sales,
	  SUM(payment_total) AS total_payments,
	  SUM(invoice_total - payment_total) AS what_we_expect
FROM invoices
WHERE invoice_date BETWEEN '2019-01-01' AND '2019-12-31'
```

![](https://i.imgur.com/i7CACbP.png)



💡 `SUM(invoice_total - payment_total)` 等於 `SUM(invoice_total) - SUM(payment_total)`


## The GROUP BY Clause

SELECT 出所有銷售

```sql
SELECT 
	SUM(invoice_total) AS total_sales
FROM invoices
```

![](https://i.imgur.com/1C5rDah.png)


那若想知道每個顧客的總銷售呢？用 `GROUP BY` 對列進行分組：

```sql
SELECT 
	client_id,
	SUM(invoice_total) AS total_sales
FROM invoices
GROUP BY client_id
```

![](https://i.imgur.com/FPUs4vH.png)


<aside>
💡 默認的順序是依照 `GROUP BY` 進行分組的欄位去排序的。可以再用 `ORDER BY` 去排序 🔽

</aside>

```sql
SELECT 
	client_id,
	SUM(invoice_total) AS total_sales
FROM invoices
-- WHERE invoice_date >= '2019-07-01'
-- 也可以再加篩選語句
GROUP BY client_id
ORDER BY total_sales DESC
```

![](https://i.imgur.com/M8ngJ0K.png)


<aside>
💡 注意！`GROUP BY` 的位置要放在 `FROM`、`WHERE` 之後 `ORDER BY` 之前！

</aside>

## The HAVING Clause

`HAVING` 和 `WHERE` 都是篩選語法，寫法也都一樣，它們的不同同在於：

1. `WHERE` 只能篩選資料本中本來有的欄位，如使用聚合函數過後的欄位就無法用。
2. `HAVING` 後面只能接有被 SELECT 的欄位。

```sql
SELECT 
    client_id,
    SUM(invoice_total) AS total_sales,
    COUNT(*) AS number_of_invoices
FROM invoices
GROUP BY client_id
HAVING total_sales > 500 AND number_of_invoices > 5
```

這邊如果將 `HAVING` 替換成 `WHERE` 會報錯，因為原資料表中沒有 total_sales 這欄。

## WITH ROLLUP 運算子 (MySQL 才有的語法)

若 `SELECT` 的欄位有運用到聚合函數，加上 `WITH ROLLUP` 之後的結果會新增一列計算該欄位的總和。

```sql
SELECT 
    client_id,
    SUM(invoice_total)
FROM invoices
GROUP BY client_id WITH ROLLUP
```

![](https://i.imgur.com/vhUTccb.png)


# Writing Complex Query

### Subqueries 子查詢

簡單來說就是 `SELECT` 語句中在寫 `SELECT`：

```sql
SELECT *
FROM products
WHERE unit_price > (   -- 記得用括號包起來
    SELECT unit_price
    FROM products
    WHERE product_id = 3
)
```

```sql
-- 查詢薪水大於平均的人
SELECT *
FROM employees
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
)
```

## The ALL Keyword

`> ALL(SELECT...)` 與 `> (SELECT MAX(column)` 同校，目的都是要找出最大值。

範例：sql_invoicing 資料庫中要選出大於 `id = 3` 的顧客的最大發票金額的所有發票

1.  MAX() 寫法

```sql
SELECT *
FROM invoices
WHERE invoice_total > (
    SELECT MAX(invoice_total)
    FROM invoices
    WHERE client_id = 3
)
```

1. ALL() 寫法

```sql
SELECT *
FROM invoices
WHERE invoice_total > ALL (
    SELECT invoice_total
    FROM invoices
    WHERE client_id = 3
)
```

`MAX()` 方法會返回一個顧客 `id = 3` 的最大的發票金額，再判斷哪些發票的金額比這個值大；`ALL()` 方法是先返回顧客 `id = 3`的所有發票金額，是一列值，再用 `ALL()` 判斷比這些金額都大的發票有哪些。

## The ANY Keyword

`> ANY/SOME (SELECT...)` 與 `> (SELECT MIN(column))` 同效

 `= ANY/SOME (SELECT...)` 與 `IN (…)` 同效

範例一：sql_invoicing 資料庫中要選出大於 `id = 3` 的顧客的最小發票金額的所有發票

1. ANY() 寫法

```sql
SELECT *
FROM invoices

WHERE invoice_total > ANY (
    SELECT invoice_total
    FROM invoices
    WHERE client_id = 3
)
```

1.  MIN() 寫法

```sql
WHERE invoice_total > (
    SELECT MIN(invoice_total)
    FROM invoices
    WHERE client_id = 3
)
```

範例二：選出至少有兩張發票的顧客

1.  ANY() 寫法

```sql
SELECT *
FROM clients
WHERE client_id = ANY ( 
    SELECT client_id
    FROM invoices
    GROUP BY client_id
    HAVING COUNT(*) >= 2
)
```

1. IN 寫法

```sql
SELECT *
FROM clients
WHERE client_id IN (
    SELECT client_id
    FROM invoices
    GROUP BY client_id
    HAVING COUNT(*) >= 2
)
```

## Correlated Subqueries 相關子查詢

選出 sql_hr.employees 裡那些薪水超過他所隸屬的辦公室的平均薪水的員工：

```sql
SELECT *
FROM employees e
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
    WHERE office_id = e.office_id  -- 記得取別名區分
)
```

因為 WHEHR 會去逐列比對去做篩選，所以可以將 e.office_id 想像成從 1 開始一直遞增，類似迴圈的概念，（其中一張表必須要取別名去區分）。

## The EXISTS Operator

`EXISTS (相關子查詢)` 等於 `IN (SELECT...子查詢)` 但 EXISTS 效率較佳。  

1.  IN 寫法

```sql
SELECT *
FROM clients
WHERE client_id IN (
    SELECT DISTINCT client_id
    FROM invoices
)
```

2. EXISTS

```sql
SELECT *
FROM clients c
WHERE EXISTS (
		SELECT client_id
		FROM invoices
		WHERE clients_id = c.client_id
)
```
待續...