跳至主要内容

[SQL] Query 常用指令

:thumbsup: PostgreSQL Tutorial

FAQ

單引號和雙引號有什麼不同?

What is the difference between single quotes and double quotes in PostgreSQL? @ StackOverflow

在 Postgres 中,單引號用來表示「字串」,而雙引號是針對欄位名稱或資料表名稱使用(一般來說可以省略):

select * from "employee" where "employee_name"='aaron';

SELECT

SELECT column_1, column_2, ... FROM table_name;
SELECT * FROM table_name;
  • SQL 語法本身是不區分打小寫的(case-insensitive),但慣例上來說,會把 SQL 內的關鍵字用大寫呈現,以方便閱讀。
  • 使用 * 來選出所有欄位的資料一般來說不是太好的作法,這會拖慢資料庫以及應用程式的效能。因為你可能會拿出許多沒有必要的欄位資料,同時導致伺服器的負荷增加,拖慢應用程式的效能。因此,最好只在特殊的情況使用(例如,直接操作資料庫時)。

範例

SELECT first_name FROM customer;

Querying Data

ORDER BY

PostgreSQL ORDER BY

keywords: ASC, DESC, NULLS FIRST, NULLS LAST
SELECT column_1, column_2, ...
FROM table_name
ORDER BY column_1 ASC[, column_2 DESC];
  • ORDER BY 預設是使用 ASC 排序。
  • 如果根據兩個以上的欄位排序,則會以前者為優先,例如先依照 column_1 排序,當 column_1 的值一樣的話,在根據 column_2 排序。
  • PSQL 中,即使要排序的欄位沒有透過 SELECT 選起來,也可以指定它來作為排序的依據,但在 MYSQLOracle SQL 中可能不行這麼做。

範例

-- 排序多個欄位
SELECT first_name, last_name FROM customer ORDER BY first_name ASC, last_name DESC;

-- 將 expression 作為排序依據
SELECT first_name, LENGTH(first_name) len
FROM customer
ORDER BY len DESC;

-- 使用 NULLS FIRST 或 NULLS LAST 來搭配 NULL 進行排序
SELECT num FROM sort_demo ORDER BY num DESC NULLS FIRST;

AS

keywords: column alias

PostgreSQL Column Alias

透過 AS 可以將欄位改一個匿名(alias):

SELECT amount as amount_alias FROM payment;

-- 在 SELECT 中使用 AS
SELECT customer_id, SUM(amount) AS total_amount FROM payment
GROUP BY customer_id;

-- 搭配 concatenation operator
SELECT first_name || ' ' || last_name AS full_name FROM customer;

-- 搭配 JOIN 使用
SELECT title, name
FROM film
INNER JOIN language AS lan ON film.language_id = lan.language_id;

在 SQL 中甚至可以把 AS 這個關鍵字省略,用 空格 即可

-- 不寫出 AS 而是用「空格」
SELECT customer_id, SUM(amount) total_amount FROM payment
GROUP BY customer_id;

--
SELECT title, name
FROM film
INNER JOIN language lan ON film.language_id = lan.language_id;

DISTINCT

-- 列出 column 中不重複的值
SELECT DISTINCT column FROM table_name;

-- 列出 column_1 且 column_2 不重複的值(會結合 column_1 和 column_2)
SELECT DISTINCT column_1, column_2, ... FROM table_name;

--
SELECT DISTINCT ON (column1) column_alias FROM table_name ORDER BY column1, column2;
  • 在一個 table 中常常會有許多重複的值,透過 DISTINCT 可以只列出不重複的值。
  • DISTINCT ON 的使用最好都能搭配 ORDER BY
/* DISTINCT 也可以搭配 COUNT 使用 */
SELECT COUNT(DISTINCT column_1) FROM table_name ;

Filtering Data

WHERE

SELECT column_1, column_2, ...
FROM table_name
WHERE conditions;

Operators

  • >, <, =, >=, <=, <>(not equal), !=(not equal), AND, OR
