🔎EF Core 連 Oracle 出現 ORA-00600 [kpp_concatq:2] 的完整排錯指南(含 EF Core ToString/CultureInfo 錯誤)

 

摘要(Meta Description)

在 .NET EF Core 對 Oracle 查詢時,若遭遇 ORA-00600 [kpp_concatq:2],多半是查詢優化器在字串型別轉換或重寫(Query Transformation)時踩到版本 Bug。本文以實戰日誌形式,示範如何用 FromSqlRaw + 參數型別/長度對齊 + CAST + Hint 立即止血,並處理 EF Core 另一個常見錯誤:「CultureInfo 常值被傳進 ToString 造成可能記憶體外洩」。附可直接套用的程式碼。


目錄


錯誤現象

常見堆疊片段如下(節錄):

Oracle.ManagedDataAccess.Client.OracleException (0x80004005): ORA-00600: 內部錯誤代碼, 引數: [kpp_concatq:2], [4], [3], [0], ... at Oracle.EntityFrameworkCore ... ExecuteReaderAsync(...) at Microsoft.EntityFrameworkCore ... ToListAsync(...)

在修正過程中,亦可能遇到 EF Core 自己丟的錯(與 Oracle 無關):

System.InvalidOperationException: The client projection contains a reference to a constant expression of 'System.Globalization.CultureInfo' which is being passed as an argument to the method 'ToString'...

