SQL огромный выбор идентификаторов - Как сделать это быстрее? - PullRequest
2 голосов
/ 02 февраля 2010

У меня есть массив с огромным количеством идентификаторов, которые я бы хотел выбрать из БД.

Обычный подход - сделать select blabla from xxx where yyy IN (ids) OPTION (RECOMPILE).(Параметр перекомпилировать необходим, поскольку сервер SQL недостаточно интеллектуален, чтобы видеть, что помещение этого запроса в кэш запросов - это огромная трата памяти)

Однако SQL Server ужасенЭтот тип запроса, когда количество идентификаторов велико, анализатор, который он использует, просто слишком медленно.Позвольте мне привести пример:

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

Время выполнения: ~ 1100 мсек (в моем примере это возвращает приблизительно 200 строк)

Versus:

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

Время выполнения: ~ 80 мсек (в моем примере это возвращает приблизительно 50000 строк)

Так что, хотя я получаю в 250 раз больше данных, он выполняется в 14 раз быстрее...

Итак, я построил эту функцию, чтобы взять мой список идентификаторов и создать что-то, что будет возвращать разумный компромисс между ними (то, что не возвращает в 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 ]

2 голосов
/ 02 февраля 2010

По моему опыту, самым быстрым способом было упаковать числа в двоичном формате в изображение. Я отправлял до 100K идентификаторов, что прекрасно работает:

Имитация параметра табличной переменной с изображением

Еще не так давно. Следующие статьи Эрланда Соммарскога актуальны:

Массивы и списки в SQL Server

1 голос
/ 02 февраля 2010

Вы используете (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)

Решение о выборке и фильтрации идентификаторов 3-6 теперь зависит только от разницы между 2 и 7/8. Однако здесь не учитывается, является ли 2 частью диапазона или отдельным идентификатором.

Я думаю, правильным критерием является количество сохраненных вами индивидуальных идентификаторов. Преобразование двух лиц в диапазон удалений дает чистую выгоду 2 * Cost(Individual) - Cost(range), тогда как расширение диапазона дает чистую выгоду Cost(individual) - Cost(range extension).

1 голос
/ 02 февраля 2010

Если бы список идентификаторов находился в другой таблице, которая была проиндексирована, он бы выполнялся намного быстрее при использовании простого INNER JOIN

если это невозможно, попробуйте создать переменную TABLE, например,

DECLARE @tTable TABLE
(
   @Id int
)

сначала сохраните идентификаторы в табличной переменной, затем INNER JOIN к вашей таблице xxx, у меня был ограниченный успех с этим методом, но стоит попробовать

0 голосов
/ 02 февраля 2010

Скажи здесь - поможет ли вообще использование производной таблицы? Я не настроен на полное тестирование, просто интересно, оптимизирует ли это использование между ними, а затем отфильтровать ненужные строки:

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
)
0 голосов
/ 02 февраля 2010

Эффективный способ сделать это:

  1. Создать временную таблицу для хранения идентификаторов
  2. Вызов хранимой процедуры SQL со строковым параметром, содержащим все разделенные запятыми идентификаторы
  3. Хранимая процедура SQL использует цикл с CHARINDEX (), чтобы найти каждую запятую, затем SUBSTRING, чтобы извлечь строку между двумя запятыми, и CONVERT, чтобы сделать его целым, и использовать INSERT INTO @Teorary VALUES ..., чтобы вставить его в временная таблица
  4. INNER Присоединиться к временной таблице или использовать ее в подзапросе IN (SELECT ID из @Teorary)

Каждый из этих шагов является чрезвычайно быстрым, поскольку передается одна строка, во время цикла не выполняется компиляция и не создаются подстроки, кроме фактических значений идентификатора.

Никакая перекомпиляция вообще не выполняется, если она выполняется, пока большая строка передается в качестве параметра.

Обратите внимание, что в цикле вы должны отслеживать предыдущую и текущую запятую в двух отдельных значениях

0 голосов
/ 02 февраля 2010

Еще одна грязная идея, похожая на Нилса,

  • Иметь индексированное представление, содержащее только идентификаторы в зависимости от состояния вашего бизнеса
  • И вы можете объединить представление с вашей фактической таблицей и получить желаемый результат.
0 голосов
/ 02 февраля 2010

Добавление перекомпиляции не очень хорошая идея.Прекомпиляция означает, что sql не сохраняет результаты вашего запроса, но сохраняет план выполнения.Тем самым пытаясь сделать запрос быстрее.Если вы добавите перекомпиляцию, то это всегда будет накладываться на компиляцию запроса.Попробуйте создать хранимую процедуру, сохранить запрос и вызвать его оттуда.Поскольку хранимые процедуры всегда предварительно скомпилированы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...