/* example: =, AND*/
SELECT last_name, first_name
FROM customer
WHERE first_name = 'Jamie' AND last_name = 'Rice';
/* example: <=, >=, OR */
SELECT customer_id, amount, payment_date
FROM payment
WHERE amount <= 1 OR amount >= 8;

時間也可以:

SELECT * FROM vital_signs WHERE measured_at > '2020-08-17T02:42:45.702Z';

BETWEEN, NOT BETWEEN

/* 等同於 WHERE value >= low AND value <= high */
SELECT * FROM table_name
WHERE column_1 BETWEEN low_value AND hight_value;
/* 等同於 WHERE value < low OR value > high */
SELECT * FROM table_name
WHERE column_1 NOT BETWEEN low_value AND high_value;
  • BETWEEN 等同於 WHERE value >= low AND value <= high
  • NOT BETWEEN 等同於 WHERE value < low OR value > high

也可以用來篩選時間

SELECT * FROM payment
WHERE payment_date
BETWEEN '2007-02-07' AND '2007-02-15';

IN, NOT IN:作為多個 OR 使用

使用 IN 可以取代使用多個 OR 條件,效能又較好:

SELECT * FROM table_name
WHERE column_1 IN (value_1, value_2, ...);

/* 上面的寫法等同於 */
SELECT * FROM table_name
WHERE column_1 = value_1
OR column_1 = value_2
OR ...;

IN 裡面除了可以放一般的字串和數值外,也可以是另一個 SELECT 後的結果:

SELECT * FROM table_name
WHERE column_1 IN (SELECT value FROM table_name);
/* 使用例子 Example */
SELECT * FROM rental
WHERE customer_id IN (1, 2);

LIKE, NOT LIKE, ILIKE, NOT ILIKE:模糊比對(Pattern Matching)

SELECT column_1, column_2
FROM table_name
WHERE column_1 LIKE '%foobar%';
  • LIKENOT LIKE 是有區分大小寫的(case sensitive),若希望不區分大小寫(case insensitive),可以使用 ILIKENOT ILIKE
  • 使用 % 可以配對任何次數的文字符號
    • 'Jen%':任何以 Jen 開頭的值(用 LIKE 需區分大小寫,ILIKE 則不用)
    • '%er%':任何值當中包含 er
  • 使用 _ 則只能配對單一次的文字符號
    • '_sther':任意文字加上 sther

記得要加上單引號 ''

/* example */
/* 任何以 Jen 開頭的 first_name */
SELECT first_name, last_name
FROM customer
WHERE first_name LIKE 'Jen%';

IS NULL

IS 後面可以接 null

SELECT * FROM order
WHERE deleted_at IS NULL;

LIMIT

SELECT column_1, column_2, ...
FROM table_name
LIMIT row_num_you_want;
/* example */
SELECT * FROM customer LIMIT 5;

GROUP BY

SELECT column_1, aggregate_function(column_2)
FROM table
GROUP BY column_1;
  • 透過 GROUP BY 可以將許多列(row)的資料進行分組。
  • 每一個分組過的資料,都可以在經過 aggregate function 計算所需要的數值。

範例:取出唯一值

SELECT payment_id FROM payment GROUP BY payment_id;

-- 等同於
SELECT DISTINCT payment_id FROM payment;
  • 針對同一個所選的欄位進行 GROUP BY 等同於使用 DISTINCT 的效果。
  • 有些 SQL 語言會要求你一定要放入 aggregate function 而不能只放入 GROUP BY 中所代入的欄位。

範例:搭配 aggregate function 計算數值

SUM:計算分組內某欄位的加總
-- 先根據 customer_id 分組,接著將組內的 amount 分數加總
SELECT customer_id, SUM(amount)
FROM payment
GROUP BY customer_id
ORDER BY SUM(amount) DESC;
  • 在其他 SQL 語言中,可能會需要把 GROUP BY 的欄位也一定要代到 SELECT 中才行(即,這裡的 customer_id),但在 psql 中較具彈性,因此可以不用這麼做。
  • 透過 GROUP BY 計算 SUM(amount) 的這個欄位,一樣可以放到 ORDER BY 中使用。
