Есть несколько проблем с этим вопросом. Индексы в SQL Server могут очень эффективно выполнять следующие действия с помощью нескольких логических операций чтения:
- проверьте, что строка существует
- проверьте, что строки не существует
- найти следующую строку, начиная с некоторой точки
- найти предыдущую строку, начиная с некоторой точки
Однако их нельзя использовать для поиска N-й строки в индексе. Для этого необходимо свернуть собственный индекс, сохраненный в виде таблицы, или отсканировать первые N строк в индексе. Ваш код C # в значительной степени основан на том факте, что вы можете эффективно найти N-й элемент массива, но вы не можете сделать это здесь. Я думаю, что алгоритм не может использоваться для T-SQL без изменения модели данных.
Вторая проблема связана с ограничениями на BINARY
типы данных. Насколько я могу сказать, вы не можете выполнять сложение, вычитание или деление обычными способами. Вы можете преобразовать ваше BINARY(64)
в a, BIGINT
и оно не будет выдавать ошибки преобразования, но поведение не определено :
Преобразования между любыми типами данных и двоичными типами данных не гарантируются одинаковыми между версиями SQL Server.
Кроме того, отсутствие ошибок конвертации является в некоторой степени проблемой здесь. Вы можете конвертировать все, что больше, чем максимально возможное BIGINT
значение, но это даст вам неправильные результаты.
Это правда, что сейчас у вас есть значения, которые больше, чем 9223372036854775807. Однако, если вы всегда начинаете с 1 и ищете наименьшее минимальное значение, эти большие значения не могут быть релевантными, если в вашей таблице не более 9223372036854775807 строк. Это кажется маловероятным, поскольку ваша таблица в этот момент будет иметь размер около 2000 эксабайт, поэтому для ответа на ваш вопрос я собираюсь предположить, что не нужно искать очень большие значения. Я также собираюсь сделать преобразование типов данных, потому что они кажутся неизбежными.
Для тестовых данных я вставил в таблицу эквивалент 50 миллионов последовательных целых чисел, а также еще 50 миллионов целых чисел с одним пропуском значений примерно на каждые 20 значений. Я также вставил одно значение, которое не помещается в подписи BIGINT
:
CREATE TABLE dbo.BINARY_PROBLEMS (
KeyCol BINARY(64) NOT NULL
);
INSERT INTO dbo.BINARY_PROBLEMS WITH (TABLOCK)
SELECT CAST(SUM(OFFSET) OVER (ORDER BY (SELECT NULL) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BINARY(64))
FROM
(
SELECT 1 + CASE WHEN t.RN > 50000000 THEN
CASE WHEN ABS(CHECKSUM(NewId()) % 20) = 10 THEN 1 ELSE 0 END
ELSE 0 END OFFSET
FROM
(
SELECT TOP (100000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
) t
) tt
OPTION (MAXDOP 1);
CREATE UNIQUE CLUSTERED INDEX CI_BINARY_PROBLEMS ON dbo.BINARY_PROBLEMS (KeyCol);
-- add a value too large for BIGINT
INSERT INTO dbo.BINARY_PROBLEMS
SELECT CAST(0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000 AS BINARY(64));
Этот код занял несколько минут для запуска на моей машине. Я сделал, чтобы первая половина таблицы не имела пробелов, чтобы представить своего рода худший случай для производительности. Код, который я использовал для решения этой проблемы, сканирует индекс по порядку, поэтому он очень быстро завершится, если в таблице будет первый пробел. Прежде чем мы перейдем к этому, давайте проверим, что данные должны быть такими:
SELECT TOP (2) KeyColBigInt
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
FROM dbo.BINARY_PROBLEMS
) t
ORDER By KeyCol DESC;
Результаты показывают, что максимальное значение, которое мы конвертируем, BIGINT
равно 102500672:
╔══════════════════════╗
║ KeyColBigInt ║
╠══════════════════════╣
║ -9223372036854775808 ║
║ 102500672 ║
╚══════════════════════╝
Есть 100 миллионов строк со значениями, которые вписываются в BIGINT, как и ожидалось:
SELECT COUNT(*)
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF;
Одним из подходов к этой проблеме является сканирование индекса по порядку и выход, когда ROW_NUMBER()
значение строки не соответствует ожидаемому значению. Не нужно сканировать всю таблицу, чтобы получить первую строку: только строки до первого разрыва. Вот один из способов написания кода, который может получить план запроса:
SELECT TOP (1) KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF
) t
WHERE KeyColBigInt <> RN
ORDER BY KeyCol;
По причинам, которые не вписываются в этот ответ, этот запрос часто запускается последовательно SQL Server, а SQL Server часто недооценивает количество строк, которые необходимо отсканировать, прежде чем будет найдено первое совпадение. На моем компьютере SQL Server сканирует 50000022 строк из индекса, прежде чем находит первое совпадение. Выполнение запроса занимает 11 секунд. Обратите внимание, что это возвращает первое значение после пробела. Непонятно, какую именно строку вы хотите получить, но вы сможете изменить запрос в соответствии со своими потребностями без особых проблем. Вот как выглядит план :
Моя единственная другая идея заключалась в том, чтобы заставить SQL Server использовать параллелизм для запроса. У меня четыре процессора, поэтому я собираюсь разбить данные на четыре диапазона и выполнить поиск по этим диапазонам. Каждому ЦПУ будет присвоен диапазон. Чтобы вычислить диапазоны, я просто взял максимальное значение и предположил, что данные были распределены равномерно. Если вы хотите быть более умным в этом, вы можете посмотреть на выборочную гистограмму статистики для значений столбцов и построить свои диапазоны таким образом. Приведенный ниже код опирается на множество недокументированных приемов, которые небезопасны для работы, включая флаг трассировки 8649 :
SELECT TOP 1 ca.KeyCol
FROM (
SELECT 1 bucket_min_value, 25625168 bucket_max_value
UNION ALL
SELECT 25625169, 51250336
UNION ALL
SELECT 51250337, 76875504
UNION ALL
SELECT 76875505, 102500672
) buckets
CROSS APPLY (
SELECT TOP 1 t.KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, buckets.bucket_min_value - 1 + ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol >= CAST(buckets.bucket_min_value AS BINARY(64)) AND KeyCol <= CAST(buckets.bucket_max_value AS BINARY(64))
) t
WHERE t.KeyColBigInt <> t.RN
ORDER BY t.KeyCol
) ca
ORDER BY ca.KeyCol
OPTION (QUERYTRACEON 8649);
Вот как выглядит шаблон параллельного вложенного цикла:
В целом, запрос выполняет больше работы, чем раньше, поскольку он будет сканировать больше строк в таблице. Тем не менее, теперь он запускается за 7 секунд на моем рабочем столе. Это может лучше распараллелить на реальном сервере. Вот ссылка на фактический план .
Я действительно не могу придумать хороший способ решить эту проблему. Выполнение расчетов за пределами SQL или изменение модели данных может быть вашим лучшим выбором.
delete
на таблицу триггер, который бы сбрасывал доступный теперь двоичный файл в отдельную таблицу (например,create table available_for_reuse(id binary64)
), особенно в свете требования делать этот поиск очень часто ?