Описание проблемы:
задан диапазон
x -> y
целых без знака
гдеx
иy
находятся в диапазоне0 -> 2
n
иn
-0 -> 32
(или 64 в альтернативных случаях)
найти минимальную доступную стоимость
не равнымx
илиy
что нет в существующем наборе
где существующими множествами являются произвольные подмножестваx -> y
Я работаю над созданием подсетей IPv4 и IPv6 в базе данных. Каждая подсеть определяется начальным адресом и конечным адресом (я гарантирую целостность диапазонов с помощью бизнес-правил). Поскольку IPv6 слишком велик для хранения в типе bigint
, мы храним IP-адреса как binary(4)
или binary(16)
.
Связанные данные хранятся в таблицах subnet
, dhcp_range
и ip_address
:
- Subnet:
Диапазон подсети определяется начальным и конечным IP-адресом и сохраняется в таблице
subnet
. Диапазон подсети всегда имеет размер 2 n (согласно определению CIDR/netmask). - IP:
В подсети есть
0..*
IP-адреса, сохраненные в таблицеip_address
. IP-адрес должен быть между начальным и конечным адресами, но не равным диапазону, как определено его связанной подсетью. - Диапазон DHCP:
В подсети есть
0..*
диапазоны DHCP, хранящиеся в таблицеdhcp_range
. Подобно подсети, каждый диапазон DHCP определяет начальный и конечный адреса. Диапазон DHCP ограничен связанным диапазоном подсети. Диапазоны DHCP не перекрывают друг друга.
То, что я хочу определить, - это следующий доступный IP для подсети:
- который еще не назначен (не в таблице IP-адресов)
- не в диапазоне DHCP
- и не равен начальному или конечному адресу диапазона подсети.
Я ищу решение, которое найдет либо минимальный доступный адрес, либо все доступные адреса.
Моя первоначальная мысль заключалась в том, чтобы сгенерировать диапазон возможных адресов (чисел), связанных диапазоном подсети, а затем удалить адреса на основе используемых наборов:
declare @subnet_sk int = 42
;with
address_range as (
select cast(ipv4_begin as bigint) as available_address
,cast(ipv4_end as bigint) as end_address, subnet_sk
from subnet s
where subnet_sk = @subnet_sk
union all
select available_address + 1, end_address, subnet_sk
from address_range
where available_address + 1 <= end_address
),
assigned_addresses as (
select ip.[address]
,subnet_sk
from ip_address ip
where ip.subnet_sk = @subnet_sk
and ip.address_family = 'InterNetwork'),
dhcp_ranges as (
select dhcp.begin_address
,dhcp.end_address
,subnet_sk
from dhcp_range dhcp
where dhcp.subnet_sk = @subnet_sk
and dhcp.address_family = 'InterNetwork')
select distinct ar.available_address
from address_range ar
join dhcp_ranges dhcp
on ar.available_address
not between dhcp.begin_address
and dhcp.end_address
left join assigned_addresses aa
on ar.available_address = aa.[address]
join subnet s
on ar.available_address != s.ipv4_begin
and ar.available_address != s.ipv4_end
where aa.[address] is null
and s.subnet_sk = @subnet_sk
order by available_address
option (MAXRECURSION 32767)
В приведенном выше запросе используется рекурсивный CTE и не работает для всех перестановок данных. Рекурсивный КТВ является хлопотным, поскольку он ограничен максимальным размером 32 767 (намного меньше, чем размеры потенциальных диапазонов) и имеет очень реальную возможность быть очень медленным. Вероятно, я мог бы преодолеть свои проблемы с рекурсивным CTE, но запрос не выполняется при следующих условиях:
- когда не назначены IP-адреса или диапазоны DHCP: он ничего не возвращает
должен возвращать все IP-адреса, определенные диапазоном подсети - при назначении нескольких диапазонов DHCP: возвращает IP-адреса внутри диапазонов DHCP
Помощник в устранении проблемы, я создал SQL Fiddle с тремя подсетей; каждая с другой характеристикой: рубленый, пустой или в основном смежный. Вышеприведенный запрос и настройка в скрипте работают для большей части непрерывной подсети, но не для других. Существует также GitHub Gist схемы и примеры данных.
Я попытался сгенерировать последовательность чисел с рекурсивными и сложными CTE, но, как указано выше, боюсь, что они будут плохо выполняться, а в случае рекурсивных CTE искусственно ограничивают. Аарон Бертран детализирует некоторые альтернативы CTE в своей серии Генерировать набор или последовательность без циклов. К сожалению, набор данных слишком велик для таблицы чисел, так как создание одного только для адресного пространства IPv4 потребует 32 гигабайта дискового пространства (SQL Server сохраняет bigint
значения в 8 байт). Я бы предпочел генерировать последовательность "на лету", но придумал хороший способ сделать это.
В качестве альтернативы, я попытался засеять мой запрос, посмотрев, что я знаю, для использования адресов:
declare @subnet_sk int = 1
select unassigned_range.*
from (select cast(l.address as bigint) + 1 as start
,min(cast(fr.address as bigint)) - 1 as stop
from ip_address as l
left join ip_address as r on l.address = r.address - 1
left join ip_address as fr on l.address < fr.address
where r.address is null and fr.address is not null
and l.subnet_sk = @subnet_sk
group by l.address, r.address) as unassigned_range
join dhcp_range dhcp
on unassigned_range.start
not between cast(dhcp.begin_address as bigint)
and cast(dhcp.end_address as bigint)
and unassigned_range.stop
not between cast(dhcp.begin_address as bigint)
and cast(dhcp.end_address as bigint)
where dhcp.subnet_sk = @subnet_sk
К сожалению, указанный выше запрос не работает, когда в таблицах ip_address
или dhcp_range
нет ничего. Хуже того, поскольку он не знает границ диапазона подсети a dhcp_range
к верхней границе диапазона подсети, искусственно ограничивает то, что возвращается, поскольку запрос не может возвращать строки из пустого пространства по краям. Производительность также не выдающаяся.
Использование SQL или TSQL, как определить следующее минимальное доступное целочисленное значение в пределах произвольного целочисленного диапазона, ограниченного другими диапазонами?