COUNT:計算分組內的資料數

此外也可以搭配 COUNT 使用:

-- 先根據 staff_id 分組,接著計算每組內的資料筆數(rows)
SELECT staff_id, COUNT(*)
FROM payment
GROUP BY staff_id;
-- 先根據 rating 分組,接著計算每組內的資料筆數(rows)
SELECT rating, COUNT(*)
FROM film
GROUP BY rating;
AVG:計算分組內某欄位的平均值
-- 先根據 rating 分組,接著計算每一組內 `rental_rate` 的平均
SELECT rating, AVG(rental_rate)
FROM film
GROUP BY rating;

HAVING

HAVING 搭配 GROUP BY 使用,針對已經透過 GROUP BY 分組後的欄位進行某些條件過濾:

SELECT column_1, aggregate_function(column_2)
FROM table_name
GROUP BY column_1
HAVING condition;

HAVINGWHERE 的作用非常相近,主要差別在於 HAVING 作用在 GROUP BY 之後的欄位資料,而 WHERE 是作用在 GROUP BY 之前的欄位資料。

範例

-- 先根據 customer_id 分組後,再透過分組後的 SUM(amount) 進行過濾
SELECT customer_id, SUM(amount)
FROM payment
GROUP BY customer_id
HAVING SUM(amount) > 200;
-- 先根據 store_id 分組後,再透過分組後的 COUNT(customer_id) 進行過濾
SELECT store_id, COUNT(customer_id)
FROM customer
GROUP BY store_id
HAVING COUNT(customer_id) > 300;

Subquery

提示

在閱讀含有 subquery 的 SQL,比較容易的閱讀方式是先看 subquery 會組出的內容是什麼,而不是從 SQL 頭開始看。原因是 Subquery 的內容會先被執行,接著才來執行 main query。

在 SELECT、FROM 或 JOIN 中使用

-- https://www.udemy.com/course/sql-advanced-queries/learn/lecture/47029249

-- 在 SELECT 中使用,可以用來「計算」
SELECT product_id,
product_name,
unit_price,
unit_price - (
SELECT(AVG(unit_price))
FROM products
) AS diff_price
FROM products
ORDER BY unit_price DESC;

-- 在 FROM 中使用,可以用來 JOIN 另一個 query,一定要搭配 AS
SELECT
hs.country,
hs.year,
hs.happiness_score,
country_hs.avg_hs_by_country
FROM
happiness_scores hs
INNER JOIN (
SELECT
country,
AVG(happiness_score) AS avg_hs_by_country
FROM
happiness_scores
GROUP BY
country
) AS country_hs ON hs.country = country_hs.country;

在 WHERE 或 HAVING 中使用

假設我們希望看所有的影片中,哪些影片的價格高於平均,我們可以寫兩次不同的 query:

-- 1. 使用 query 來找出所有電影的平均價格
SELECT AVG(amount) FROM film; -- 4.8

-- 2. 使用 query 找出所有價格大於平均價格的電影
SELECT * FROM film WHERE amount > 4.8;

但若我們使用 subquery 則可以一次完成,Subquery 的特徵是透過 () 包住另一個 query:

SELECT * FROM film
WHERE amount > (SELECT AVG(amount) FROM film);

下面是 Subquery 的一些說明:

  • 實際上在執行的時候Subquery 會比 main query 更早被執行
  • Subquery 可以在許多不同地方被使用,例如 WHEREJOINHAVING 等等都可以
  • 通常會把 Subquery 出來的 view 用 AS 取一個 alias
  • 可以搭配 ALLANYEXISTS 使用
提示

在 JOIN 後使用 Subquery 通常可以帶來更好的效能,因為你不是 JOIN 另外一整張 table,而是 SELECT 後較小的表。

