[C#] แนวทางแก้ปัญหา The incoming request has too many parameters. The server supports a maximum of 2100 parameters

น่าจะเป็นเรื่องที่ปวดหัวของ Developer ที่ต้องมาทำงาน DBMS ตระกูล Microsoft SQL Server ครับ เวลาส่ง IList เข้าไป Where ผ่าน ADO.NET / Dapper / EF จะเจอปัญหานี้เหมือนกันครับ

The incoming request has too many parameters. The server supports a maximum of 2100 parameters

NOTE: สำหรับตัว EF ทางผมไม่ได้ใช้งานเป็นหลักนะครับ แต่ลอง Search มาเหมือนมีหลายความเห็นบอกว่าเจอเรื่องนี้ หรือไม่เจอครับ sql - Entity Framework Hitting 2100 Parameter Limit - Stack Overflow

2100 parameters มันของอะไรใน SQL Server

  • หลายคนเคยแบบเวลาเจอ Error นี้ ลองเขียน SQL ตรงๆใน Clause WHERE IN เลย ปรากฏว่ามันำได้
  • จริงๆแล้วข้อจำกัดตรงนี้มาจาก Stored Procedure ของ SQL Server ที่รองรับ Parameter ได้สูงสุด 2100 ตัวครับ ตามนี้ครับ Specify Parameters - SQL Server
  • ที่นี้ตัว Library ที่ Implement อย่าง ADO.NET / Dapper มันใช้เทคนิค Parameterized Queries เบื่องหลังมันไปทำงานในลักษณะ Stored Procedure การเข้าไป Query ข้อมูลให้พวกเราเหมือนกันครับ

แก้ไขอย่างไร

- วิธีที่ 1 ไม่ต้องใช้แล้ว Parameterized Queries ต่อ String SQL เองเลย
  • อย่าหาทำเด็ดขาด ถึงแม้ว่ามันจะ Query ได้จริงๆ แต่มันไม่ป้องกัน SQL Injection นะครับ ที่ย้ายมาใช้ Parameterized Queries มันช่วยกรองเรื่องนี้ด้วยระดับนึงครับ
  • ตัว MS SQL SERVER เองก็มีข้อจำกัดด้วย แม้ว่าจะแก้ปัญหา 2100 แต่จะไปเจอปัญหาใหม่ตรงแถวๆ 32768 จะเจอ Error QUERY_EXPRESSION_TOO_COMPLEX (Msg 8632)
- วิธีที่ 2 ยอม Split ข้อมูล เพื่อมา Where IN แทน
  • วิธีนี้จริงๆ ผมว่าดีนะ ถ้า Query มันซับซ้อน แล้วเราค่อยๆ ปรับ SQL ให้มันแบ่ง Query ที่ละชุด อย่างเช่น Split ที่ละ x ตัว แล้วส่งผลลัพธ์มาให้ App Server ประมวลผลต่อ
  • ความยากของวิธันี้อยู่ที่ตัว x ถ้าตั้งน้อยไปจะมี Round Trip ในการดึงข้อมูลมากขึ้น แต่ถ้ามากไปอาจจะไปช้าที่ DB Server แทนได้

Sample ประมาณนี้มั่ง ไม่ได้เทส

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Text;

public class DatabaseHelper
{
    private readonly string _connectionString;

    public DatabaseHelper(string connectionString)
    {
        _connectionString = connectionString;
    }

    public DataTable ExecuteQuery(string baseQuery, Dictionary<string, List<int>> parameters)
    {
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            connection.Open();

            StringBuilder queryBuilder = new StringBuilder(baseQuery);
            List<SqlParameter> sqlParameters = new List<SqlParameter>();

            foreach (var param in parameters)
            {
                List<string> parameterNames = new List<string>();
                for (int i = 0; i < param.Value.Count; i++)
                {
                    string paramName = $"@{param.Key}{i}";
                    parameterNames.Add(paramName);
                    sqlParameters.Add(new SqlParameter(paramName, param.Value[i]));
                }

                string inClause = string.Join(", ", parameterNames);
                queryBuilder.Replace($"@{param.Key}", inClause);
            }

            string finalQuery = queryBuilder.ToString();
            using (SqlCommand command = new SqlCommand(finalQuery, connection))
            {
                command.Parameters.AddRange(sqlParameters.ToArray());

                using (SqlDataAdapter adapter = new SqlDataAdapter(command))
                {
                    DataTable resultTable = new DataTable();
                    adapter.Fill(resultTable);
                    return resultTable;
                }
            }
        }
    }
}
class Program
{
    static void Main()
    {
        string connectionString = "YourConnectionStringHere";
        DatabaseHelper dbHelper = new DatabaseHelper(connectionString);

        string query = "SELECT * FROM STUDENT WHERE STUDENTID IN (@STUDENTID)";

        Dictionary<string, List<int>> parameters = new Dictionary<string, List<int>>
        {
            { "STUDENTID", new List<int> { 1, 2, 3, 4, 5, /* up to 2100 IDs */ } }
        };

        DataTable result = dbHelper.ExecuteQuery(query, parameters);

        //Your Business Logic Here !!!
    }
}
- วิธีที่ 3 ทำ Table Temp ขึ้นมา ยัด KEY และไป Where ต่อ
  • ง่ายๆ ทำ Table แนวๆ Temp Lookup มาเลย มีอยู่ 1 Column
  • เริ่มจากตอนแรก ยัด Key ลงไปก่อน
  • ใน Query หลัก เปลี่ยน Query เป็น WHERE PERFIDX IN (SELECT IDX FROM TEMPLKP) แทนครับ ลองดู Query จะเข้าได้มากขึ้นครับ
