【问题标题】:How can I use more than 2100 values in an IN clause using Dapper?如何使用 Dapper 在 IN 子句中使用超过 2100 个值?
【发布时间】:2016-09-20 11:00:24
【问题描述】:

我有一个包含 ID 的列表,我想使用 Dapper 将其插入到临时表中,以避免“IN”子句中参数的 SQL 限制。

所以目前我的代码如下所示:

public IList<int> LoadAnimalTypeIdsFromAnimalIds(IList<int> animalIds)
{
    using (var db = new SqlConnection(this.connectionString))
    {
        return db.Query<int>(
            @"SELECT a.animalID        
            FROM
            dbo.animalTypes [at]
            INNER JOIN animals [a] on a.animalTypeId = at.animalTypeId
            INNER JOIN edibleAnimals e on e.animalID = a.animalID
            WHERE
            at.animalId in @animalIds", new { animalIds }).ToList();
    }
}

我需要解决的问题是,当animalIds列表中的id超过2100个时,我得到一个SQL错误“传入的请求参数太多。服务器最多支持2100个参数”。

所以现在我想创建一个临时表,其中填充了传递给方法的 animalIds。然后我可以在临时表上加入动物表,避免出现巨大的“IN”子句。

我尝试了各种语法组合,但没有成功。 这就是我现在的位置:

public IList<int> LoadAnimalTypeIdsFromAnimalIds(IList<int> animalIds)
{
    using (var db = new SqlConnection(this.connectionString))
    {
        db.Execute(@"SELECT INTO #tempAnmialIds @animalIds");

        return db.Query<int>(
            @"SELECT a.animalID        
            FROM
            dbo.animalTypes [at]
            INNER JOIN animals [a] on a.animalTypeId = at.animalTypeId
            INNER JOIN edibleAnimals e on e.animalID = a.animalID
            INNER JOIN #tempAnmialIds tmp on tmp.animalID = a.animalID).ToList();
    }
}

我无法让 SELECT INTO 与 ID 列表一起使用。我是不是走错了路,也许有更好的方法来避免“IN”子句限制。

我确实有一个备份解决方案,我可以将传入的 animalID 列表拆分为 1000 个块,但我读到大的“IN”子句会受到性能影响,加入临时表会更有效率,而且也意味着我不需要额外的“拆分”代码来将 id 分成 1000 个块。

【问题讨论】:

  • INSERT INTO 语句在哪里?
  • 2100 个值是一个非常大的数字。足够大,应该将它们存储在具有适当索引的 自己的 表中。这些价值观从何而来?它们是查询的结果吗?那么为什么不包含在 SELECT 语句中呢?还是它们来自外部来源,例如 CSV 文件?那么最好的选择是将文件导入临时表,然后加入该表
  • 它们来自 CSV
  • 然后将它们导入一个表并加入它,例如使用bcpBULK INSERT 或SSIS。您还可以使用SqlBulkCopy 类将行从客户端批量发送到服务器。

标签: c# sql-server dapper


【解决方案1】:

好的,这是您想要的版本。我将此添加为单独的答案,因为我使用 SP/TVP 的第一个答案使用了不同的概念。

public IList<int> LoadAnimalTypeIdsFromAnimalIds(IList<int> animalIds)
{
  using (var db = new SqlConnection(this.connectionString))
  {
    // This Open() call is vital! If you don't open the connection, Dapper will
    // open/close it automagically, which means that you'll loose the created
    // temp table directly after the statement completes.
    db.Open();

    // This temp table is created having a primary key. So make sure you don't pass
    // any duplicate IDs
    db.Execute("CREATE TABLE #tempAnimalIds(animalId int not null primary key);");
    while (animalIds.Any())
    {
      // Build the statements to insert the Ids. For this, we need to split animalIDs
      // into chunks of 1000, as this flavour of INSERT INTO is limited to 1000 values
      // at a time.
      var ids2Insert = animalIds.Take(1000);
      animalIds = animalIds.Skip(1000).ToList();

      StringBuilder stmt = new StringBuilder("INSERT INTO #tempAnimalIds VALUES (");
      stmt.Append(string.Join("),(", ids2Insert));
      stmt.Append(");");

      db.Execute(stmt.ToString());
    }

    return db.Query<int>(@"SELECT animalID FROM #tempAnimalIds").ToList();
  }
}

测试:

var ids = LoadAnimalTypeIdsFromAnimalIds(Enumerable.Range(1, 2500).ToList());

您只需将您的 select 语句修改为原来的样子。由于我的环境中没有您的所有表,因此我只是从创建的临时表中选择以证明它可以正常工作。

陷阱,见 cmets:

  • 一开始就打开连接,否则临时表会 dapper 自动关闭连接后消失 创建表。
  • INSERT INTO 的这种特殊风味是有限的 一次到 1000 个值,因此需要将传递的 ID 拆分为 相应地分块。
  • 不要传递重复的键,因为临时表上的主键不允许这样做。

编辑

似乎 Dapper 支持基于集合的操作,这也将使其工作:

public IList<int> LoadAnimalTypeIdsFromAnimalIdsV2(IList<int> animalIds)
{
  // This creates an IEnumerable of an anonymous type containing an Id property. This seems
  // to be necessary to be able to grab the Id by it's name via Dapper.
  var namedIDs = animalIds.Select(i => new {Id = i});
  using (var db = new SqlConnection(this.connectionString))
  {
    // This is vital! If you don't open the connection, Dapper will open/close it
    // automagically, which means that you'll loose the created temp table directly
    // after the statement completes.
    db.Open();

    // This temp table is created having a primary key. So make sure you don't pass
    // any duplicate IDs
    db.Execute("CREATE TABLE #tempAnimalIds(animalId int not null primary key);");

    // Using one of Dapper's convenient features, the INSERT becomes:
    db.Execute("INSERT INTO #tempAnimalIds VALUES(@Id);", namedIDs);

    return db.Query<int>(@"SELECT animalID FROM #tempAnimalIds").ToList();
  }
}

我不知道这与以前的版本相比性能如何(即 2500 个单插入而不是三个插入,每个插入有 1000、1000、500 个值)。但是文档建议如果与 async、MARS 和 Pipelining 一起使用,它的性能会更好。

【讨论】:

  • 由于索引和统计,这个版本的性能会好很多。 TVP 没有索引,优化器假定它们只包含一行。 IN 子句也比 TVP 快
  • @PanagiotisKanavos 我一直认为,如果您创建带有索引或 PK 的表类型,那将用于 TVP。不是这样吗?
  • SQL Server doesn't maintain statistics 用于 TVP,这会导致错误的基数估计,即优化器不知道有多少唯一值。即使您有主键,这也可能导致执行计划效率低下。 > 2100 行就足够了,我首先要进行基准测试。虽然,> 2100 行我宁愿使用适当的临时表来导入和处理源数据
  • 谢谢,这是我正在努力解决的问题。我现在可以看到我应该怎么做。感谢您的帮助。
  • 仅供参考,如果您使用 OPTION(RECOMPILE) 或跟踪标志 2453,将对表变量使用正确的基数估计。例如:sqlperformance.com/2014/06/t-sql-queries/…
【解决方案2】:

在您的示例中,我看不到您的 animalIds 列表实际上是如何传递给要插入到 #tempAnimalIDs 表中的查询的。

有一种方法可以不使用临时表,而是使用带有表值参数的存储过程。

SQL:

CREATE TYPE [dbo].[udtKeys] AS TABLE([i] [int] NOT NULL)
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[myProc](@data as dbo.udtKeys readonly)AS
BEGIN
    select i from @data;
END
GO

这将创建一个名为udtKeys 的用户定义表类型,其中仅包含一个名为i 的int 列,以及一个需要该类型参数的存储过程。 proc 除了选择您传递的 ID 之外什么也不做,但您当然可以将其他表加入其中。有关语法的提示,see here

C#:

var dataTable = new DataTable();
dataTable.Columns.Add("i", typeof(int));
foreach (var animalId in animalIds)
    dataTable.Rows.Add(animalId);
using(SqlConnection conn = new SqlConnection("connectionString goes here"))
{
    var r=conn.Query("myProc", new {data=dataTable},commandType: CommandType.StoredProcedure);
    // r contains your results
}

过程中的参数通过传递一个 DataTable 来填充,并且该 DataTable 的结构必须与您创建的表类型之一匹配。

如果您确实需要传递超过 2100 个值,您可能需要考虑为您的表类型建立索引以提高性能。如果你不传递任何重复的键,你实际上可以给它一个主键,像这样:

CREATE TYPE [dbo].[udtKeys] AS TABLE(
    [i] [int] NOT NULL,
    PRIMARY KEY CLUSTERED 
    (
        [i] ASC
    )WITH (IGNORE_DUP_KEY = OFF)
)
GO

您可能还需要为执行此操作的数据库用户分配该类型的执行权限,如下所示:

GRANT EXEC ON TYPE::[dbo].[udtKeys] TO [User]
GO

另请参阅 herehere

【讨论】:

  • 感谢您,尽管我并不是真的想创建一个 SP 或对数据库进行任何更改。我不完全确定您提供的这两个链接是什么意思,我没有执行权限问题。主要问题是形成一种使用 Dapper 将普通 INT 值列表插入到表中的方法
  • @ChrisB 这两个链接只是说明了一个事实,如果你走SP路线,你可能需要对你创建的类型授予exec权限,如果你不知道,这并不明显.大多数人认为执行 SP 的权限就足够了,但事实并非如此。
  • 嗯,这是一个有用的问题,谢谢
【解决方案3】:

对我来说,我能想到的最好方法是在 C# 中将列表转换为逗号分隔的列表,然后在 SQL 中使用 string_split 将数据插入临时表。这可能有上限,但就我而言,我只处理了 6,000 条记录,而且运行速度非常快。

public IList<int> LoadAnimalTypeIdsFromAnimalIds(IList<int> animalIds)
{
    using (var db = new SqlConnection(this.connectionString))
    {
        return db.Query<int>(
            @"  --Created a temp table to join to later. An index on this would probably be good too.
                CREATE TABLE #tempAnimals (Id INT)
                INSERT INTO #tempAnimals (ID)
                SELECT value FROM string_split(@animalIdStrings)

                SELECT at.animalTypeID        
                FROM dbo.animalTypes [at]
                JOIN animals [a] ON a.animalTypeId = at.animalTypeId
                JOIN #tempAnimals temp ON temp.ID = a.animalID -- <-- added this
                JOIN edibleAnimals e ON e.animalID = a.animalID", 
            new { animalIdStrings = string.Join(",", animalIds) }).ToList();
    }
}

值得注意的是,string_split 仅适用于 SQL Server 2016 或更高版本,或者如果使用 Azure SQL,则兼容模式 130 或更高版本。 https://docs.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql?view=sql-server-ver15

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-11-20
    • 1970-01-01
    • 2011-01-24
    • 2013-11-24
    • 2018-06-08
    • 2016-11-26
    • 2013-02-19
    相关资源
    最近更新 更多