---
# System prepended metadata

title: '我所理解的 PostgreSQL 04: Temporary Table 是什麼? 跟 CTE 和子查詢差在哪?'
tags: [PostgreSQL, 我所理解的 PostgreSQL]

---

# 我所理解的 PostgreSQL 04: Temporary Table 是什麼? 跟 CTE 和子查詢差在哪?

{%hackmd @moment89/tXJIcGqmSI6_fJQp2hnJUA %}

## 什麼是 Temporary Table (Temp Table)?

### 📌特性

* Session 範圍有效
    Temp Table 會在建立它的 Session 結束時自動清除。
* 跨 Transaction 使用
    在一個 Session 中，多個 Transaction 可以存取同一個 Temp Table。
* 資料通常先儲存在記憶體中，適合短期、大量資料的處理。
* 與 Table 功能類似
    可以建立 Primary Key、Unique Constraint、Index 等，幫助提升資料處理期間查詢效能。
    (通常只在 Temp Table 要處理的資料量大的時候使用較具效益，畢竟建立 index 也是成本。)

### 📌常見應用情境

* Function、Procedure 的中繼資料儲存，除了提供後續資料處理步驟使用，也可以做為 Debug 用途使用。
* 複雜串查或資料集處理，當需進行多步驟、彙整、排序等操作時，可將中間結果暫存至 Temp Table，加快查詢效能並簡化 SQL。
* 一般操作資料庫下 SQL 也可以利用 Temp Table 幫助開發測試。

## Temp Table vs CTE (Common Table Expressions) vs Sub Query

SQL 寫法中，經常使用到 CTE 和 Sub Query，是不是感覺三者用途很像，都是拿來暫存某段 SQL 的查詢結果?
Sub Query 和 CTE 確實很類似，在多數情境下這兩者的查詢計畫會一模一樣，但 Temp Table 則和前兩者有較大的差異，不過這三者是完全可以一起使用的 (實務上經常都用)。

| | Sub Query | CTE | Temp Table |
|--|--|--|--|
| 可重複使用 | 不可重複使用，查詢完成即消失 | 單一查詢內可重複使用，查詢完成即消失 | 同一 Session 內不同 Transaction 可重複使用，Session 結束即消失 |
| 查詢效能 | 小資料量處理快速 | 小資料量處理快速[^CTE] | 適合大資料量處理[^TempTable] |
| 支援 index | 不支援 | 不支援 | 支援 |

