--- title: 'Postgresql函數 & 基本SQL語法紀錄' disqus: hackmd --- # X. 子查詢運算子-Exists * **作用**:檢查**某個子查詢是否有結果** * **重點**:它不管子查詢回傳什麼資料,**只在意有沒有資料** * **語法**: ```sql= SELECT EXISTS ( SELECT 1 FROM table_name WHERE conditions ); ``` * `SELECT 1`是**習慣用法**,只要子查詢有資料就會回傳`true` * `EXISTS` 會在**發現第一筆資料**後就**停止查詢**,**效率**通常比`COUNT(*) > 0`**好** * **例**:**判斷某筆資料是否存在** ```sql= SELECT EXISTS ( SELECT 1 FROM users WHERE username = 'nicolas' ); ``` * **例**: 在**條件式**中使用 `EXISTS` 做**邏輯判斷** ```sql= DELETE FROM orders WHERE EXISTS (SELECT 1 FROM users WHERE users.id = orders.user_id AND users.status = 'banned' ); ``` * **例**:搭配 `NOT EXISTS` 查找「孤兒資料」 ```sql= SELECT * FROM products p WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.product_id = p.id ); ``` * 查找返回`False` * **例**:**在 JOIN 無法簡單解的情境下,EXISTS 是救星**,找出**有修過 CS101 的學生**。 ```sql= SELECT name FROM students s WHERE EXISTS ( SELECT 1 FROM enrollments e WHERE e.student_id = s.id AND e.course_id = 'CS101' ); ``` # X. 模擬「最大值」查詢 - NOT EXISTS + HAVING * **代碼**: ```sql= SELECT s.name FROM students s JOIN enrollments e ON s.id = e.student_id GROUP BY s.id, s.name HAVING NOT EXISTS ( SELECT 1 FROM enrollments e2 GROUP BY e2.student_id HAVING COUNT(*) > COUNT(e.student_id) ); ``` * `GROUP BY s.id, s.name`: **把每個學生的資料「合併」** * `COUNT(*) > COUNT(e.student_id)`:**比較「別人」選的課數 >「我」選的課數** * `COUNT(*)`:**子查詢** (`e2`) 的**結果** * `COUNT(e.student_id)`:**外層查詢**的**結果** * **其他**: * **最小值**:`HAVING COUNT(*) < COUNT(e.student_id)` # X. 時間字段擷取-EXTRACT * **語法**: * `extract(field from timestamp)` * `extract(field from interval)` * **例**:**擷取天** * select **extract**(`day` from now()) * **字串**(`轉成 timestamp`):select **extract**(`day` from timestamp '2013-04-13') * **例**:擷取相差**秒數**(`epoch`) * select extract(`epoch` from `(timestamp '2012-09-02 00:00:00' - timestamp '2012-08-31 01:00:00')`) * **回傳**`double precision` * **參考**:https://www.postgresql.org/docs/9.2/functions-datetime.html # X. 字串 to 時間 * **例**:**字串 to timestamp** * select `timestamp '2013-04-13'` * select `'2013-04-13'::timestamp` * select `cast('2012-08-31 01:00:00' as timestamp);` # X. 產生連續值的序列-generate_series() * 數字序列、日期序列 * 語法: ```sql= generate_series(start, stop [, step]) ``` * **參數**: * start: 起始值(可以是整數、timestamp) * stop: 結束值 * step: 步長(預設是 1,interval 可自定) ## 1. 整數序列 * **代碼**:產生`1 ~ 10` ```sql= SELECT generate_series(1, 10); ``` ## 2. 時間序列 * **代碼**:產生`01/1 ~ 01/31`,**每天**(`'1 day'::interval`) ```sql= SELECT generate_series( '2025-01-01'::timestamp, '2025-01-31'::timestamp, '1 day'::interval ) AS time_point; ``` ### 2.1 時間序列-每月月底 * **正確**: ```sql= select (month + interval '1 month')- interval '1 day' , from ( select generate_series( timestamp '2012-01-01', timestamp '2012-12-01', interval '1 month' ) as month ) a ``` * **錯誤**: ```sql= SELECT generate_series( '2012-01-31 00:00:00'::timestamp, '2012-12-31 00:00:00'::timestamp, '1 month'::interval ) AS time_point ``` * **結果**:`與預期的「每個月月底」不符` ```yaml= 2012-01-31 00:00:00 2012-02-29 00:00:00 2012-03-29 00:00:00 2012-04-29 00:00:00 ... 2012-12-29 00:00:00 ``` * **問題原因解析**: * 它會「原樣保留」起始日的「日(day)」數字。 * 當**該月**`沒有這一天`,會 **「往前回推」** 到**當月**`最後『合法』日期` * **例**: * 1/31 + 1 month → 2/29(閏年,合法) * `2/29 + 1 month` → `3/29`(`不是月底,因為是 2/29 + 1 month`) # X. 日期時間型別-Interval * `INTERVAL` 表示 **『時間的長度』** 或 **『時間差值』** * **例**: * 1 day * 3 hours * 5 minutes 30 seconds * 不像 `timestamp` 是**一個時間點**,`interval` 是一個 **『持續多久』** * **常見單位**: | 單位 | 用法 | | -------- | ----------------------- | | `second` | `INTERVAL '30 seconds'` | | `minute` | `INTERVAL '15 minutes'` | | `hour` | `INTERVAL '4 hours'` | | `day` | `INTERVAL '7 days'` | | `month` | `INTERVAL '2 months'` | | `year` | `INTERVAL '1 year'` | * **時間加減** * **返回**`Interval` * **例**:select `timestamp '2012-08-31 01:00:00' - timestamp '2012-07-30 01:00:00'` * **返回**:`32 days` * 時間加減`Interval` * **返回**`時間` * 例:select timestamp '2012-08-31 01:00:00' + `'1 day'::interval` * **返回**:`2012-09-01 01:00:00.000` # X. 各種時間/日期運算的語法與結果清單 ## 1. 加法運算 | 運算類型 | 結果類型 | 說明 | | ---------------------- | ----------- | ------------------- | | `date + integer` | `date` | 加上天數(單位是天) | | `date + interval` | `timestamp` | 日期加時間段,轉成 timestamp | | `timestamp + interval` | `timestamp` | 加上時間段,結果保留時間 | | `time + interval` | `time` | 時間加時間段,不含日期 | | `interval + interval` | `interval` | 相加組合時間段 | ## 2. 減法運算 | 運算類型 | 結果類型 | 說明 | | ----------------------- | ----------- | ------------------ | | `date - date` | `integer` | 計算兩日期間的天數差(回傳整數) | | `date - integer` | `date` | 減去天數(單位是天) | | `date - interval` | `timestamp` | 減去時間段,轉成 timestamp | | `time - time` | `interval` | 計算時間差 | | `time - interval` | `time` | 減去時間段 | | `timestamp - interval` | `timestamp` | 減去時間段 | | `interval - interval` | `interval` | 兩個時間段相減 | | `timestamp - timestamp` | `interval` | 計算時間差(精確到秒) | ## 3. 程法運算 | 運算類型 | 結果類型 | 說明 | | -------------------- | ---------- | --------------- | | `numeric * interval` | `interval` | 擴大時間段,例如 3.5 小時 | | `integer * interval` | `interval` | 同上,整數倍 | ## 4. 除法運算 | 運算類型 | 結果類型 | 說明 | | -------------------- | ---------- | ---------------- | | `interval / numeric` | `interval` | 把時間段除以倍數,等於縮短時間段 | # X. 分層彙總(Hierarchical Aggregation)- ROLLUP * 只能用在標準的 `GROUP BY`,不能混用 `GROUPING SETS` 和 `CUBE` 一起用(會混亂) * **使用**: | 你要的分析 | 寫法 | | ----------------- | ---------------------------------------- | | 每月/每季/每年營收 + 年度總和 | `GROUP BY ROLLUP (year, month)` | | 每個業務員業績 + 部門總和 | `GROUP BY ROLLUP (department, employee)` | | 商品每週銷售量 + 商品總銷售 | `GROUP BY ROLLUP (product_id, week)` | # X.「截斷」日期/時間到指定的時間粒度-date_trunc * **作用**:來 **「截斷」** 日期/時間到**指定的時間粒度**,回傳**截斷後的時間** * **函數**:`date_trunc(text, timestamp)` * 例:`2025-05-27 13:45:30` * 截斷到`月`:SELECT `date_trunc('month', TIMESTAMP '2025-05-27 13:45:30');` * **返回**:`2025-05-01 00:00:00` * 截斷到`天`:SELECT `date_trunc('day', TIMESTAMP '2025-05-27 13:45:30');` * **返回**:`2025-05-27 00:00:00` * 截斷到`小時`:SELECT `date_trunc('hour', TIMESTAMP '2025-05-27 13:45:30');` * **返回**:`2025-05-27 13:00:00` * 截斷到`分鐘`:SELECT `date_trunc('minute', TIMESTAMP '2025-05-27 13:45:30');` * **返回**:`2025-05-27 13:45:00` * **使用場景**: * **日期分組**:例如你想做某月、某天、某小時的統計時,把時間截到月初、日初、或小時初,**方便**`GROUP BY` * **時間區間比較**:判斷兩筆時間是否落在同一週、同一月、同一天等 * **日期計算基準點**:如剛剛提的當月天數,**先算出當月第一天**,方便往後加減天數 # X. 窗口函數 over() - 滾動範圍語法 * **語法**: ```sql= <aggregate_function>(column) OVER ( PARTITION BY partition_column -- 可選 ORDER BY sort_column -- 必要 ROWS|RANGE BETWEEN <start> AND <end> ) ``` * **語法片段**: | 語法片段 | 說明 | | ----------------- | ------------------------- | | `PARTITION BY` | 對每個分組獨立計算(例如:每個使用者、每個地區) | | `ORDER BY` | 必須,指定資料的時間或順序 | | `ROWS` / `RANGE` | **定義視窗的方式**(**列數** vs **數值範圍**) | | `BETWEEN A AND B` | **滾動**的`開始`與`結束`位置(例如:往前 14 天到今天) | * **ROWS** VS **RANGE** | 類型 | 說明 | 使用時機 | | ------- | -------- | ---------------------- | | `ROWS` | 精確的列數 | 適合資料間隔均勻 | | `RANGE` | 根據排序欄位的值 | 適合時間戳不連續、使用 `INTERVAL` | * **起始和結束的關鍵字**: | 關鍵字語法 | 中文意思 | 範圍代表什麼 | | --------------------- | -------- | --------------------- | | `UNBOUNDED PRECEDING` | 最前面那筆開始 | 從 partition 的開頭開始 | | `n PRECEDING` | **往前**第 n 筆 | 目前這一列**往前數** n 筆(不包含目前這筆) | | `CURRENT ROW` | **當前這一筆** | 僅包含目前這一筆 | | `n FOLLOWING` | **往後**第 n 筆 | 目前這一列**往後數** n 筆(包含目前這筆) | | `UNBOUNDED FOLLOWING` |最後面那筆為止 | 直到 partition 的結尾 | ## 1. 常見滾動範圍寫法範例 * **往前**` 14 筆` + **當前筆**(`共 15 筆的視窗`) * **ROWS** BETWEEN `UNBOUNDED PRECEDING` AND `CURRENT ROW` * **從開頭到目前這筆**(累積平均、累積總和) * **ROWS** BETWEEN `UNBOUNDED PRECEDING` AND `CURRENT ROW` * **從目前這筆到結尾**(累積向後) * **ROWS** BETWEEN `CURRENT ROW` AND `UNBOUNDED FOLLOWING` * **往後 3 筆**(不含自己) * ROWS BETWEEN `1 FOLLOWING` AND `3 FOLLOWING` * **計算 15 天滾動平均收入**(`需要 RANGE 搭配 INTERVAL,PostgreSQL`) * **RANGE** BETWEEN `INTERVAL '14 days' PRECEDING` AND `CURRENT ROW` # X. 窗口函數 over() - 配合的函數 ## 1. 聚合型窗口函數 (Aggregate Window Functions) 這類聚合函數在 `OVER()` 下不縮減筆數,而是在窗口上做計算。 - `SUM()` - `AVG()` - `MIN()` - `MAX()` - `COUNT()` ## 2. 排名與序列函數 (Ranking and Row Number Functions) * 用來給資料排序、排名、編號。 * `ROW_NUMBER()`:排名相同時不重複,按順序排序 * `RANK()`:排名相同時重複,總數不會變 * `DENSE_RANK()`:排名相同時重複,總數減少 * `NTILE(n)`:資料平均分組**成 N 等份** * 把一個集合數據,分成**N組**(`等份`) ## 3. 偏移與導航函數 (Offset and Navigation Functions) * 用來在分區內取**前一筆**、**下一筆**或指定偏移的值。 * **前一筆**:`LAG(column, offset, default)` * **下一筆**:`LEAD(column, offset, default)` * 例:[Consecutive Numbers](https://leetcode.com/problems/consecutive-numbers/) 取前後一筆 ``` sql= SELECT Num, LAG(Num) OVER (ORDER BY Id) AS prev_num, LEAD(Num) OVER (ORDER BY Id) AS next_num FROM Logs ``` ## 4. 統計與分布函數 (Statistical and Distribution Functions) * `CUME_DIST()`:**累積分布百分比(Cumulative Distribution)** * **回傳值範圍**:`0 ~ 1` * 不會重複,每筆記錄都代表**累加的百分比** * **應用場景**: * 分析某商品在類別中價位分布百分比 * 判斷用戶消費佔比前幾%的 VIP 分群 * `PERCENT_RANK()`:**百分比排名** * 計算目前行的百分比排名(`依據排名位置,而非值的分布`) * **回傳值範圍**:`0 ~ 1` * 第一筆永遠是 0 * **應用場景**: * 中位數分層 * 前後段班評比 * `FIRST_VALUE(column)`:取得**分組**中**排序**後的**第一筆值** * **應用場景**: * 每個用戶的**第一筆**交易金額 * **最早**建立時間的產品價格 * `LAST_VALUE(column)`:取得**分組**中**排序**後的**最後一筆值** * **應用場景**: * 每個會員**最近一次**登入時間或 IP * **最新**的產品價格 * `NTH_VALUE(column, n)`:取得**排序後**的**第 N 筆值** * **應用場景**: * 找出每個產品上架的**第3筆**價格 * 使用者的**第N筆**操作或交易紀錄 ## 5. 使用注意 * `OVER()` 裡可以加 `PARTITION BY` 來分區,`ORDER BY` 來排序。 * Window Functions 不會減少結果筆數。 * 無法用在 `WHERE`,但能用在 `SELECT`、`ORDER BY` # X. 整數四捨五入 ## 1. 整數捨入技巧(integer rounding trick) * **公式**:`((數值 + 基準/2) / 基準) * 基準` * **四捨五入到最接近的「基準倍數」** * **例**:`((x + 10/2 ) / 10 ) * 10` * **概念**: * **四捨五入的關鍵動作**:數據+半個基準 * 整數除法**自動捨棄小數**,會達到「四捨五入」效果 ## 2. round() * **公式**:`ROUND(數值 / 基準(小數)) * 基準` * **例**:`ROUND(x / 10.0) * 10` # X. `GROUP BY` 欄位選擇策略 ## 1. `GROUP BY` 欄位選擇規則 | `GROUP BY` 條件 | SELECT 可否用其他欄位 | 原因說明 | |----------------------------------|------------------------|----------------------------------------------| | 欄位是 `Primary Key` (PK) | ✅ 可以 | PK 唯一識別一筆資料,其他欄位具**函數相依性** | | 欄位是 `Unique Key` (UK) | ✅ 可以 | UK 同上,唯一性保障函數相依 | | 是組合唯一鍵(Composite PK/UK) | ✅ 可以 | 若組合唯一,其他欄位也有穩定相依性 | | 非唯一欄位(例如 `name`) | ❌ 不可以 | 不保證唯一性,可能產生多筆不穩定結果 | ## 2. 什麼是函數相依(Functional Dependency)? * **概述**:若欄位 A 唯一**決定**欄位 B 的值,則稱 **B 函數相依於 A** * 這表示當你 `GROUP BY` 某欄位(如 facid),若 facid 能唯一對應出 name、monthlymaintenance,PostgreSQL 就允許你直接 SELECT 它們。 * **代碼**: ```sql -- ✅ facid 是唯一的,name 與 monthlymaintenance 就能被 SELECT SELECT facid, name, monthlymaintenance, SUM(slots) FROM cd.facilities f JOIN cd.bookings b ON f.facid = b.facid GROUP BY facid; ``` # X. 查看數據類型-pg_typeof() * **函數**:**pg_typeof**(`col`) * **例**: ```sql= SELECT pg_typeof(id), pg_typeof(name), pg_typeof(created_at) FROM your_table LIMIT 1; ``` # X. 字串「補滿到指定長度」- lpad() & rpad() * 字元**補**在 * **左邊**(`lpad`) * **右邊**(`rpad`) * **語法**: ```sql= lpad(string text, length int [, fill text]) rpad(string text, length int [, fill text]) ``` * **參數**: * `string`:原始字串 * `length`:最終要**補到的總長度**(`字元數`) * `fill`:**要補上的字元**(`預設是空格 ' '`) * **例**: * `左邊`**補**`0`到**6個字元**:`SELECT lpad('123', 6, '0'); -- 結果:'000123'` * `右邊`**補**`0`到**6個字元**:`SELECT rpad('ABC', 6, '0'); -- 結果:'ABC '` # X. 字符替換-TRANSLATE * **多字符一對一替換** * **情境**: * **只想改字元**,比如:`a → x、b → y` * 移除**幾個特定符號** * **語法**:`translate ( string text, from text, to text )` * **建議**:`from` & `to` 的**長度**建議**一樣** * **例**:SELECT `TRANSLATE('a1b2c3', 'abc', 'xyz');` * **結果**: `x1y2z3` * 把 `'a'` 換成 `'x'`,`'b'` 換成 `'y'`,`'c'` 換成 `'z'` * **例**:SELECT `TRANSLATE('(02)-1234/5678', '()/-', '');` * **結果**:`0212345678` # X. 字符替換-REGEXP_REPLACE * **針對樣式或整段字串替換** * **情境**: * 處理**一堆奇怪的格式**、刪**特定 pattern** * 刪掉**非數字、非字母字元** * **例**:SELECT `TRANSLATE('(02)-1234/5678', '[^0-9]', '');` * **結果**:`0212345678` # X. 遞迴查詢(recursive query)- WITH RECURSIVE * **作用**:處理**階層資料結構**(`樹狀結構、家族譜、組織架構、目錄等`) * **語法**: ```sql= WITH RECURSIVE cte_name (column1, column2, ...) AS ( -- Anchor member: 基礎查詢(起始點) SELECT ... FROM ... WHERE ... UNION [ALL] -- Recursive member: 遞迴查詢(會引用前一層的結果) SELECT ... FROM some_table JOIN cte_name ON ... ) SELECT * FROM cte_name; ``` * **說明**: | 組件 | 說明 | | ---------------- | ------------------------------------------- | | `WITH RECURSIVE` | 宣告一個具名的遞迴 CTE(Common Table Expression) | |**Anchor member** | 遞迴的起點,**只跑一次** | | **Recursive member** | 遞迴的本體,每次會用**上次結果**再去 `JOIN` 或`處理新的資料` | | `UNION ALL` | **合併** `Anchor` 和 `Recursive` 的結果集(可用 `UNION` 去除重複) | * **注意**: * **遞迴終止條件**:不需要明確寫出終止條件,因為遞迴**無法 JOIN 到更多資料時**就會**自動停止** * **避免無窮遞迴**:小心循環依賴,否則會爆 stack(PostgreSQL 預設遞迴最大 100 層,可調整 max_recursion_depth)。 * **性能**:**複雜的遞迴查詢**可考慮 `materialized views`、或`一次性展平表格`換取效能。 # X. 數值 & 日期格式化 - TO_CHAR * [官網](https://www.postgresql.org/docs/17/functions-formatting.html) ## 1. 數值格式化 * **基本符號** | 符號 | 說明 | 實例 | | --- | --------------- | ------------------------------------- | | `9` | **可省略位**(`不顯示前導空白`) | `TO_CHAR(5, '999') ➜ ' 5'` | | `0` | **不可省略**,**強制**`補零` | `TO_CHAR(5, '000') ➜ '005'` | | `.` | 小數點(非 locale 版) | `TO_CHAR(5.5, '999.99') ➜ ' 5.50'` | | `,` | 千分位分隔符 | `TO_CHAR(12345, '99,999') ➜ '12,345'` | * **例**: * **整數9位,小數兩位(補0)**:`FM999999990.00` * **小數點前**是`0`,避免數值**只有小數的部分**,格式化後,**整數第一位**可以**補0** * `FM`(`修飾符`):**清除前綴空格**(`整數部分,未達格式設定位數時,會有空格`) # X. LATERAL 子查詢 * **用在子查詢**(`subquery`),它讓**子查詢**可以 **『參考 同一層查詢裡 前面表的欄位』** * **子查詢**能 **『拿外層變數用』** * **例**:針對 `table_a` 的每一筆資料,去 `table_b` 找**對應**的`最新一筆 val` ```sql SELECT a.id, b.val FROM table_a a JOIN LATERAL ( SELECT val FROM table_b WHERE table_b.a_id = a.id ORDER BY created_at DESC LIMIT 1 ) b ON true; ``` * **子查詢裡**的 `a.id` 來自**外層表** * `JOIN`可以使用 * `{ [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN` + `LATERAL` * `ON true;`:**無條件成立的 JOIN** * **本質上是JOIN表,所以需要JOIN寫條件** * 場景: * 找`最新`或`最相關`的**子資料**(`One-to-Many場景`) * **簡化**`複雜的聚合`或`分組查詢` ###### tags: `Postgresql` `sql`