SQL巨大的ID选择 - 如何让它更快?

时间:2010-02-02 07:58:46

标签: c# sql-server performance

我有一个包含大量ID的数组,我想从数据库中选择。

通常的做法是select blabla from xxx where yyy IN (ids) OPTION (RECOMPILE)。 (需要选项重新编译,因为SQL服务器不够智能,无法看到将此查询放入其查询缓存中是一个巨大的内存浪费)

然而,当ID数量很高时,SQL Server在这种类型的查询中非常糟糕,它使用的解析器太慢了。 让我举个例子:

SELECT * FROM table WHERE id IN (288525, 288528, 288529,<about 5000 ids>, 403043, 403044) OPTION (RECOMPILE)

执行时间: ~1100 毫秒(在我的示例中返回appx 200行)

对战:

SELECT * FROM table WHERE id BETWEEN 288525 AND 403044 OPTION (RECOMPILE)

执行时间: ~80 毫秒(在我的示例中返回appx 50000行)

所以尽管我得到的数据还要多250倍,但执行速度要快14倍......

所以我构建了这个函数来获取我的id列表并构建一些能够在两者之间返回合理折衷的东西(这些东西不会返回250倍的数据,但仍然可以更快地解析查询)

  private const int MAX_NUMBER_OF_EXTRA_OBJECTS_TO_FETCH = 5;
  public static string MassIdSelectionStringBuilder(
       List<int> keys, ref int startindex, string colname)
  {
     const int maxlength = 63000;
     if (keys.Count - startindex == 1)
     {
        string idstring = String.Format("{0} = {1}", colname, keys[startindex]);
        startindex++;
        return idstring;
     }
     StringBuilder sb = new StringBuilder(maxlength + 1000);
     List<int> individualkeys = new List<int>(256);
     int min = keys[startindex++];
     int max = min;
     sb.Append("(");
     const string betweenAnd = "{0} BETWEEN {1} AND {2}\n";
     for (; startindex < keys.Count && sb.Length + individualkeys.Count * 8 < maxlength; startindex++)
     {
        int key = keys[startindex];
        if (key > max+MAX_NUMBER_OF_EXTRA_OBJECTS_TO_FETCH)
        {
           if (min == max)
              individualkeys.Add(min);
           else
           {
              if(sb.Length > 2)
                 sb.Append(" OR ");
              sb.AppendFormat(betweenAnd, colname, min, max);
           }
           min = max = key;
        }
        else
        {
           max = key;
        }
     }
     if (min == max)
        individualkeys.Add(min);
     else
     {
        if (sb.Length > 2)
           sb.Append(" OR ");
        sb.AppendFormat(betweenAnd, colname, min, max);
     }
     if (individualkeys.Count > 0)
     {
        if (sb.Length > 2)
           sb.Append(" OR ");
        string[] individualkeysstr = new string[individualkeys.Count];
        for (int i = 0; i < individualkeys.Count; i++)
           individualkeysstr[i] = individualkeys[i].ToString();
        sb.AppendFormat("{0} IN ({1})", colname,  String.Join(",",individualkeysstr));
     }
     sb.Append(")");
     return sb.ToString();
  }

然后使用它:

 List<int> keys; //Sort and make unique
 ...
 for (int i = 0; i < keys.Count;)
 {
    string idstring = MassIdSelectionStringBuilder(keys, ref i, "id");
    string sqlstring = string.Format("SELECT * FROM table WHERE {0} OPTION (RECOMPILE)", idstring);

然而,我的问题是...... 有没有人知道更好/更快/更聪明的方法呢?

7 个答案:

答案 0 :(得分:2)

根据我的经验,最快的方法是将二进制格式的数字打包成图像。我发送了多达100K的ID,效果很好:

Mimicking a table variable parameter with an image

还有一段时间以前。 Erland Sommarskog的以下文章是最新的:

Arrays and Lists in SQL Server

答案 1 :(得分:1)

如果ID列表在另一个被索引的表中,那么使用简单的INNER JOIN

可以更快地执行

如果不可能,那么尝试创建一个像这样的TABLE变量

DECLARE @tTable TABLE
(
   @Id int
)

首先将ids存储在表变量中,然后将INNER JOIN存储到表xxx中,我使用此方法取得了有限的成功,但值得一试

答案 2 :(得分:1)

您正在使用(key > max+MAX_NUMBER_OF_EXTRA_OBJECTS_TO_FETCH)作为检查来确定是否进行范围提取而不是单独提取。看起来这不是最好的方法。

让我们考虑4个ID序列{2,7},{2,8},{1,2,7}和{1,2,8}。 他们转化为

ID BETWEEN 2 AND 7
ID ID in (2, 8)
ID BETWEEN 1 AND 7 
ID BETWEEN 1 AND 2 OR ID in (8)

现在,获取和过滤ID 3-6的决定仅取决于2和7/8之间的差异。但是,它不考虑2是否已经是范围或个人ID的一部分。

我认为正确的标准是您节省了多少个人ID。将两个人转换为范围删除具有2 * Cost(Individual) - Cost(range)的净收益,而扩展范围具有Cost(individual) - Cost(range extension)的净收益。

答案 3 :(得分:0)

添加重新编译不是一个好主意。预编译意味着sql不保存您的查询结果,但它保存了执行计划。从而试图使查询更快。如果添加重新编译,那么它将始终具有编译查询的开销。尝试创建存储过程并保存查询并从那里调用它。由于存储过程始终是预编译的。

答案 4 :(得分:0)

与Neils类似的另一个肮脏的想法,

  • 根据您的业务条件建立一个仅包含ID的索引视图
  • 您可以使用实际表格加入视图并获得所需的结果。

答案 5 :(得分:0)

这样做的有效方法是:

  1. 创建一个临时表来保存ID
  2. 使用包含所有以逗号分隔的ID
  3. 的字符串参数调用SQL存储过程
  4. SQL存储过程使用带有CHARINDEX()的循环查找每个逗号,然后使用SUBSTRING在两个逗号和CONVERT之间提取字符串以使其成为int,并使用INSERT INTO @Temporary VALUES ...将其插入到临时表
  5. INNER JOIN临时表或在IN(来自@Temporary的SELECT ID)子查询中使用它
  6. 这些步骤中的每一步都非常快,因为传递了一个字符串,在循环期间没有进行编译,并且除了实际的id值之外没有创建子字符串。

    只要将大字符串作为参数传递,就不会重新编译。

    请注意,在循环中,您必须在两个单独的值中跟踪先前和当前逗号

答案 6 :(得分:0)

在这里关闭袖口 - 是否合并了派生表帮助表现?我没有设置完全测试,只是想知道这是否会优化使用之间,然后过滤掉不需要的行:

Select * from 
( SELECT *
  FROM dbo.table 
  WHERE ID between <lowerbound> and <upperbound>) as range
where ID in ( 
    1206,
    1207,
    1208,
    1209,
    1210,
    1211,
    1212,
    1213,
    1214,
    1215,
    1216,
    1217,
    1218,
    1219,
    1220,
    1221,
    1222,
    1223,
    1224,
    1225,
    1226,
    1227,
    1228,
    <...>,
    1230,
    1231
)