-- แบบเดิม
SELECT * FROM Orders WHERE CustomerID IN (@p1, @p2, ..., @p2101) 

-- แบบใหม่ subquery มาดึงจาก Table Temp DSCORE_TEMP_CRITERIA 
SELECT * FROM Orders
WHERE CustomerID IN (SELECT DATAVALUE FROM DSCORE_TEMP_CRITERIA 
                     WHERE UNIQUEID = @dataid AND DATAKEY = @datakey)
  • เมื่อได้ผลลัพธ์แล้ว Clear Table Temp ออก
  • ใช้ในเคสที่ Query เดิม เพราะ
    • ยากซับซ้อนแบบว่าไม่มีเวลามาระเบิดมัน แต่ต้องเสียเวลามาคุม State ของ Table Temp Lookup
    • ถ้าเป็น String ผมว่ามันมีแอบเสี่ยงโดน SQL INJECTION อยู่ดีครับ
    • อาจจะเจอปัญหาเคส Concurrent ตรง Table Temp

ต่อไปตัวอย่าง psudo code SQL Server จริงอาจจะต้องปรับสำหรับ DB2 / Postgresql เน้น Logic นะ ใช้กับ ADO.NET

  • Code ของ Table Temp
using Microsoft.Data.SqlClient;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading;

namespace dsCore.DAO.ADO.Helper
{
    public static class TempTableHelper
    {
        // ===== Connection String =====
        private static string _connectionString = "Server=YOUR_SERVER;Database=YOUR_DB;User Id=YOUR_USER;Password=YOUR_PASSWORD;TrustServerCertificate=True;";

        public static void SetConnectionString(string connectionString)
        {
            _connectionString = connectionString;
        }

        // ===== Insert Overloads =====
        public static string InsertIntValue(string pComputerName, string pTempTable, string pDataKey, IList<int> pValue)
        {
            string uniqueId = Thread.CurrentThread.ManagedThreadId + "_" + pComputerName;
            InsertValue(pTempTable, uniqueId, pDataKey, pValue.Select(s => (string.Empty, (int?)s, (int?)null, (int?)null, (int?)null)));
            return uniqueId;
        }

        public static string InsertValue(string pComputerName, string pTempTable, string pDataKey, IList<(int, int)> pValue)
        {
            string uniqueId = Thread.CurrentThread.ManagedThreadId + "_" + pComputerName;
            InsertValue(pTempTable, uniqueId, pDataKey, pValue.Select(s => (string.Empty, (int?)s.Item1, (int?)s.Item2, (int?)null, (int?)null)));
            return uniqueId;
        }