根因分析

  1. Oracle 端(ORA-00600 [kpp_concatq:2]

    • 多與 查詢優化器的 Query Transformation/OR 展開 有關。

    • 實務上常見誘因:N'前段' 這種 NVARCHAR2 字面量 與資料表 VARCHAR2 欄位比較,導致 隱式型別轉換;優化器重寫時碰到某些版本 bug 就炸。

  2. EF Core 端(CultureInfo 常值)

    • ToListAsync() 之前(也就是還在 SQL 端),把 ToString("N0", CultureInfo...)、日期 ToString(...) 之類格式化塞進查詢投影;EF Core 為了避免快取膨脹報錯。


三步驟「立即止血」

Step 1. 改用 FromSqlRaw,不要讓 LINQ 自行產 SQL。
Step 2. 參數化 所有常值(例如 "前段"),並 指定正確 OracleDbType 與長度,同時在 SQL 端 CAST 成與欄位一致的型別(VARCHAR2NVARCHAR2)。
Step 3.SELECT 後加 Oracle Hint/*+ NO_QUERY_TRANSFORMATION */(必要時加 NO_EXPAND)。


安全重構:可直接貼用的程式碼

假設 APS_Z_PLAN.PROCESS_NAMEVARCHAR2(20 CHAR);若為 NVARCHAR2,請見後述 FAQ。

using Oracle.ManagedDataAccess.Client; // ✅ 透過 APS_VERSION 查詢 APSZRESULTC(Oracle 端止血:參數+CAST+Hint) public async Task<List<APSZRESULTC>> GenerateAPSZRESULTC(string apsVersion) { const string sql = @" SELECT /*+ NO_QUERY_TRANSFORMATION */ a.""SEQ_NO"", a.""APS_PLAN_NO"", a.""APS_PLAN_LIST"", a.""SITE"", a.""WAITING_FLAG"", a.""PLANT"", a.""APPL"", a.""CUSTOMER_NAME"", a.""PRODUCT_ID"", a.""ORI_PLAN_WEEK"", a.""ORI_PLAN_DATE"", a.""DEMAND_MONTH"", a.""DEMAND_WEEK"", a.""DEMAND_QTY"", a.""APS_DEAL"", a.""APS_DEAL_QTY"", a.""REMARK"", a.""LATE_QTY"", a.""LATE_WEEK"", a.""APS_PLAN_SEQ"", a.""DISPLACE_PROD_ID"", a.""DISPLACE_QTY"" FROM ""APS_Z_PLAN"" a WHERE a.""APS_VERSION"" = :aps AND a.""PROCESS_NAME"" = CAST(:proc AS VARCHAR2(20 CHAR)) AND a.""APS_PLAN_LIST"" <= :lst ORDER BY a.""APS_VERSION"""; // ✅ 別用過大長度(避免奇怪的隱式轉換/執行路徑) var pAps = new OracleParameter("aps", OracleDbType.Varchar2, 40) { Value = apsVersion }; var pProc = new OracleParameter("proc", OracleDbType.Varchar2, 20) { Value = "前段" }; var pLst = new OracleParameter("lst", OracleDbType.Decimal) { Value = 3 }; // 1) 先取原始資料(不做任何 ToString 格式化) var raw = await _context.APSZPLANs .FromSqlRaw(sql, pAps, pProc, pLst) .AsNoTracking() .ToListAsync(); // 2) remark 一次載入為字典,避免 O(n^2) var lcmDict = await _context.APS_U_LCMREMARK.AsNoTracking() .ToDictionaryAsync(x => x.APS_PLAN_NO, x => x); var mDict = await _context.APS_U_MREMARK.AsNoTracking() .ToDictionaryAsync(x => x.APS_PLAN_NO, x => x); // 3) ToListAsync() 之後才在記憶體端做格式化(數字/日期) var result = raw.Select(z => { var r = new APSZRESULTC { SEQ_NO = z.SEQ_NO, APS_PLAN_NO = z.APS_PLAN_NO, APS_PLAN_LIST = (z.APS_PLAN_LIST ?? 0).ToString("N0"), SITE = z.SITE, WAITING_FLAG = z.WAITING_FLAG, PLANT = z.PLANT, APPL = z.APPL, CUSTOMER_NAME = z.CUSTOMER_NAME, PRODUCT_ID = z.PRODUCT_ID, ORI_PLAN_WEEK = z.ORI_PLAN_WEEK ?? "", ORI_PLAN_DATE = z.ORI_PLAN_DATE.HasValue ? z.ORI_PLAN_DATE.Value.ToString("yyyy-MM-dd") : "", DEMAND_MONTH = z.DEMAND_MONTH, DEMAND_WEEK = z.DEMAND_WEEK, DEMAND_QTY = (z.DEMAND_QTY ?? 0).ToString("N0"), SATISFY_WAY = z.APS_DEAL, DEAL_QTY = (z.APS_DEAL_QTY ?? 0).ToString("N0"), TFT1 = z.REMARK, TFT2 = (z.LATE_QTY ?? 0).ToString("N0"), TFT3 = z.LATE_WEEK, APS_PLAN_SEQ = (z.APS_PLAN_SEQ ?? 0).ToString("N0"), TFT4 = z.DISPLACE_PROD_ID, TFT5 = (z.DISPLACE_QTY ?? 0).ToString("N0"), LCM1 = "", LCM2 = "0", LCM3 = "", LCM4 = "", LCM5 = "0", Material1 = "", Material2 = "0", Material3 = "" }; if (lcmDict.TryGetValue(r.APS_PLAN_NO, out var lcm)) { r.LCM1 = lcm.REMARK ?? ""; r.LCM2 = (lcm.LATE_QTY ?? 0).ToString("N0"); r.LCM3 = lcm.LATE_WEEK; r.LCM4 = lcm.DISPLACE_PROD_ID; r.LCM5 = (lcm.DISPLACE_QTY ?? 0).ToString("N0"); } if (mDict.TryGetValue(r.APS_PLAN_NO, out var mr)) { r.Material1 = mr.REMARK ?? ""; r.Material2 = (mr.LATE_QTY ?? 0).ToString("N0"); r.Material3 = mr.LATE_WEEK ?? ""; } return r; }).ToList(); // 4) 去重(注意:最後要回傳去重後的清單) var deduped = result.GroupBy(r => new { r.SEQ_NO, r.APS_PLAN_NO, r.SITE, r.APPL, r.CUSTOMER_NAME, r.PRODUCT_ID, r.DEMAND_MONTH, r.DEMAND_WEEK, r.DEMAND_QTY, r.SATISFY_WAY, r.DEAL_QTY, r.TFT4, r.TFT5 }) .Select(g => g.First()) .ToList(); return deduped; }

若欄位為 NVARCHAR2:把 CAST(:proc AS VARCHAR2(20 CHAR)) 改成 CAST(:proc AS NVARCHAR2(20)),並把 OracleDbType.Varchar2 改為 OracleDbType.NVarchar2


進階避雷:CTE/MATERIALIZE 與 View 包裝

作法 1:CTE + MATERIALIZE 強制物化

WITH q AS ( SELECT /*+ MATERIALIZE */ ... FROM "APS_Z_PLAN" a WHERE a."APS_VERSION" = :aps AND a."PROCESS_NAME" = CAST(:proc AS VARCHAR2(20 CHAR)) AND a."APS_PLAN_LIST" <= :lst ) SELECT /*+ NO_QUERY_TRANSFORMATION */ * FROM q ORDER BY q."APS_VERSION";

作法 2:建立資料庫 View,Hint 寫死在 View 裡

CREATE OR REPLACE VIEW V_APS_Z_PLAN_FRONT AS SELECT /*+ NO_QUERY_TRANSFORMATION */ ... FROM "APS_Z_PLAN" a;

應用程式就針對 View 查詢,避免每支 SQL 都塞 Hint。


EF Core ToString/CultureInfo 錯誤的正確處理

原則: 所有格式化(千分位、日期字串等)一定放在 ToListAsync() 之後(記憶體端)。
若想同一條鏈上處理,可在查詢後加 .AsEnumerable() 切到客戶端再 Select(...)

var culture = System.Globalization.CultureInfo.InvariantCulture; var rows = await query.AsNoTracking().ToListAsync(); var dto = rows.Select(x => new { AmountText = (x.Amount ?? 0).ToString("N0", culture), DateText = x.Date.HasValue ? x.Date.Value.ToString("yyyy-MM-dd", culture) : "" }).ToList();

版本與長期解法(根治)

  • 讓 DBA 查 v$versionopatch lsinventory,確認是否有對應 RU/PSU 可修正 kpp_concatq 相關已知 Bug。

  • 規劃升到 較新且受支援的 19c/23c RU。

  • 統一字元型別策略:表欄位是 VARCHAR2 就一律用 Varchar2 參數與 CAST;若是 NVARCHAR2 也全線一致。


效能與穩定性小技巧

  • AsNoTracking():查詢純展示資料時,避免建立追蹤圖。

  • 字典快取關聯資料:把 APS_U_LCMREMARKAPS_U_MREMARK 轉成 Dictionary,避免 FirstOrDefault O(n²)。

  • 索引:確保有 (APS_VERSION, PROCESS_NAME, APS_PLAN_LIST) 的覆蓋性索引(或至少前兩列)。

  • 會話層關掉重寫(可選):

    ALTER SESSION SET query_rewrite_enabled = false;

檢查清單(Checklist)

  • FromSqlRaw 取代 LINQ 產生的 SQL。

  • 所有常值 參數化,指定正確 OracleDbType + 長度

  • SQL 端對應欄位 CAST 成一致型別

  • 加上 /*+ NO_QUERY_TRANSFORMATION */(必要時 NO_EXPAND)。

  • ToListAsync() 之後 才做 ToString("N0")、日期格式化。

  • remark 轉字典、避免 O(n²)。

  • 檢查/更新 Oracle RU/PSU;統一字元型別策略。


FAQ

Q1:我的 PROCESS_NAMENVARCHAR2,怎麼改?
A:把參數型別改為 OracleDbType.NVarchar2,並在 SQL 用 CAST(:proc AS NVARCHAR2(實際長度))

Q2:不用 FromSqlRaw 可以嗎?
A:理論上可以,但 LINQ 常會產生 N'...' 字面量或不易控制的查詢轉換。為了穩定,建議改 FromSqlRaw

Q3:只改參數化但不加 Hint 可以嗎?
A:多數情況已足夠,但某些版本仍會在其他重寫路徑踩雷;加上 NO_QUERY_TRANSFORMATION 更保險。


結語

ORA-00600 [kpp_concatq:2] 本質是 Oracle 查詢重寫在特定條件下的內部錯誤。最快速的工程解法就是:FromSqlRaw + 參數/型別對齊 + CAST + Hint。同時,所有資料格式化移到 ToListAsync() 之後,可一併解掉 EF Core 的 CultureInfo 常值錯誤。
把上述模板納入你專案的資料存取慣用法,之後遇到同型問題幾乎都能一次搞定。

留言

這個網誌中的熱門文章

🔍Vue.js 專案錯誤排查:解決 numericFields is not defined 與合併儲存格邏輯最佳化

🛠【ASP.NET Core + Oracle】解決 ORA-00904 "FALSE": 無效的 ID 錯誤與資料欄位動態插入顯示問題