主題: PostgreSQL
一個 SELECT,要講清楚四件事:欄位、篩選、順序、上限
可靠的 SQL query 不只把資料撈出來:明列欄位、用 WHERE 篩選、以 ORDER BY 定義順序,最後才用 LIMIT 截取結果。
很多「最新文章」、「最近訂單」或「我的任務」功能,最後都會長得像一條 SELECT。最短的版本也許只有:
SELECT * FROM articles LIMIT 10;
它能跑,卻還沒有把產品需求講完。要哪些資料?哪些資料可以出現?最新到底怎麼算?同一時間的兩筆資料誰在前?如果這些問題沒有寫進 query,畫面今天看起來正常,不代表下次還會回傳同一批資料。
動態迷因(展開/收合)
對單一 table 的讀取,我現在會先拆成四個決定:輸出哪些欄位、保留哪些 rows、如何排序、最多拿幾列。 這不是背語法順序,而是把需求翻譯成每個子句各自負責的事。
先看一條完整,但仍然很小的 query
假設 articles 有 id、title、status 與 published_at。首頁要列出最新 10 篇已發布文章:
SELECT id, title, published_at
FROM articles
WHERE status = 'published'
ORDER BY published_at DESC, id DESC
LIMIT 10;
這五行各自回答一個問題:
| 子句 | 回答的問題 |
|---|---|
SELECT id, title, published_at |
呼叫端會拿到哪些欄位? |
FROM articles |
資料來自哪個 table? |
WHERE status = 'published' |
哪些 rows 可以進入結果? |
ORDER BY published_at DESC, id DESC |
結果以什麼可預期的順序呈現? |
LIMIT 10 |
最多需要幾列? |
讀 query 時,先用這張表拆責任,比直接背「資料庫內部先做哪一個步驟」更有用。之後學到 JOIN、GROUP BY 或 subquery,也能繼續問同樣的問題。
SELECT:把輸出當成 contract
SELECT list 是回傳資料的形狀。只需要標題與時間時,明列 title, published_at 比 SELECT * 更誠實:呼叫端看到的就是它依賴的欄位。
SELECT * 並不是錯誤;在 psql 裡臨時查看 table 很方便。但 application code 若長期用它,某天 table 新增內部欄位,回傳資料就可能在沒有改 query 的情況下改變。PostgreSQL 的 tutorial 也把它視為即席查詢方便、但 production code 應謹慎使用的 shorthand。
這不是為了少傳幾個 bytes 而做的潔癖,而是把 API 與 UI 需要什麼說清楚。欄位明列後,review 時也更容易問:「這個畫面真的需要 email 或內部備註嗎?」
WHERE:決定哪些 rows 有資格出現
WHERE 接的是條件。只有條件結果為 true 的 rows 會留在結果裡:
SELECT id, title
FROM articles
WHERE status = 'published';
這和「在前端先拿全部資料,再用 JavaScript 過濾」不是同一件事。把篩選條件放進 query,能讓資料庫只交出符合需求的結果;同時也讓讀取規則留在可檢視、可測試的位置。
實際把使用者輸入帶進條件時,仍應透過 driver 或 ORM 的參數綁定,而不是自己拼接 SQL 字串。這篇的重點是讀取責任,不是把字串拼接變成另一套 API。
ORDER BY:資料表不是排好隊的清單
若需求寫「最新 10 篇」,排序不是可有可無的裝飾。ORDER BY published_at DESC 的 DESC 表示較晚的時間在前;如果沒寫方向,預設是 ASC,也就是較早的時間在前。
更容易漏掉的是 tie-breaker。同一秒或同一個 timestamp 出現兩筆文章時,只排序 published_at 仍不能完全決定誰先誰後。再補上一個穩定欄位:
ORDER BY published_at DESC, id DESC
這不是保證 id 的業務意義等於時間,而是讓「時間相同時怎麼排」也成為明確規則。PostgreSQL 文件同樣提醒:只排序一個值時,該值相同的 rows 仍可能以不同順序回傳。
動態迷因(展開/收合)
LIMIT:截取結果,不創造順序
LIMIT 10 的意思是最多回傳 10 列;符合條件的資料如果只有 3 列,就只會得到 3 列。它不會保證剛好 10 列,也不會替資料加上「最新」這個概念。
因此下面這條 query 對臨時瀏覽可以,但不該作為產品的最新列表:
SELECT title
FROM articles
LIMIT 10;
沒有 ORDER BY 時,SQL 不承諾 rows 的順序;搭配 LIMIT 時,甚至同一條 query 在不同執行時可能選到不同子集。PostgreSQL 對這點說得很直接:若要可靠地截取結果,排序必須先限制成可預期的順序。
這也是為什麼「先 ORDER BY,再 LIMIT」不只是看起來整齊,而是 page、feed 與最近項目功能的資料契約。
把需求改寫成 query 的小流程
遇到讀取需求時,我會先用自然語言列出四句話:
- 這個畫面或 API 要顯示哪些欄位?
- 哪些 rows 符合資格?
- 使用者期望什麼順序?同值時如何處理?
- 這次最多需要幾列?
再把答案放回 SELECT、WHERE、ORDER BY、LIMIT。例如「最新 10 篇已發布文章」會自然變成前面的五行 SQL;「依標題排序的所有已發布文章」則不需要硬塞 LIMIT。
這個流程也能避免兩種常見誤會:把 LIMIT 當排序、把 SELECT * 當永久 API contract。先讓需求每一部分有自己的位置,query 才比較容易改、容易 review,也不會在資料量長大時突然變得難以解釋。
我學到什麼
- 我會把單表讀取拆成欄位、rows、順序與上限四個決定,而不是先寫一條看起來能跑的
SELECT。 ORDER BY沒寫方向時是升冪;需要最新資料在前,就要明寫DESC。LIMIT只限制數量,不能讓沒有排序的結果變得穩定;同值排序還需要明確的 tie-breaker。SELECT *適合臨時探索,應用程式則應明列真正需要的欄位,讓輸出成為可 review 的 contract。
結論:先定義結果,再把資料拿回來
SQL 的入門不是記住所有子句,而是知道每個子句替需求補上哪一塊缺口。一條可靠的讀取 query 會清楚指定 output、filter、order 與 count。
當這四件事都被說清楚,SELECT 就不只是「資料撈得出來」;它會變成一個可讀、可測試、可預期的讀取契約。
外部參考連結
- PostgreSQL:Querying a Table
- PostgreSQL:SELECT
- PostgreSQL:LIMIT and OFFSET
- daily.dev:Day 36 of #100DaysOfCode — SQL Basics
- Reddit:SELECT、WHERE、LIKE 與 LIMIT 的初學者討論
- Reddit:ORDER BY、LIMIT 與 OFFSET 的初學者討論
延伸學習
- CS50 SQL Week 0:Querying:Harvard CS50 的官方影片課程,可跟著練習
SELECT、WHERE、ORDER BY與LIMIT。