        public static string InsertValue(string pComputerName, string pTempTable, string pDataKey, IList<(int, int, int)> pValue)
        {
            string uniqueId = Thread.CurrentThread.ManagedThreadId + "_" + pComputerName;
            InsertValue(pTempTable, uniqueId, pDataKey, pValue.Select(s => (string.Empty, (int?)s.Item1, (int?)s.Item2, (int?)s.Item3, (int?)null)));
            return uniqueId;
        }

        public static string InsertValue(string pComputerName, string pTempTable, string pDataKey, IList<(int, int, int, int)> pValue)
        {
            string uniqueId = Thread.CurrentThread.ManagedThreadId + "_" + pComputerName;
            InsertValue(pTempTable, uniqueId, pDataKey, pValue.Select(s => (string.Empty, (int?)s.Item1, (int?)s.Item2, (int?)s.Item3, (int?)s.Item4)));
            return uniqueId;
        }

        public static string InsertValue(string pComputerName, string pTempTable, string pDataKey, IList<string> pValue)
        {
            string uniqueId = Thread.CurrentThread.ManagedThreadId + "_" + pComputerName;
            InsertValue(pTempTable, uniqueId, pDataKey, pValue.Select(s => (s, (int?)null, (int?)null, (int?)null, (int?)null)));
            return uniqueId;
        }

        public static string InsertValue(string pComputerName, string pTempTable, string pDataKey, IList<(string, int)> pValue)
        {
            string uniqueId = Thread.CurrentThread.ManagedThreadId + "_" + pComputerName;
            InsertValue(pTempTable, uniqueId, pDataKey, pValue.Select(s => (s.Item1, (int?)s.Item2, (int?)null, (int?)null, (int?)null)));
            return uniqueId;
        }

        public static string InsertValue(string pComputerName, string pTempTable, string pDataKey, IList<(string, int, int)> pValue)
        {
            string uniqueId = Thread.CurrentThread.ManagedThreadId + "_" + pComputerName;
            InsertValue(pTempTable, uniqueId, pDataKey, pValue.Select(s => (s.Item1, (int?)s.Item2, (int?)s.Item3, (int?)null, (int?)null)));
            return uniqueId;
        }

        public static string InsertValue(string pComputerName, string pTempTable, string pDataKey, IList<(string, int, int, int)> pValue)
        {
            string uniqueId = Thread.CurrentThread.ManagedThreadId + "_" + pComputerName;
            InsertValue(pTempTable, uniqueId, pDataKey, pValue.Select(s => (s.Item1, (int?)s.Item2, (int?)s.Item3, (int?)s.Item4, (int?)null)));
            return uniqueId;
        }

        public static string InsertValue(string pComputerName, string pTempTable, string pDataKey, IList<(string, int, int, int, int)> pValue)
        {
            string uniqueId = Thread.CurrentThread.ManagedThreadId + "_" + pComputerName;
            InsertValue(pTempTable, uniqueId, pDataKey, pValue.Select(s => (s.Item1, (int?)s.Item2, (int?)s.Item3, (int?)s.Item4, (int?)s.Item5)));
            return uniqueId;
        }

        // ===== Constants =====
        public const string TEMP_CRITERIA    = "DSCORE_TEMP_CRITERIA";
        public const string SQL_GET_INTDATA  = "(SELECT CRITERIA.DATAVALUE     FROM DSCORE_TEMP_CRITERIA CRITERIA WHERE CRITERIA.UNIQUEID = @dataid AND CRITERIA.DATAKEY = @datakey)";
        public const string SQL_GET_INT1DATA = "(SELECT CRITERIA.DATAVALUE1    FROM DSCORE_TEMP_CRITERIA CRITERIA WHERE CRITERIA.UNIQUEID = @dataid AND CRITERIA.DATAKEY = @datakey)";
        public const string SQL_GET_INT2DATA = "(SELECT CRITERIA.DATAVALUE2    FROM DSCORE_TEMP_CRITERIA CRITERIA WHERE CRITERIA.UNIQUEID = @dataid AND CRITERIA.DATAKEY = @datakey)";
        public const string SQL_GET_INT3DATA = "(SELECT CRITERIA.DATAVALUE3    FROM DSCORE_TEMP_CRITERIA CRITERIA WHERE CRITERIA.UNIQUEID = @dataid AND CRITERIA.DATAKEY = @datakey)";
        public const string SQL_GET_STRDATA  = "(SELECT CRITERIA.DATAVALUESTR  FROM DSCORE_TEMP_CRITERIA CRITERIA WHERE CRITERIA.UNIQUEID = @dataid AND CRITERIA.DATAKEY = @datakey)";