-- 搭配 WHERE ... IN 使用
SELECT film_id, title
FROM film
WHERE film_id IN (
SELECT film_id FROM rental
INNER JOIN inventory ON inventory.inventory_id = rental.inventory_id
WHERE return_date BETWEEN '2005-05-29' AND '2005-05-30'
);
-- 在 HAVING 中使用
SELECT AVG(happiness_score) AS region_hs, region
FROM happiness_scores
GROUP BY region
HAVING
region_hs > (
SELECT
AVG(happiness_score) AS avg_hs
FROM
happiness_scores
);

搭配 ANY、ALL 或 EXISTS 使用

-- happiness_score 必須要大於所有 happiness_scores_current 中的 ladder_score
SELECT
*
FROM
happiness_scores
WHERE
happiness_score > ALL (
SELECT
ladder_score
FROM
happiness_scores_current
);

下面使用 EXISTS 的範例中:

  • 在 subquery 裡引用了外層的 table 的欄位,因此稱作 correlated subquery。correlated subquery 在某些 RDBMS 中的速度會比較慢,應該避免。
SELECT
*
FROM
happiness_scores h
WHERE
EXISTS (
SELECT
i.country_name
FROM
inflation_rates i
WHERE
i.country_name = h.country
);

如果使用 correlated subquery 發生了效能問題,可以使用 INNER JOIN 改寫:

SELECT
COUNT(*)
FROM
happiness_scores hs
INNER JOIN inflation_rates ir ON hs.country = ir.country_name
AND hs.year = ir.year;

CTE (Common Table Expression)

CTE 會以 WITH 開頭,在 AS 後面則會放 query:

-- https://www.udemy.com/course/sql-advanced-queries/learn/lecture/47029277

WITH
country_hs AS (
SELECT
country,
AVG(happiness_score) AS avg_hs_by_country
FROM
happiness_scores
GROUP BY
country
)
SELECT
hs.year,
hs.country,
hs.happiness_score,
country_hs.avg_hs_by_country
FROM
happiness_scores hs
LEFT JOIN country_hs ON hs.country = country_hs.country;

CTE 和 subquery 主要的差別或好處在於:

  • Readability:可讀性通常比較高
  • Reusability:定義後在同一個 Query 中可以重複被使用
  • Recursiveness:可以用來處理 recursive queries
提示

雖然 CTE 通常可讀性比較高,但有些時候用 Subquery 也能清楚表達的時候,不一定要使用 CTE。

Multiple CTEs

可以使用一個 WITH 和多個 xxx AS (...) 來建立多個 CTEs:

-- https://www.udemy.com/course/sql-advanced-queries/learn/lecture/47029277

WITH
hs23 AS (
SELECT
*
FROM
happiness_scores
WHERE
YEAR = 2023
),
hs24 AS (
SELECT
*
FROM
happiness_scores_current
)
SELECT
*
FROM
hs23
LEFT JOIN hs24 ON hs23.country = hs24.country;

Recursive CTE

recursive CTE 可以參照到自己,適合用在時間序列(sequence)或階層性的資料:

WITH RECURSIVE cte_name AS (
SELECT ... # anchor member(起始點)
UNION ALL # UNION or UNION ALL
SELECT ... FROM cte_name # recursize member that reference the CTE
)
SELECT * FROM cte_name;

實務上,recursive CTE 並不常見,可以有這個概念就好:

-- https://www.udemy.com/course/sql-advanced-queries/learn/lecture/47029293


-- 產生 2024-11-01 到 2024-11-06
WITH RECURSIVE
my_dates(dt) AS (
SELECT '2024-11-01' # 起始

UNION ALL # UNION or UNION ALL

SELECT # recursive 的條件
dt + INTERVAL 1 DAY
FROM
my_dates # 參照到自己
WHERE
dt < '2024-11-06'
)
SELECT * FROM my_dates;

