Java 開發人員在編寫程式時,需要融合物件導向與命令式程式設計思維,這考驗著開發者的技術深度、架構能力與工程心態。然而,當撰寫 SQL 時,情況截然不同。SQL 是一種宣告式語言,而非物件導向或命令式語言。在 SQL 中編寫複雜查詢需要不同的思維模型,許多 Java 開發者因習慣在記憶體中處理資料,而忽略了資料庫本身強大的集合運算與優化能力。以下整理 Java 程式設計師撰寫 SQL 時最常見的 10 大錯誤,並提供改善方向。
Java 開發者對 SQL 中 NULL 的誤解,往往是最大的錯誤來源。NULL 在 SQL 中代表「未知」,而非單純的「空值」。JDBC 將 SQL NULL 映射為 Java null,導致 NULL = NULL 在 SQL 中評估為 UNKNOWN,而在 Java 中 null == null 為 true,這種語意差異極易引發邏輯錯誤。
特別是在 NOT IN 反連接或行值表達式完整性約束中,NULL 的存在會導致查詢結果不符預期。
IS NULL / IS NOT NULL 而非等號比較。NOT IN 子查詢中確保子查詢結果不含 NULL,或改用 NOT EXISTS。許多開發者習慣將資料庫資料載入記憶體,轉換為 Collection,再用迴圈進行運算。這在 Java 8 Stream API 普及前更為明顯。事實上,現代資料庫支援強大的 OLAP 功能(視窗函數、分組集合、MODEL 子句等),將運算下推到資料庫層不僅程式碼更簡潔,效能也遠超應用層處理,且避免了大量資料的網路傳輸。
每當在 Java 實作資料導向演算法時,自問:資料庫能否以單一 SQL 完成?
UNION 會移除重複列,需對兩個子查詢結果排序並逐列比較,對大資料集效能極差。大多數業務場景允許重複或已知無重複,應優先使用 UNION ALL。
撰寫 UNION 時,務必評估是否真正需要去重,預設使用 UNION ALL。
主流資料庫皆支援原生分頁語法(LIMIT ... OFFSET、OFFSET ... FETCH、ROWNUM、ROW_NUMBER() OVER() 等)。在應用層載入全量資料再分頁,不僅浪費記憶體與網路頻寬,效能更隨資料量線性惡化。
直接在 SQL 使用原生分頁語法;若需跨資料庫相容,可使用 jOOQ 等框架自動轉譯。
部分開發者因恐懼 JOIN 效能不佳,選擇分別查詢多張表後在記憶體中關聯。實則在有適當索引、統計資訊與約束下,資料庫的合併連接、雜湊連接效能極佳,且能大幅減少資料傳輸量。
若多步驟查詢可合併為單一 SQL,務必下推至資料庫執行。
複雜 JOIN 時若遺漏 ON 條件,會產生非預期的笛卡爾積與重複列。以 DISTINCT 或 UNION 強行去重,不僅治標不治本,更因排序去重導致效能災難,且可能漏掉極端情況下的錯誤資料。
發現非預期重複時,優先檢視 JOIN 條件是否缺失,尋找隱藏的笛卡爾積。
許多資料庫支援 MERGE 或類似 INSERT ... ON DUPLICATE KEY UPDATE 的 UPSERT 語法。手動組合 SELECT ... FOR UPDATE + INSERT/UPDATE 不僅程式碼冗長,更易在高併發下產生競爭條件。
涉及「不存在則新增,存在則更新」邏輯時,優先考慮單一 MERGE 敘述。
SQL:2003 引入視窗函數,可在不分組的前提下對結果集進行聚合運算,每個視窗函數擁有獨立的 PARTITION BY,極適合報表查詢。傳統做法需在子查詢中 GROUP BY 再外層 JOIN,不僅可讀性差,優化器也較難優化。
當子查詢出現 GROUP BY 時,評估是否可改寫為視窗函數。
SQL ORDER BY 支援 CASE 等複雜表達式,功能強大。將資料撈到 Java 記憶體排序,不僅增加應用層負擔,更失去資料庫利用索引避免排序的機會,對分頁查詢尤為關鍵。
凡涉及排序,優先在 SQL 完成;僅在極少量資料或極特殊邏輯下考慮應用層排序。
JDBC 支援批次處理。逐筆 INSERT 會重複建立 PreparedStatement、產生大量網路往返與 Undo Log。應使用 addBatch() / executeBatch(),並適時提交事務控制 Undo Log 體積。
大量寫入必須使用批次處理,並根據資料庫特性調整批次大小與提交頻率。
SQL 與 Java 思維模式迥異,優秀的 Java 開發者應主動學習資料庫進階特性,將運算下推,善用宣告式語言的集合運算優勢,編寫出更簡潔、高效、可維護的資料存取層程式碼。