        public const string UNIQUEID      = "UniqueID";
        public const string DATAKEY       = "DataKey";
        public const string CREATETIME    = "CreateDate";
        public const string DATAVALUESTR  = "DataValueStr";
        public const string DATAVALUE     = "DataValue";
        public const string DATAVALUE1    = "DataValue1";
        public const string DATAVALUE2    = "DataValue2";
        public const string DATAVALUE3    = "DataValue3";

        // ===== Core Insert =====
        private static void InsertValue(
            string pTempTable,
            string pUniqueId,
            string pDataKey,
            IEnumerable<(string, int?, int?, int?, int?)> pValues)
        {
            DateTime dt = DateTime.Now;

            // Build DataTable
            DataTable tbl = new DataTable();
            tbl.Columns.Add(new DataColumn(UNIQUEID,     typeof(string)));
            tbl.Columns.Add(new DataColumn(DATAKEY,      typeof(string)));
            tbl.Columns.Add(new DataColumn(CREATETIME,   typeof(DateTime)));
            tbl.Columns.Add(new DataColumn(DATAVALUESTR, typeof(string)));
            tbl.Columns.Add(new DataColumn(DATAVALUE,    typeof(int)));
            tbl.Columns.Add(new DataColumn(DATAVALUE1,   typeof(int)));
            tbl.Columns.Add(new DataColumn(DATAVALUE2,   typeof(int)));
            tbl.Columns.Add(new DataColumn(DATAVALUE3,   typeof(int)));

            foreach (var item in pValues)
            {
                DataRow dr = tbl.NewRow();
                dr[UNIQUEID]     = pUniqueId;
                dr[DATAKEY]      = pDataKey;
                dr[CREATETIME]   = dt;
                dr[DATAVALUESTR] = !string.IsNullOrEmpty(item.Item1) ? (object)item.Item1 : DBNull.Value;
                dr[DATAVALUE]    = item.Item2.HasValue ? (object)item.Item2.Value : DBNull.Value;
                dr[DATAVALUE1]   = item.Item3.HasValue ? (object)item.Item3.Value : DBNull.Value;
                dr[DATAVALUE2]   = item.Item4.HasValue ? (object)item.Item4.Value : DBNull.Value;
                dr[DATAVALUE3]   = item.Item5.HasValue ? (object)item.Item5.Value : DBNull.Value;
                tbl.Rows.Add(dr);
            }

            // BulkCopy ด้วย SqlConnection จริงๆ
            using (SqlConnection conn = new SqlConnection(_connectionString))
            {
                conn.Open();
                using (SqlBulkCopy bulk = new SqlBulkCopy(conn))
                {
                    bulk.DestinationTableName = pTempTable;
                    bulk.ColumnMappings.Add(UNIQUEID,     UNIQUEID);
                    bulk.ColumnMappings.Add(DATAKEY,      DATAKEY);
                    bulk.ColumnMappings.Add(CREATETIME,   CREATETIME);
                    bulk.ColumnMappings.Add(DATAVALUESTR, DATAVALUESTR);
                    bulk.ColumnMappings.Add(DATAVALUE,    DATAVALUE);
                    bulk.ColumnMappings.Add(DATAVALUE1,   DATAVALUE1);
                    bulk.ColumnMappings.Add(DATAVALUE2,   DATAVALUE2);
                    bulk.ColumnMappings.Add(DATAVALUE3,   DATAVALUE3);
                    bulk.WriteToServer(tbl);
                }
            }
        }