Subqueries、CTEs、暫存表(Temporary tables)和 Views 間的比較

提示

通常,如果只是一次性的中間結果,可以用子查詢或 CTE。如果你在同一 session 中會重複使用中間結果,可以考慮暫存表。如果需要跨 session 長期使用,可以考慮檢視表。

Subquries、CTEs

  • 它們只會在該次查詢執行的期間存在。當查詢執行結束後,子查詢或 CTE 中的結果就不會再保留。

Temporary Table

Temporary Table 只會在一個 session(工作階段)中存在。只要你的 SQL 工具還是開啟的,暫存表就會存在。一旦關閉,暫存表就會消失。

-- 建立 temporary table
CREATE TEMPORARY TABLE my_temp_table AS
SELECT ...;

SELECT * FROM my_temp_table;

View Table

View Table 會永久存在,即使你關閉並重新開啟 SQL 工具,它依然會存在,直到你修改或刪除它。

實際上 View Table 本身不會儲存資料,而是只儲存 Query;當這個 View 被使用的時候,才會即時從來源資料表把資料撈出來

-- 建立 view table
CREATE VIEW TABLE my_view AS
SELECT ...;

SELECT * FROM my_view;

Timestamps and EXTRACT

透過 extract 可以截取 date 當中的部分:

SELECT
SUM(amount) AS total_amount,
extract(month from payment_date) AS month
FROM payment
GROUP BY month
ORDER BY total_amount DESC;

除了 extract,還有許多時間相關的方法可以使用,參考官方文件 9.9. Date/Time Functions and Operators @ PostgreSQL Documentation

Aggregate Function

keywords: COUNT, MIN, MAX, AVG, SUM, ROUND

各個 aggregate function 的使用方式大同小異。

COUNT

SELECT COUNT(*) FROM table_name;
SELECT COUNT(column_name) FROM table_name;
SELECT COUNT(DISTINCT column_name) FROM table_name;
  • 使用 COUNT(column_name) 時,若該欄位有值為 NULL 時,不會被 COUNT 語法算入。

AVG, SUM

SELECT AVG(column_name) FROM table_name;
SELECT SUM(column_name) FROM table_name;

MIN, MAX

SELECT MIN(column_name) FROM table_name;
SELECT MAX(column_name) FROM table_name;

ROUND

SELECT ROUND(AVG(column_name), decimal) FROM table_name;

/* 取的 AVG(amount) 後四捨五入到小數第二位 */
SELECT ROUND(AVG(amount), 2) FROM payment;

Mathematical Functions

-- 一般加減乘除
SELECT *, customer_id + rental_id AS new_id
FROM payment;

-- function
SELECT round(AVG(amount), 2 ) FROM payment;

--
SELECT 5 * 3;

Mathematical Functions and Operators @ PostgreSQL Documentation

String Functions

  • concatenation operator (||)
-- 字串連接使用 ||
SELECT *, first_name || ' ' || last_name AS full_name
FROM customer;

-- function
SELECT char_length(first_name), first_name
FROM customer;

SELECT upper(first_name), first_name
FROM customer;

SELECT lower(first_name), first_name
FROM customer;

String Functions and Operators @ PostgreSQL Documentation

UPDATE

UPDATE [table_name] SET [column1]=value, [column2]=value2,... WHERE [some_column]=some_value

UPDATE students SET cHeight=174, cWeight=92 WHERE cID = 11
DELETE
DELETE from [table_name] WHERE [some_column]=some_value

特殊字符

註解(Comment)

在 SQL 中加入註解的方式:

/* */     are inline comments
-- , # are line comments

SELECT * FROM users WHERE name = 'admin' --and pass = 'pass'

連接多行 query

透過 ; 可以連接多行 query:

-- ;        allows query chaining

SELECT * FROM users; DROP TABLE users;

連接字串(string concatenation)

-- ',+,||   allows string concatenation
-- Char() strings without quotes

SELECT * FROM users WHERE name = '+char(27) or 1=1

