В практике разработчика 1С часто возникает задача анализа непрерывности нумерации. Например, необходимо выяснить, какие номера документов были пропущены, или, наоборот, вывести список всех непрерывных "островов" нумерации в формате "с номера X по номер Y". В классическом SQL эта задача известна как Gaps and Islands (пропуски и острова).
В этой статье мы подробно разберем, как реализовать такой поиск средствами языка запросов 1С, рассмотрим способы преобразования строковых номеров в числа и проанализируем эффективные алгоритмы поиска границ последовательностей.
Первая сложность, с которой мы сталкиваемся в 1С — это то, что номера документов (поле Номер) являются строковыми. Для выполнения математических операций, таких как поиск следующего числа (n+1), нам необходимо преобразовать строку в число прямо в запросе. Поскольку в языке запросов 1С нет функции Число(), мы применим метод суммирования разрядов.
Рассмотрим, как подготовить таблицу с числовыми значениями номеров. Мы создадим вспомогательную таблицу коэффициентов для каждого разряда строки (единицы, десятки, сотни и т.д.):
// Создаем таблицу весов для разрядов (16-ричная или 10-ричная система)
ВЫБРАТЬ 0 КАК С, 1 КАК П ПОМЕСТИТЬ Р10
ОБЪЕДИНИТЬ ВЫБРАТЬ 1, 10 ОБЪЕДИНИТЬ ВЫБРАТЬ 2, 100
ОБЪЕДИНИТЬ ВЫБРАТЬ 3, 1000 ОБЪЕДИНИТЬ ВЫБРАТЬ 4, 10000;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
СУММА(Разряды.П * Значения.Цифра) КАК ЗначениеЧисло,
Дано.Номер КАК ОригинальныйНомер
ПОМЕСТИТЬ НомераСЧислами
ИЗ
Дано КАК Дано
// Здесь предполагается логика выделения цифр из подстроки и умножения на веса
// Для простоты разберем алгоритм уже на готовых числах
ГРУППИРОВАТЬ ПО
Дано.Номер;
Важный момент: Если вы работаете с типовыми номерами, содержащими префиксы (например, "00БП-00001"), предварительно используйте функцию ПОДСТРОКА(), чтобы выделить только цифровую часть. Однако помните, что длина префикса может меняться, поэтому надежнее всего хранить числовой номер в отдельном реквизите при записи документа.
Разберем самый понятный алгоритм: поиск элементов, у которых нет "соседа" слева (это будет начало интервала) и элементов, у которых нет "соседа" справа (это будет конец интервала).
Проанализируем логику:
X является началом, если в таблице отсутствует число X - 1.X является концом, если в таблице отсутствует число X + 1.Реализуем это в запросе:
// Находим все начала интервалов
ВЫБРАТЬ
ТЗ.Значение КАК НачалоЧисло
ПОМЕСТИТЬ ВТ_Начала
ИЗ
ТЗ_Чисел КАК ТЗ
ЛЕВОЕ СОЕДИНЕНИЕ ТЗ_Чисел КАК ТЗ_Минус_1
ПО (ТЗ.Значение - 1 = ТЗ_Минус_1.Значение)
ГДЕ
ТЗ_Минус_1.Значение ЕСТЬ NULL;
////////////////////////////////////////////////////////////////////////////////
// Находим все концы интервалов
ВЫБРАТЬ
ТЗ.Значение КАК КонецЧисло
ПОМЕСТИТЬ ВТ_Концы
ИЗ
ТЗ_Чисел КАК ТЗ
ЛЕВОЕ СОЕДИНЕНИЕ ТЗ_Чисел КАК ТЗ_Плюс_1
ПО (ТЗ.Значение + 1 = ТЗ_Плюс_1.Значение)
ГДЕ
ТЗ_Плюс_1.Значение ЕСТЬ NULL;
Теперь, когда у нас есть две временные таблицы (ВТ_Начала и ВТ_Концы), нам нужно "сшить" их. Для каждого начала интервала концом будет минимальное из чисел в таблице концов, которое больше или равно этому началу.
Рассмотрим финальный запрос:
ВЫБРАТЬ
ВТ_Начала.НачалоЧисло КАК НачалоИнтервала,
МИНИМУМ(ВТ_Концы.КонецЧисло) КАК КонецИнтервала
ИЗ
ВТ_Начала КАК ВТ_Начала
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Концы КАК ВТ_Концы
ПО ВТ_Начала.НачалоЧисло <= ВТ_Концы.КонецЧисло
ГРУППИРОВАТЬ ПО
ВТ_Начала.НачалоЧисло
УПОРЯДОЧИТЬ ПО
НачалоИнтервала;
В результате мы получим таблицу, где каждая строка — это отдельный непрерывный период. Например, для последовательности 1, 2, 5, 6, 7 мы получим две строки: (1, 2) и (5, 7).
Существует еще один изящный способ решения задачи "островов", который часто работает быстрее на больших объемах данных. Он основан на использовании порядкового номера (ранга).
Выясним суть метода: если вычесть из самого значения его порядковый номер в отсортированном списке, то для всех чисел одной непрерывной последовательности этот результат будет одинаковым.
Пример:
Как мы видим, у первой группы разность всегда 9, у второй — 11. В запросе 1С порядковый номер можно получить через соединение таблицы самой с собой (количество элементов, которые меньше текущего).
ВЫБРАТЬ
Т1.Значение КАК Значение,
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Т2.Значение) КАК НомерПоПорядку
ПОМЕСТИТЬ ТаблицаСРангом
ИЗ
ТЗ_Чисел КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ТЗ_Чисел КАК Т2
ПО Т1.Значение >= Т2.Значение
ГРУППИРОВАТЬ ПО
Т1.Значение;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
МИНИМУМ(Значение) КАК Начало,
МАКСИМУМ(Значение) КАК Конец
ИЗ
ТаблицаСРангом
ГРУППИРОВАТЬ ПО
(Значение - НомерПоПорядку);
Проанализируем технические ограничения, которые важно учитывать при реализации данного механизма:
ПО Т1.Значение >= Т2.Значение или Т1.Значение = Т2.Значение + 1, имеют квадратичную зависимость от объема данных. Если в вашей выборке десятки тысяч документов, запрос может выполняться крайне медленно. В таких случаях эффективнее выгрузить отсортированные данные в ТаблицуЗначений и пройтись по ней один раз циклом на встроенном языке.Значение. Это критически важно для скорости выполнения соединений.Используя предложенные методики, мы можем гибко управлять анализом последовательностей, обеспечивая корректность данных и прозрачность нумерации в системе.