        // ===== Delete =====
        public static void DeleteValue(string pUniqueId, string pDataKey)
        {
            const string sql = "DELETE FROM DSCORE_TEMP_CRITERIA WHERE UNIQUEID = @UniqueID AND DATAKEY = @DataKey";
            using (SqlConnection conn = new SqlConnection(_connectionString))
            using (SqlCommand cmd = new SqlCommand(sql, conn))
            {
                cmd.Parameters.AddWithValue("@UniqueID", pUniqueId);
                cmd.Parameters.AddWithValue("@DataKey",  pDataKey);
                conn.Open();
                cmd.ExecuteNonQuery();
            }
        }

        public static void DeleteAll(string pUniqueId)
        {
            const string sql = "DELETE FROM DSCORE_TEMP_CRITERIA WHERE UNIQUEID = @UniqueID";
            using (SqlConnection conn = new SqlConnection(_connectionString))
            using (SqlCommand cmd = new SqlCommand(sql, conn))
            {
                cmd.Parameters.AddWithValue("@UniqueID", pUniqueId);
                conn.Open();
                cmd.ExecuteNonQuery();
            }
        }

        // ===== Cleanup เก่า (Table จะได้ไม่บวม) =====
        public static void DeleteExpired(int olderThanMinutes = 60)
        {
            const string sql = "DELETE FROM DSCORE_TEMP_CRITERIA WHERE CreateDate < @Cutoff";
            using (SqlConnection conn = new SqlConnection(_connectionString))
            using (SqlCommand cmd = new SqlCommand(sql, conn))
            {
                cmd.Parameters.AddWithValue("@Cutoff", DateTime.Now.AddMinutes(-olderThanMinutes));
                conn.Open();
                cmd.ExecuteNonQuery();
            }
        }

        // ===== Helper =====
        public static string GenerateDataKey(string pName)
        {
            return Guid.NewGuid().ToString() + "-" + pName;
        }
    }
}
  • Code ตอน Run
string dataKey = TempTableHelper.GenerateDataKey("CustomerFilter");
string uniqueId = TempTableHelper.InsertIntValue(computerName, TempTableHelper.TEMP_CRITERIA, dataKey, customerIds);

string sql = $@"
    SELECT * FROM Orders
    WHERE CustomerID IN {TempTableHelper.SQL_GET_INTDATA}";

using (SqlConnection conn = new SqlConnection(connectionString))
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
    cmd.Parameters.AddWithValue("@dataid",  uniqueId);
    cmd.Parameters.AddWithValue("@datakey", dataKey);
    conn.Open();
    // execute...
}
- วิธีที่ 4 ถ้าใช้ Stored Procedure ใช้ MSSQL Table-Valued parameters สิ
  • Table-Valued parameters ของ SQL SERVER มาแก้ปัญหาเรื่องนี้แหละครับ
  • แต่ถ้าทำไปแล้ว พวกที่ระบบต่อกับ DBMS หลายๆค่ายได้ อาจจะต้องมาประเมิน ว่ามันคุ้มไหมที่จะต้องแยก Class ใน Level ของ ADO ในแต่ละ DBMS
  • หรือจะ Apply Table-Valued parameters กับเคสอื่นๆดูครับ

สรุป

  • ทีมเลือกวิธีที่ 2 เพราะปัญหาที่เจอส่วนใหญ่จะเป็นประเภท WHERE IN มากกว่าที่จะไปเจอปัญหาของ Stored Procedure ถ้าไปใช้ Table-Valued parameters มันระบบอาจจะยีดติดกับกับ DBMS ค่ายใดค่ายหนึ่งมากไปครับ และแก้ได้เร็วสำหรับเคสไฟไหมต้นช่วงต้นปี 2021
  • ส่วนวิธีที่ 3 มันต้องมาจัดการเคสซวยเพื่อด้วยมี 2 request มาใช้ SQL ชุดนี้พร้อมกัน อันนี้ถ้าจัดการได้ แล้วทำเป็น Framework จะดีมากครับ
  • และนอกจาก C# แล้ว เหมือนภาษาอื่นๆ จะเจอกัน สามารถนำเอาแนวทางไปประยุกต์ใช้ได้เช่นกันครับ

อ๋อพวก EF เหมือนมีคน Apply ประมาณนี้มั้ง https://www.tabsoverspaces.com/233644-playing-with-parameters-limit-on-sql-server-with-entity-framework

Reference


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts sent to your email.