# 我所理解的 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