[^CTE]: 只要沒有下 MATERIALIZED，則 PostgreSQL 優化器處理 CTE 的邏輯和 Sub Query 一模一樣。
[^TempTable]: 要注意 Temp Table 無法觸發平行查詢。➡️[相關討論](https://stackoverflow.com/questions/69533864/why-are-scans-of-ctes-and-temporary-tables-parallel-restricted)

以下舉例，同一種查詢用 Sub Query、CTE、Temp Table 分別會長什麼樣子?

### 測試資料

隨意建立幾張資料表，塞入 Demo 用的資料。

```sql=
-- 客戶資料表
DROP TABLE IF EXISTS test.customers CASCADE;
CREATE TABLE test.customers (
    customer_id bigserial PRIMARY KEY,
    customer_name varchar(100),
    customer_level int not null CHECK (customer_level BETWEEN 1 AND 3), -- 1: 超級會員, 2: 特輯會員, 3: 普通會員
    created_at timestamp with time zone DEFAULT now(),
    modified_at timestamp with time zone DEFAULT now()
);

-- 產品資料表
DROP TABLE IF EXISTS test.products CASCADE;
CREATE TABLE test.products (
    product_id bigserial PRIMARY KEY,
    product_name varchar(100),
    price numeric,
    inventory int not null default 0,
    created_at timestamp with time zone DEFAULT now(),
    modified_at timestamp with time zone DEFAULT now()
);

-- 訂單資料表
DROP TABLE IF EXISTS test.orders CASCADE;
CREATE TABLE test.orders (
    order_id bigserial PRIMARY KEY,
    order_status int CHECK (order_status BETWEEN -1 AND 2), -- 0: 待付款, 1: 已付款, 2: 已出貨, -1: 已取消
    customer_id bigint references test.customers(customer_id) on delete cascade on update cascade,
    created_at timestamp with time zone DEFAULT now(),
    modified_at timestamp with time zone DEFAULT now()
);

-- 訂單內容資料表
DROP TABLE IF EXISTS test.order_info CASCADE;
CREATE TABLE test.order_info (
    order_id bigint references test.orders(order_id) on delete cascade on update cascade,
    product_id bigint references test.products(product_id) on delete cascade on update cascade,
    quantity int,
    created_at timestamp with time zone DEFAULT now(),
    modified_at timestamp with time zone DEFAULT now()
);

-- 塞入測試資料
INSERT INTO test.customers (customer_name, customer_level) VALUES ('Customer A', 1), ('Customer B', 1), ('Customer C', 2), ('Customer D', 2), ('Customer E', 3);
INSERT INTO test.products (product_name, price, inventory) VALUES ('Product A', 10, 500), ('Product B', 20, 480), ('Product C', 30, 190);
INSERT INTO test.orders (order_status, customer_id) VALUES (0, 1), (1, 2), (-1, 3), (2, 4), (1, 5);
INSERT INTO test.order_info (order_id, product_id, quantity) VALUES (1, 1, 10), (1, 2, 5), (2, 3, 10), (2, 2, 20), (3, 1, 15), (3, 3, 30);
```

### Demo

#### Sub Query

用 Sub Query 的方式查詢超級客戶買的產品分布。
在這樣單純的查詢條件下，看起來 SQL 不會太難閱讀，但若串查條件較多，在閱讀上會比較不方便。

```sql=
SELECT
    product_id,
    sum(quantity) AS total_quantity
FROM test.order_info
WHERE order_id IN (
    SELECT order_id
    FROM test.orders
    WHERE customer_id IN (
        SELECT customer_id
        FROM test.customers
        WHERE customer_level = 1
    )
)
GROUP BY product_id
ORDER BY product_id;
```

查詢結果和查詢計劃如下圖。
<center class="half">
    <img src=https://hackmd.io/_uploads/ryPQ69Ddxx.png width="300"/>
    <img src=https://hackmd.io/_uploads/HkbaR9Dugl.png width="300"/>
</center>

#### CTE

用 CTE 的方式查詢超級客戶買的產品分布。
看似比 Sub Query 冗長一點，但可以避免過深的巢狀 SQL，在串查條件較多的情況下會較容易閱讀。

```sql=
WITH cte_super_customer AS (
    SELECT customer_id
    FROM test.customers
    WHERE customer_level = 1
), cte_orders AS (
    SELECT o.order_id
    FROM test.orders AS o
    JOIN cte_super_customer AS c
    ON o.customer_id = c.customer_id
)
SELECT
    oi.product_id,
    sum(oi.quantity) AS total_quantity
FROM test.order_info AS oi
JOIN cte_orders AS o
ON oi.order_id = o.order_id
GROUP BY oi.product_id
ORDER BY oi.product_id;
```

查詢結果和查詢計劃如下圖。
可以看到查詢計劃和 Sub Query 幾乎一樣。
<center class="half">
    <img src=https://hackmd.io/_uploads/r1KJAqv_lg.png width="300"/>
    <img src=https://hackmd.io/_uploads/Hy-myjDdxe.png width="300"/>
</center>

#### Temp Table

Temp Table 的寫法看起來最冗長，但同一 Session 下每張 Temp Table 都可以反覆查詢，非常適合資料要重複利用或是要 Debug、驗證的場景。

```sql=
DROP TABLE IF EXISTS tt_super_customer;
CREATE TEMP TABLE tt_super_customer AS
SELECT customer_id
FROM test.customers
WHERE customer_level = 1;

DROP TABLE IF EXISTS tt_orders;
CREATE TEMP TABLE tt_orders AS
SELECT o.order_id
FROM test.orders AS o
JOIN tt_super_customer AS c
ON o.customer_id = c.customer_id;

DROP TABLE IF EXISTS tt_total_quantity;
CREATE TEMP TABLE tt_total_quantity AS
SELECT
    oi.product_id,
    sum(oi.quantity) AS total_quantity
FROM test.order_info AS oi
JOIN tt_orders AS o
ON oi.order_id = o.order_id
GROUP BY oi.product_id;

SELECT * FROM tt_total_quantity ORDER BY product_id;
```
<center class="half">
    <img src=https://hackmd.io/_uploads/BkxbejvOxe.png width="300"/>
</center>

#### 比較

可以看到以上三種做法的差異，但其實他們是可以依需求混用的。

比如說，我認為串查客戶 id 的部分只取用一個欄位、查詢條件單純，就會選用 Sub Query；想特別驗證計算過程中取得的超級會員訂單id有沒有錯誤，就會選擇把這段資料存為 Temp Table 以利後續複查。
那我最後的寫法可能就會如下：

```sql=
-- 混用方案: 想要驗證超級會員的訂單id
DROP TABLE IF EXISTS tt_super_customer_orders;
CREATE TEMP TABLE tt_super_customer_orders AS
SELECT order_id FROM test.orders
WHERE customer_id IN (
    SELECT customer_id FROM test.customers
    WHERE customer_level = 1
);

DROP TABLE IF EXISTS tt_total_quantity;
CREATE TEMP TABLE tt_total_quantity AS
SELECT
    oi.product_id,
    sum(oi.quantity) AS total_quantity
FROM test.order_info AS oi
JOIN tt_super_customer_orders AS o
ON oi.order_id = o.order_id
GROUP BY oi.product_id
ORDER BY product_id;

SELECT * FROM tt_total_quantity; -- 查看產品分布
SELECT * FROM tt_super_customer_orders; -- 驗證超級會員的訂單id有沒有串錯
```

### 結論

以下分享我個人在寫 SQL 時應用的思路。
一次性查詢、條件單純 ➡️Sub Query
條件複雜想提升可讀性 ➡️ CTE、Temp Table
需要在單一 SQL 中重複使用的中間查詢 ➡️ CTE
需要跨多個 SQL 間重複使用的中間查詢 ➡️ Temp Table