Multi-Tabele Analysis: JOIN & UNION

Basic Join Types

image-20250620235425570

最常用的是 INNER JOINLEFT JOINRIGHT JOINFULL OUTER JOIN 較少用,甚至在某些 RDBMS 不支援。

INNER JOIN

INNER JOIN 的話,被 JOIN 的欄位在兩張 table 都需要共同存在,所以不會有空值(NULL

SELECT hs.year,
hs.country,
hs.happiness_score,
cs.country,
cs.continent
FROM happiness_scores hs
INNER JOIN country_stats cs ON hs.country = cs.country;

LEFT JOIN

LEFT JOIN 的話,被 JOIN 的欄位在右邊那張表如果不存在的話,全部剩下的欄位都會用空值(NULL)取代

-- reference: https://www.udemy.com/course/sql-advanced-queries/learn/lecture/47028921

SELECT hs.year,
hs.country,
hs.happiness_score,
cs.country,
cs.continent
FROM happiness_scores hs
LEFT JOIN country_stats cs ON hs.country = cs.country
WHERE cs.country IS NULL;

Joining Multiple Tables

-- reference: https://www.udemy.com/course/sql-advanced-queries/learn/lecture/47028921

SELECT hs.year,
hs.country,
hs.happiness_score,
cs.continent,
ir.inflation_rate
FROM happiness_scores hs
-- 可以使用多個 Left Join 來 Join 多張 Tables
LEFT JOIN country_stats cs ON hs.country = cs.country
LEFT JOIN inflation_rates ir ON hs.year = ir.year
AND hs.country = ir.country_name;

Self JOIN

情境一:找出同一張表中,資料間的階層關係,例如主管-員工、類別-子類別、部門-上級部門等結構

-- reference: https://www.udemy.com/course/sql-advanced-queries/learn/lecture/47028921

SELECT e1.employee_id,
e1.employee_name,
e1.salary,
e1.manager_id,
e2.employee_id,
e2.employee_name,
e2.salary
FROM employees e1
LEFT JOIN employees e2 ON e1.manager_id = e2.employee_id;

情境二:找出同一張表中,資料間的比較分析

-- reference: https://www.udemy.com/course/sql-advanced-queries/learn/lecture/47028921

SELECT p1.product_id,
p1.product_name,
p1.unit_price,
p2.product_id,
p2.product_name,
p2.unit_price,
ABS(p1.unit_price - p2.unit_price) AS price_diff
FROM products p1
INNER JOIN products p2 ON ABS(p1.unit_price - p2.unit_price) < 0.25
WHERE p1.product_id <> p2.product_id;

Cross JOIN

當你需要組合「所有可能的配對」,而不是依照某種鍵值進行連接時,就會使用 CROSS JOIN,是 SQL 中較少使用、但在特定場景下非常實用的 JOIN 類型。

假設 Table A 有 3 筆資料,Table B 有 5 筆資料,Cross JOIN 後,一共會有 15(3 x 5)筆資料。

  SELECT p1.product_name as product1,
p2.product_name as product2,
p1.product_id
FROM products p1
CROSS JOIN products p2

UNION & UNION ALL

JOIN 是把多張 Table 用「水平的方式」合併在一起;而 UNION 則是把多張表用「垂直的方式」合併起來:

  • UNION 會移除「資料完全重複」的 row
  • UNION ALL 則會保留重複值
警告

UNION 很單純只是把兩張表的資料垂直合併,並不會管表中的欄位名稱、順序是否相同。它只採用第一個 SELECT 所提供的欄位名稱(例如 item)來命名 UNION 後整張表的欄位。

SELECT id, item FROM tops;
UNION
SELECT id, size FROM sizes;
提示

如果你知道要合併的這幾張表間,資料不會有重複值時,使用 UNION ALL 會比 UNION 快得多(因為 UNION 需要多檢查有沒有 duplicate values)

其他

縮寫說明
fkforeign key
pkprimary key

參考