📌為什麼 SQL 有 800 筆但 C# 只剩 60 筆?Oracle 資料重複、JOIN 與去重邏輯完整解析
一、前言
在企業後端系統中,我們常遇到「資料在資料庫查出來一大堆,但程式處理後剩很少」的情況。這篇文章用淺顯方式拆解一組真實案例:
Oracle SQL 查出約 800 筆資料,但 C# 內存處理後只剩 60 多筆。
您會看到錯誤來源、為何發生、如何用 SQL 與 C# 各自正確處理,以及比較用法與修正策略。
二、背景概念(讓非技術人也懂)
-
資料來源:有兩張表
APS_Z_PLAN
和APS_Z_LCM
,需要根據某個版本(APS_VERSION
)把它們「左外連接(LEFT JOIN)」起來。若某些條件符合(如 process 為「後段」、客戶名稱、數量、產品一致),就把對應的 LCM 資料合併進 plan 資料裡。 -
需求:把合併後的資料格式化(像日期變成
yyyy-MM-dd
字串、數字加千分位、欄位空值補空字串),再給前端用。 -
疑問:原始 SQL 撈出 800 筆,但 C# 做完邏輯後只剩 60 多筆。哪裡被「吞掉」了?
三、主要問題拆解
1. C# 版本筆數變少的最大原因:過度去重(GroupBy 全欄位當 key)
在
GetDataByVersionA
裡面最後一段:
.GroupBy(r => ( r.DEMAND_DATE, r.DEMAND_QTY, ... // 幾十個欄位全部列出 r.DEAL_QTY )) .Select(g => g.First())
這段的意思是「把所有欄位值完全一樣的列分在一組,只保留每組的第一筆」。
如果資料裡大量欄位(格式化後的字串、數字)都重複,那整組就只剩一筆,導致 800
筆縮成 60 多筆。
解法選擇:
-
如果原始的 800 筆都要保留:直接移除這段 GroupBy,回傳
resultData
。 -
如果要去重,只依「真正判斷重複業務關鍵」的欄位(例如
APS_PLAN_NO
+SEQ_NO
)分組,不要把所有欄位都當 key。
2. format/轉字串導致「表面相同」加劇去重
像 FormatNumber
把
null
、0
、其他數字都格式化成字串(例如
"0"
或
"1,000"
),原本可能不同的細節在字串層面變同一樣,更容易被當成「完全一樣」被歸為一組。
建議:
-
去重的 Key 用原始 numeric/nullable 型別(不格式化),或限制只以最小必要欄位去重。格式化留給輸出層,不當 group key。
3. JOIN邏輯在 C# 只有
PROCESS_NAME == "後段"
才嘗試配對
若
plan.PROCESS_NAME != "後段"
,matchingLcm
會是
null,產生一大堆只有 plan 本身(lcm 欄位空白)的資料。若這些也被 group key
全部包含,又碰上很多欄位值相同,也會被壓縮去重。
建議:
-
如果預期行為是「SQL 層做 LEFT JOIN(包含所有 plan)」再統一把結果映射,應該把連接邏輯下放到 SQL,把
後段
的條件寫在 JOIN ON 裡(像範例 SQL),一次拿齊再映射,避免在 C# 裡做條件性配對造成不一致。
四、SQL 版本對應:只保留「完全重複」一筆的寫法(用於比對)
如果你要從 SQL 端拿出「把重複資料(欄位值完全一樣)只顯示一筆」來比對 C#
原始全筆結果,可以用分析函式
ROW_NUMBER()
:
WITH raw_data AS ( -- 原始查出來的欄位(格式化 + NVL 處理) SELECT CASE WHEN p.DEMAND_DATE IS NULL THEN NULL ELSE TO_CHAR(p.DEMAND_DATE, 'YYYY-MM-DD') END AS DEMAND_DATE, NVL(TO_CHAR(p.DEMAND_QTY, 'FM999G999G999'), '0') AS DEMAND_QTY, NVL(p.SITE,'') AS SITE, NVL(p.CUSTOMER_NAME,'') AS CUSTOMER_NAME, NVL(p.APPL,'') AS APPL, NVL(l.PLANT,'') AS PLANT, NVL(p.PRODUCT_ID,'') AS PRODUCT_ID, NVL(l.RECIPE,'') AS RECIPE, NVL(l.EQ,'') AS EQ, CASE WHEN l.INPUT_DATE IS NULL THEN NULL ELSE TO_CHAR(l.INPUT_DATE,'YYYY-MM-DD') END AS INPUT_DATE, NVL(TO_CHAR(p.INPUT_QTY,'FM999G999G999'),'0') AS INPUT_QTY, NVL(TO_CHAR(p.NORMAL_OUTPUT,'FM999G999G999'),'0') AS NORMAL_OUTPUT, p.APS_PLAN_NO, NVL(p.SEQ_NO,'') AS SEQ_NO, NVL(p.PAIR_FLAG,'') AS PAIR_FLAG, NVL(p.WAITING_FLAG,'') AS WAITING_FLAG, p.PROCESS_NAME, NVL(TO_CHAR(p.APS_PLAN_LIST,'FM999G999G999'),'0') AS APS_PLAN_LIST, NVL(TO_CHAR(p.APS_PLAN_SEQ,'FM999G999G999'),'0') AS APS_PLAN_SEQ, p.APS_DEAL, CASE WHEN p.APS_DEAL_DATE IS NULL THEN NULL ELSE TO_CHAR(p.APS_DEAL_DATE,'YYYY-MM-DD') END AS APS_DEAL_DATE, NVL(TO_CHAR(p.APS_DEAL_QTY,'FM999G999G999'),'0') AS APS_DEAL_QTY, NVL(TO_CHAR(p.GROUP_ID,'FM999G999G999'),'0') AS GROUP_ID, NVL(p.MODEL_VERSION,'') AS MODEL_VERSION, NVL(p.ORI_PLAN_WEEK,'') AS ORI_PLAN_WEEK, NVL(p.DEMAND_MONTH,'') AS DEMAND_MONTH, NVL(p.DEMAND_WEEK,'') AS DEMAND_WEEK, NVL(p.SATISFY_WAY,'') AS SATISFY_WAY, CASE WHEN p.DEAL_START_DATE IS NULL THEN NULL ELSE TO_CHAR(p.DEAL_START_DATE,'YYYY-MM-DD') END AS DEAL_START_DATE, CASE WHEN p.DEAL_END_DATE IS NULL THEN NULL ELSE TO_CHAR(p.DEAL_END_DATE,'YYYY-MM-DD') END AS DEAL_END_DATE, NVL(TO_CHAR(p.DEAL_QTY,'FM999G999G999'),'0') AS DEAL_QTY FROM APS_Z_PLAN p LEFT JOIN APS_Z_LCM l ON l.APS_VERSION = p.APS_VERSION AND p.PROCESS_NAME = '後段' AND l.CUSTOMER_NAME = p.CUSTOMER_NAME AND NVL(l.DEMAND_QTY,0) = NVL(p.DEMAND_QTY,0) AND l.PRODUCT_ID = p.PRODUCT_ID WHERE p.APS_VERSION = :apsVersion ) SELECT * FROM ( SELECT raw_data.*, ROW_NUMBER() OVER ( PARTITION BY DEMAND_DATE, DEMAND_QTY, SITE, CUSTOMER_NAME, APPL, PLANT, PRODUCT_ID, RECIPE, EQ, INPUT_DATE, INPUT_QTY, NORMAL_OUTPUT, APS_PLAN_NO, SEQ_NO, PAIR_FLAG, WAITING_FLAG, PROCESS_NAME, APS_PLAN_LIST, APS_PLAN_SEQ, APS_DEAL, APS_DEAL_DATE, APS_DEAL_QTY, GROUP_ID, MODEL_VERSION, ORI_PLAN_WEEK, DEMAND_MONTH, DEMAND_WEEK, SATISFY_WAY, DEAL_START_DATE, DEAL_END_DATE, DEAL_QTY ORDER BY APS_PLAN_NO ) AS rn FROM raw_data ) WHERE rn = 1;
這樣可以用來跟 C# 處理後(去重後)的結果做一比一對照:「哪些原始重複只留一筆的狀況在資料庫那邊也一樣」。
五、常見錯誤補充:ORA-00942 表或視圖不存在
如果執行 SQL 遇到:
ORA-00942: 表格或視觀表不存在
可能原因與排查步驟:
-
表名拼錯(大小寫/引號注意):
-
Oracle 預設比較不區分大小寫,但如果表用雙引號建立並含小寫,查的時候要「一模一樣」加雙引號。
-
-
不在自己 schema,下沒有 prefix:
-
查
ALL_TABLES
確認在哪個 owner 有這表:SELECT owner, table_name FROM all_tables WHERE table_name IN ('APS_Z_PLAN','APS_Z_LCM');
-
若在別人 schema(例如
TWFHCLIFE.APS_Z_PLAN
),就加上 schema 前綴:FROM TWFHCLIFE.APS_Z_PLAN p LEFT JOIN TWFHCLIFE.APS_Z_LCM l ...
-
-
沒有權限:
-
即使表存在,當前使用者沒
SELECT
權限也會報錯。請 DBA 給予:GRANT SELECT ON TWFHCLIFE.APS_Z_PLAN TO YOUR_USER; GRANT SELECT ON TWFHCLIFE.APS_Z_LCM TO YOUR_USER;
-
或建立 synonym 讓你用簡名存取:
CREATE SYNONYM APS_Z_PLAN FOR TWFHCLIFE.APS_Z_PLAN; CREATE SYNONYM APS_Z_LCM FOR TWFHCLIFE.APS_Z_LCM;
-
六、實務建議總結(給不熟悉的人)
-
資料撈出來 ≠ 最終給前端的筆數:中間如果有做「去重」、「合併條件不一致」或「格式化再比較」,會把資料壓縮。
-
先在資料庫(SQL)把資料整理乾淨:JOIN 與過濾最好在 SQL 端做,格式化可視需求再做輸出。
-
在程式裡不用把所有欄位都拿去當群組 key:只用能代表「同一筆」的最小集合(像主鍵組合),避免誤把細節格式化後造成的相同值當重複。
-
針對重複要比較時,用 SQL 的 analytic function(如 ROW_NUMBER)取每組第一筆,比單純
DISTINCT
更可控。 -
遇到找不到表的錯誤時:先查系統字典確認表在哪裡、名稱正確、再確認權限。
七、延伸:C# 與 SQL 的整合建議
-
建議把 JOIN 與主要條件移到一個 SQL view 或 stored procedure(例如:
VIEW_APSZRESULTA_FOR_VERSION
),C# 直接呼叫、只做 DTO 映射與格式化(如果放在 presentation layer),減少 in-memory groupby 造成筆數誤差。 -
如果需要比較「原始 vs 去重」,可以在 C# 做兩組輸出(原始
resultData
與去重後),做 diff 並記錄原因給 QA/PM。
結語
這個典型案例揭示了資料在不同層(DB vs 程式)處理時「語意不一致」的陷阱:SQL 一次拿出完整資料,C# 吃進去又做了過度去重。解法是清楚分工、降低 joint key、不要把格式化結果當內部邏輯依據、並使用可解釋的去重(如 analytic function + 明確 key)。如果您希望,我可以幫您把這套改成一個 reusable 的 view + C# DTO 映射模組,甚至做單元測試對照 800 vs 60 筆的差異來源。
留言
張貼留言