Мне нужно иметь возможность найти отсутствующий элемент из таблицы с десятками миллионов строк и иметь первичный ключ BINARY(64)
столбца (который является входным значением для вычисления). Эти значения в основном вставляются по порядку, но иногда я хочу использовать предыдущее значение, которое было удалено. Невозможно изменить удаленные записи IsDeleted
столбцом, поскольку иногда вставляется строка, которая на много миллионов значений опережает существующие в настоящее время строки. Это означает, что пример данных будет выглядеть примерно так:
KeyCol : BINARY(64)
0x..000000000001
0x..000000000002
0x..FFFFFFFFFFFF
Таким образом, вставка всех пропущенных значений между 0x000000000002
и 0xFFFFFFFFFFFF
невозможна, использованное количество времени и пространства было бы нежелательным. По сути, когда я запускаю алгоритм, я ожидаю, что он вернется 0x000000000003
, что является первым открытием.
Я придумал алгоритм бинарного поиска в C #, который будет запрашивать базу данных для каждого значения в позиции i
и проверять, ожидалось ли это значение. Для контекста, мой ужасный алгоритм: /codereview/174498/binary-search-for-a-missing-or-default-value-by-a-given-formula
Этот алгоритм будет выполнять, например, 26-27 SQL-запросов к таблице с 100 000 000 элементов. (Это не так много, но это будет происходить очень часто.) В настоящее время эта таблица содержит приблизительно 50 000 000 строк, и производительность становится заметной .
Моя первая альтернативная мысль - перевести это на хранимую процедуру, но у нее есть свои препятствия. (Я должен написать BINARY(64) + BINARY(64)
алгоритм, а также множество других вещей.) Это было бы больно, но не невозможно. Я также рассмотрел реализацию алгоритма перевода на основе ROW_NUMBER
, но у меня действительно плохое предчувствие по этому поводу. (A BIGINT
не достаточно большой для этих значений.)
Я готов к другим предложениям, поскольку мне действительно нужно, чтобы это было как можно быстрее. Для чего стоит единственный столбец, выбранный запросом C # KeyCol
, остальные не имеют значения для этой части.
Кроме того, что бы ни стоило, текущий запрос, который выбирает соответствующую запись, выглядит следующим образом:
SELECT [KeyCol]
FROM [Table]
ORDER BY [KeyCol] ASC
OFFSET <VALUE> ROWS FETCH FIRST 1 ROWS ONLY
Где <VALUE>
индекс, предоставляемый алгоритмом. У меня также не было BIGINT
проблемы с OFFSET
, но я буду. (Только наличие 50 000 000 строк прямо сейчас означает, что он никогда не запрашивает индекс выше этого значения, но в какой-то момент он окажется выше BIGINT
диапазона.)
Некоторые дополнительные данные:
- Из удалений
gap:sequential
соотношение составляет около1:20
; - Последние 35 000 строк в таблице имеют значения>
BIGINT
максимум;
источник
delete
на таблицу триггер, который бы сбрасывал доступный теперь двоичный файл в отдельную таблицу (например,create table available_for_reuse(id binary64)
), особенно в свете требования делать этот поиск очень часто ?mynameisebrown
что будет означать, что вы получитеmynameisebrowo
, что вы не хотел бы, еслиabc
есть в наличии.select t1.keycol+1 as aa from t as t1 where not exists (select 1 from t as t2 where t2.keycol = t1.keycol+1) order by keycol fetch first 1 rows only
?SELECT TOP 1 ([T1].[KeyCol] + 1) AS [AA] FROM [SearchTestTableProper] AS [T1] WHERE NOT EXISTS (SELECT 1 FROM [SearchTestTableProper] AS [T2] WHERE [T2].[KeyCol] = [T1].[KeyCol] + 1) ORDER BY [KeyCol]
, что всегда возвращает1
.Ответы:
Джо уже набрал большинство пунктов, которые я только что провел, набирая час:
KeyCol
значения <bigint
max (9.2e18), поэтому преобразование (при необходимости) в / изbigint
не должно быть проблемой, если вы ограничиваете поискиKeyCol <= 0x00..007FFFFFFFFFFFFFFF
Так что делать?
Давайте на минуту остановим (повторяющийся, интенсивно использующий процессор, перебор) идею поиска и посмотрим на картину в целом.
Я хотел бы предложить несколько дополнений к модели данных ...
KeyCol
, например:available_for_use(KeyCol binary(64) not null primary key)
KeyCol
дополнять новой партией значений (возможно, создать хранимый процесс «top top»?) [например, обновитьselect/top/row_number()
запрос Джо, чтобы сделатьtop 100000
]available_for_use
тот случай, если у вас начнут заканчиваться значенияKeyCol
значения в нашу новую таблицуavailable_for_use
всякий раз, когда строка удаляется из основной таблицыKeyCol
столбца, то новый / измененный триггер UPDATE на> main_table <также сохранит нашу новую таблицуavailable_for_use
обновленнойKeyCol
значение, которое вамselect min(KeyCol) from available_for_use
(очевидно, есть кое-что еще, поскольку а) вам нужно будет кодировать проблемы параллелизма - не нужно, чтобы 2 копии вашего процесса захватывали одноmin(KeyCol)
и то же, и б) вы нужно будет удалитьmin(KeyCol)
из таблицы; это должно быть относительно легко закодировано, возможно, как хранимый процесс, и может быть обращено в другой Q & A при необходимости)select min(KeyCol)
процесс не найдет доступных строк, вы можете запустить процедуру «top top» для генерации нового пакета строкС этими предлагаемыми изменениями в модель данных:
available_for_use
таблицы, чтобы убедиться, что вы никогда не исчерпаете новые значенияДа, предлагаемая
available_for_use
таблица - это просто таблица предварительно сгенерированных значений «следующего ключа»; и да, существует вероятность некоторой конкуренции при получении значения «next», но любая конкуренция a) легко решается с помощью надлежащей структуры таблицы / индекса / запроса и b) будет незначительной / недолгой по сравнению с издержками / задерживается с текущей идеей повторных, грубых действий, поисков по индексу.источник
n
ключи (вероятно, 10 или 20, чтобы заставить его искать то, что может быть ниже, более желательные значения). По-настоящему оцените ответ здесь, вы изложите свои мысли в письменном виде! :)KeyCol
значений ... да, это тоже сработает :-) и, очевидно, устранит необходимость изменения модели данных. ehKeyCol
менеджер и необходимость кодировать потенциальные нарушения PK, если 2 (или более) одновременных экземпляра приложения пытаются использовать одно и то жеKeyCol
значение ... хм ... определенно проще с одним сервером промежуточного программного обеспечения или db-Есть несколько проблем с этим вопросом. Индексы в SQL Server могут очень эффективно выполнять следующие действия с помощью нескольких логических операций чтения:
Однако их нельзя использовать для поиска N-й строки в индексе. Для этого необходимо свернуть собственный индекс, сохраненный в виде таблицы, или отсканировать первые N строк в индексе. Ваш код C # в значительной степени основан на том факте, что вы можете эффективно найти N-й элемент массива, но вы не можете сделать это здесь. Я думаю, что алгоритм не может использоваться для T-SQL без изменения модели данных.
Вторая проблема связана с ограничениями на
BINARY
типы данных. Насколько я могу сказать, вы не можете выполнять сложение, вычитание или деление обычными способами. Вы можете преобразовать вашеBINARY(64)
в a,BIGINT
и оно не будет выдавать ошибки преобразования, но поведение не определено :Кроме того, отсутствие ошибок конвертации является в некоторой степени проблемой здесь. Вы можете конвертировать все, что больше, чем максимально возможное
BIGINT
значение, но это даст вам неправильные результаты.Это правда, что сейчас у вас есть значения, которые больше, чем 9223372036854775807. Однако, если вы всегда начинаете с 1 и ищете наименьшее минимальное значение, эти большие значения не могут быть релевантными, если в вашей таблице не более 9223372036854775807 строк. Это кажется маловероятным, поскольку ваша таблица в этот момент будет иметь размер около 2000 эксабайт, поэтому для ответа на ваш вопрос я собираюсь предположить, что не нужно искать очень большие значения. Я также собираюсь сделать преобразование типов данных, потому что они кажутся неизбежными.
Для тестовых данных я вставил в таблицу эквивалент 50 миллионов последовательных целых чисел, а также еще 50 миллионов целых чисел с одним пропуском значений примерно на каждые 20 значений. Я также вставил одно значение, которое не помещается в подписи
BIGINT
:Этот код занял несколько минут для запуска на моей машине. Я сделал, чтобы первая половина таблицы не имела пробелов, чтобы представить своего рода худший случай для производительности. Код, который я использовал для решения этой проблемы, сканирует индекс по порядку, поэтому он очень быстро завершится, если в таблице будет первый пробел. Прежде чем мы перейдем к этому, давайте проверим, что данные должны быть такими:
Результаты показывают, что максимальное значение, которое мы конвертируем,
BIGINT
равно 102500672:Есть 100 миллионов строк со значениями, которые вписываются в BIGINT, как и ожидалось:
Одним из подходов к этой проблеме является сканирование индекса по порядку и выход, когда
ROW_NUMBER()
значение строки не соответствует ожидаемому значению. Не нужно сканировать всю таблицу, чтобы получить первую строку: только строки до первого разрыва. Вот один из способов написания кода, который может получить план запроса:По причинам, которые не вписываются в этот ответ, этот запрос часто запускается последовательно SQL Server, а SQL Server часто недооценивает количество строк, которые необходимо отсканировать, прежде чем будет найдено первое совпадение. На моем компьютере SQL Server сканирует 50000022 строк из индекса, прежде чем находит первое совпадение. Выполнение запроса занимает 11 секунд. Обратите внимание, что это возвращает первое значение после пробела. Непонятно, какую именно строку вы хотите получить, но вы сможете изменить запрос в соответствии со своими потребностями без особых проблем. Вот как выглядит план :
Моя единственная другая идея заключалась в том, чтобы заставить SQL Server использовать параллелизм для запроса. У меня четыре процессора, поэтому я собираюсь разбить данные на четыре диапазона и выполнить поиск по этим диапазонам. Каждому ЦПУ будет присвоен диапазон. Чтобы вычислить диапазоны, я просто взял максимальное значение и предположил, что данные были распределены равномерно. Если вы хотите быть более умным в этом, вы можете посмотреть на выборочную гистограмму статистики для значений столбцов и построить свои диапазоны таким образом. Приведенный ниже код опирается на множество недокументированных приемов, которые небезопасны для работы, включая флаг трассировки 8649 :
Вот как выглядит шаблон параллельного вложенного цикла:
В целом, запрос выполняет больше работы, чем раньше, поскольку он будет сканировать больше строк в таблице. Тем не менее, теперь он запускается за 7 секунд на моем рабочем столе. Это может лучше распараллелить на реальном сервере. Вот ссылка на фактический план .
Я действительно не могу придумать хороший способ решить эту проблему. Выполнение расчетов за пределами SQL или изменение модели данных может быть вашим лучшим выбором.
источник
Вот ответ, который, вероятно, не сработает для вас, но я все равно добавлю его.
Несмотря на то, что BINARY (64) перечислим, существует слабая поддержка для определения преемника элемента. Поскольку BIGINT кажется слишком маленьким для вашего домена, вы можете рассмотреть возможность использования DECIMAL (38,0), который представляется самым большим типом NUMBER в SQL-сервере.
Найти первый пробел легко, так как мы можем построить искомое число:
Соединение с вложенным циклом по индексу pk должно быть достаточным для поиска первого доступного элемента.
источник