V tomto případě není potřeba rekurze, protože máme LEAD
funkce.
Budu o problému přemýšlet z hlediska „mezer“ a „ostrovů“.
Nejprve se zaměřím na IPv4, protože je snazší s nimi provádět aritmetiku, ale myšlenka pro IPv6 je stejná a nakonec ukážu obecné řešení.
Pro začátek máme celou řadu možných IP adres:od 0x00000000
na 0xFFFFFFFF
.
Uvnitř tohoto rozsahu jsou "ostrovy" definované rozsahy (včetně) v dhcp_range
:dhcp_range.begin_address, dhcp_range.end_address
. Seznam přidělených IP adres si můžete představit jako další sadu ostrovů, z nichž každý má jeden prvek:ip_address.address, ip_address.address
. Nakonec samotná podsíť tvoří dva ostrovy:0x00000000, subnet.ipv4_begin
a subnet.ipv4_end, 0xFFFFFFFF
.
Víme, že tyto ostrovy ne překrývání, což nám usnadňuje život. Ostrovy spolu mohou dokonale sousedit. Například, když máte několik po sobě jdoucích přidělených IP adres, mezera mezi nimi je nulová. Mezi všemi těmito ostrovy musíme najít první mezeru, která má alespoň jeden prvek, tj. nenulovou mezeru, tj. další ostrov začíná na nějakou vzdálenost poté, co předchozí ostrov skončí.
Takže dáme všechny ostrovy dohromady pomocí UNION
(CTE_Islands
) a poté je projděte všechny v pořadí end_address
(nebo begin_address
, použijte pole, které má index) a použijte LEAD
nahlédnout dopředu a získat výchozí adresu dalšího ostrova. Nakonec budeme mít tabulku, kde každý řádek měl end_address
aktuálního ostrova a begin_address
dalšího ostrova (CTE_Diff
). Pokud je rozdíl mezi nimi více než jeden, znamená to, že "mezera" je dostatečně široká a vrátíme end_address
aktuálního ostrova plus 1.
První dostupná adresa IP pro danou podsíť
DECLARE @ParamSubnet_sk int = 1;
WITH
CTE_Islands
AS
(
SELECT CAST(begin_address AS bigint) AS begin_address, CAST(end_address AS bigint) AS end_address
FROM dhcp_range
WHERE subnet_sk = @ParamSubnet_sk
UNION ALL
SELECT CAST(address AS bigint) AS begin_address, CAST(address AS bigint) AS end_address
FROM ip_address
WHERE subnet_sk = @ParamSubnet_sk
UNION ALL
SELECT CAST(0x00000000 AS bigint) AS begin_address, CAST(ipv4_begin AS bigint) AS end_address
FROM subnet
WHERE subnet_sk = @ParamSubnet_sk
UNION ALL
SELECT CAST(ipv4_end AS bigint) AS begin_address, CAST(0xFFFFFFFF AS bigint) AS end_address
FROM subnet
WHERE subnet_sk = @ParamSubnet_sk
)
,CTE_Diff
AS
(
SELECT
begin_address
, end_address
--, LEAD(begin_address) OVER(ORDER BY end_address) AS BeginNextIsland
, LEAD(begin_address) OVER(ORDER BY end_address) - end_address AS Diff
FROM CTE_Islands
)
SELECT TOP(1)
CAST(end_address + 1 AS varbinary(4)) AS NextAvailableIPAddress
FROM CTE_Diff
WHERE Diff > 1
ORDER BY end_address;
Sada výsledků by obsahovala jeden řádek, pokud je k dispozici alespoň jedna IP adresa, a neobsahovala by řádky vůbec, pokud nejsou k dispozici žádné IP adresy.
For parameter 1 result is `0xAC101129`.
For parameter 2 result is `0xC0A81B1F`.
For parameter 3 result is `0xC0A8160C`.
Zde je odkaz na SQLFiddle
. Nefungovalo to s parametrem, takže jsem natvrdo zakódoval 1
tam. Změňte jej v UNION na jiné ID podsítě (2 nebo 3) a vyzkoušejte jiné podsítě. Také to nezobrazilo výsledek v varbinary
správně, tak jsem to nechal jako bigint. K ověření výsledku použijte, řekněme, kalkulačku Windows, převeďte jej na hex.
Pokud výsledky neomezíte na první mezeru pomocí TOP(1)
, získáte seznam všech dostupných rozsahů IP (mezery).
Seznam všech rozsahů dostupných IP adres pro danou podsíť
DECLARE @ParamSubnet_sk int = 1;
WITH
CTE_Islands
AS
(
SELECT CAST(begin_address AS bigint) AS begin_address, CAST(end_address AS bigint) AS end_address
FROM dhcp_range
WHERE subnet_sk = @ParamSubnet_sk
UNION ALL
SELECT CAST(address AS bigint) AS begin_address, CAST(address AS bigint) AS end_address
FROM ip_address
WHERE subnet_sk = @ParamSubnet_sk
UNION ALL
SELECT CAST(0x00000000 AS bigint) AS begin_address, CAST(ipv4_begin AS bigint) AS end_address
FROM subnet
WHERE subnet_sk = @ParamSubnet_sk
UNION ALL
SELECT CAST(ipv4_end AS bigint) AS begin_address, CAST(0xFFFFFFFF AS bigint) AS end_address
FROM subnet
WHERE subnet_sk = @ParamSubnet_sk
)
,CTE_Diff
AS
(
SELECT
begin_address
, end_address
, LEAD(begin_address) OVER(ORDER BY end_address) AS BeginNextIsland
, LEAD(begin_address) OVER(ORDER BY end_address) - end_address AS Diff
FROM CTE_Islands
)
SELECT
CAST(end_address + 1 AS varbinary(4)) AS begin_range_AvailableIPAddress
,CAST(BeginNextIsland - 1 AS varbinary(4)) AS end_range_AvailableIPAddress
FROM CTE_Diff
WHERE Diff > 1
ORDER BY end_address;
Výsledek. SQL Fiddle s výsledkem jako jednoduchý bigint, nikoli v hex, as pevně zakódovaným ID parametru.
Result set for ID = 1
begin_range_AvailableIPAddress end_range_AvailableIPAddress
0xAC101129 0xAC10112E
Result set for ID = 2
begin_range_AvailableIPAddress end_range_AvailableIPAddress
0xC0A81B1F 0xC0A81B1F
0xC0A81B22 0xC0A81B28
0xC0A81BFA 0xC0A81BFE
Result set for ID = 3
begin_range_AvailableIPAddress end_range_AvailableIPAddress
0xC0A8160C 0xC0A8160C
0xC0A816FE 0xC0A816FE
První dostupná adresa IP pro každou podsíť
Je snadné rozšířit dotaz a vrátit první dostupnou IP adresu pro všechny podsítě, spíše než zadávat jednu konkrétní podsíť. Použijte CROSS APPLY
získat seznam ostrovů pro každou podsíť a poté přidat PARTITION BY subnet_sk
do LEAD
funkce.
WITH
CTE_Islands
AS
(
SELECT
subnet_sk
, begin_address
, end_address
FROM
subnet AS Main
CROSS APPLY
(
SELECT CAST(begin_address AS bigint) AS begin_address, CAST(end_address AS bigint) AS end_address
FROM dhcp_range
WHERE dhcp_range.subnet_sk = Main.subnet_sk
UNION ALL
SELECT CAST(address AS bigint) AS begin_address, CAST(address AS bigint) AS end_address
FROM ip_address
WHERE ip_address.subnet_sk = Main.subnet_sk
UNION ALL
SELECT CAST(0x00000000 AS bigint) AS begin_address, CAST(ipv4_begin AS bigint) AS end_address
FROM subnet
WHERE subnet.subnet_sk = Main.subnet_sk
UNION ALL
SELECT CAST(ipv4_end AS bigint) AS begin_address, CAST(0xFFFFFFFF AS bigint) AS end_address
FROM subnet
WHERE subnet.subnet_sk = Main.subnet_sk
) AS CA
)
,CTE_Diff
AS
(
SELECT
subnet_sk
, begin_address
, end_address
, LEAD(begin_address) OVER(PARTITION BY subnet_sk ORDER BY end_address) - end_address AS Diff
FROM CTE_Islands
)
SELECT
subnet_sk
, CAST(MIN(end_address) + 1 as varbinary(4)) AS NextAvailableIPAddress
FROM CTE_Diff
WHERE Diff > 1
GROUP BY subnet_sk
Výsledková sada
subnet_sk NextAvailableIPAddress
1 0xAC101129
2 0xC0A81B1F
3 0xC0A8160C
Zde je SQLFiddle
. Musel jsem odstranit konverzi na varbinary
v SQL Fiddle, protože zobrazoval výsledky nesprávně.
Obecné řešení pro IPv4 i IPv6
Všechny rozsahy dostupných adres IP pro všechny podsítě
SQL Fiddle s ukázkovými daty IPv4 a IPv6, funkcemi a závěrečným dotazem
Vaše ukázková data pro IPv6 nebyla zcela správná – konec podsítě 0xFC00000000000000FFFFFFFFFFFFFFFF
byla nižší než vaše rozsahy dhcp, takže jsem to změnil na 0xFC0001066800000000000000FFFFFFFF
. Také jste měli IPv4 a IPv6 ve stejné podsíti, což je těžkopádné. V zájmu tohoto příkladu jsem trochu změnil vaše schéma – místo explicitního ipv4_begin / end
a ipv6_begin / end
v subnet
Udělal jsem to jen ip_begin / end
jako varbinary(16)
(stejně jako u vašich ostatních stolů). Také jsem odstranil address_family
, jinak byl pro SQL Fiddle příliš velký.
Aritmetické funkce
Aby to fungovalo pro IPv6, musíme zjistit, jak přidat/odečíst 1
do/z binary(16)
. Udělal bych pro to funkci CLR. Pokud nemáte povoleno povolit CLR, je to možné prostřednictvím standardního T-SQL. Vytvořil jsem dvě funkce, které vracejí tabulku, spíše než skalární, protože takovým způsobem mohou být vloženy optimalizátorem. Chtěl jsem vytvořit obecné řešení, takže funkce by akceptovala varbinary(16)
a fungují pro IPv4 i IPv6.
Zde je funkce T-SQL pro inkrementaci varbinary(16)
jedním. Pokud parametr není dlouhý 16 bajtů, předpokládám, že jde o IPv4 a jednoduše jej převeďte na bigint
přidat 1
a pak zpět na binary
. Jinak jsem rozdělil binary(16)
na dvě části o délce 8 bajtů a přelijte je do bigint
. bigint
je podepsán, ale potřebujeme nepodepsaný přírůstek, takže musíme zkontrolovat několik případů.
else
část je nejběžnější - jednoduše zvýšíme nízkou část o jednu a výsledek připojíme k původní vysoké části.
Pokud je nízká část 0xFFFFFFFFFFFFFFFF
, pak nastavíme nízkou část na 0x0000000000000000
a přenést vlajku, tj. zvýšit vysokou část o jednu.
Pokud je nízká část 0x7FFFFFFFFFFFFFFF
, pak nastavíme nízkou část na 0x8000000000000000
explicitně, protože se jedná o pokus zvýšit tento bigint
hodnota by způsobila přetečení.
Pokud je celé číslo 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
výsledek jsme nastavili na 0x00000000000000000000000000000000
.
Funkce snížení o jedna je podobná.
CREATE FUNCTION [dbo].[BinaryInc](@src varbinary(16))
RETURNS TABLE AS
RETURN
SELECT
CASE WHEN DATALENGTH(@src) = 16
THEN
-- Increment IPv6 by splitting it into two bigints 8 bytes each and then concatenating them
CASE
WHEN @src = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
THEN 0x00000000000000000000000000000000
WHEN SUBSTRING(@src, 9, 8) = 0x7FFFFFFFFFFFFFFF
THEN SUBSTRING(@src, 1, 8) + 0x8000000000000000
WHEN SUBSTRING(@src, 9, 8) = 0xFFFFFFFFFFFFFFFF
THEN CAST(CAST(SUBSTRING(@src, 1, 8) AS bigint) + 1 AS binary(8)) + 0x0000000000000000
ELSE SUBSTRING(@src, 1, 8) + CAST(CAST(SUBSTRING(@src, 9, 8) AS bigint) + 1 AS binary(8))
END
ELSE
-- Increment IPv4 by converting it into 8 byte bigint and then back into 4 bytes binary
CAST(CAST(CAST(@src AS bigint) + 1 AS binary(4)) AS varbinary(16))
END AS Result
;
GO
CREATE FUNCTION [dbo].[BinaryDec](@src varbinary(16))
RETURNS TABLE AS
RETURN
SELECT
CASE WHEN DATALENGTH(@src) = 16
THEN
-- Decrement IPv6 by splitting it into two bigints 8 bytes each and then concatenating them
CASE
WHEN @src = 0x00000000000000000000000000000000
THEN 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
WHEN SUBSTRING(@src, 9, 8) = 0x8000000000000000
THEN SUBSTRING(@src, 1, 8) + 0x7FFFFFFFFFFFFFFF
WHEN SUBSTRING(@src, 9, 8) = 0x0000000000000000
THEN CAST(CAST(SUBSTRING(@src, 1, 8) AS bigint) - 1 AS binary(8)) + 0xFFFFFFFFFFFFFFFF
ELSE SUBSTRING(@src, 1, 8) + CAST(CAST(SUBSTRING(@src, 9, 8) AS bigint) - 1 AS binary(8))
END
ELSE
-- Decrement IPv4 by converting it into 8 byte bigint and then back into 4 bytes binary
CAST(CAST(CAST(@src AS bigint) - 1 AS binary(4)) AS varbinary(16))
END AS Result
;
GO
Všechny rozsahy dostupných IP adres pro všechny podsítě
WITH
CTE_Islands
AS
(
SELECT subnet_sk, begin_address, end_address
FROM dhcp_range
UNION ALL
SELECT subnet_sk, address AS begin_address, address AS end_address
FROM ip_address
UNION ALL
SELECT subnet_sk, SUBSTRING(0x00000000000000000000000000000000, 1, DATALENGTH(ip_begin)) AS begin_address, ip_begin AS end_address
FROM subnet
UNION ALL
SELECT subnet_sk, ip_end AS begin_address, SUBSTRING(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 1, DATALENGTH(ip_end)) AS end_address
FROM subnet
)
,CTE_Gaps
AS
(
SELECT
subnet_sk
,end_address AS EndThisIsland
,LEAD(begin_address) OVER(PARTITION BY subnet_sk ORDER BY end_address) AS BeginNextIsland
FROM CTE_Islands
)
,CTE_GapsIncDec
AS
(
SELECT
subnet_sk
,EndThisIsland
,EndThisIslandInc
,BeginNextIslandDec
,BeginNextIsland
FROM CTE_Gaps
CROSS APPLY
(
SELECT bi.Result AS EndThisIslandInc
FROM dbo.BinaryInc(EndThisIsland) AS bi
) AS CA_Inc
CROSS APPLY
(
SELECT bd.Result AS BeginNextIslandDec
FROM dbo.BinaryDec(BeginNextIsland) AS bd
) AS CA_Dec
)
SELECT
subnet_sk
,EndThisIslandInc AS begin_range_AvailableIPAddress
,BeginNextIslandDec AS end_range_AvailableIPAddress
FROM CTE_GapsIncDec
WHERE CTE_GapsIncDec.EndThisIslandInc <> BeginNextIsland
ORDER BY subnet_sk, EndThisIsland;
Výsledková sada
subnet_sk begin_range_AvailableIPAddress end_range_AvailableIPAddress
1 0xAC101129 0xAC10112E
2 0xC0A81B1F 0xC0A81B1F
2 0xC0A81B22 0xC0A81B28
2 0xC0A81BFA 0xC0A81BFE
3 0xC0A8160C 0xC0A8160C
3 0xC0A816FE 0xC0A816FE
4 0xFC000000000000000000000000000001 0xFC0000000000000000000000000000FF
4 0xFC000000000000000000000000000101 0xFC0000000000000000000000000001FF
4 0xFC000000000000000000000000000201 0xFC0000000000000000000000000002FF
4 0xFC000000000000000000000000000301 0xFC0000000000000000000000000003FF
4 0xFC000000000000000000000000000401 0xFC0000000000000000000000000004FF
4 0xFC000000000000000000000000000501 0xFC0000000000000000000000000005FF
4 0xFC000000000000000000000000000601 0xFC0000000000000000000000000006FF
4 0xFC000000000000000000000000000701 0xFC0000000000000000000000000007FF
4 0xFC000000000000000000000000000801 0xFC0000000000000000000000000008FF
4 0xFC000000000000000000000000000901 0xFC00000000000000BFFFFFFFFFFFFFFD
4 0xFC00000000000000BFFFFFFFFFFFFFFF 0xFC00000000000000CFFFFFFFFFFFFFFD
4 0xFC00000000000000CFFFFFFFFFFFFFFF 0xFC00000000000000FBFFFFFFFFFFFFFD
4 0xFC00000000000000FBFFFFFFFFFFFFFF 0xFC00000000000000FCFFFFFFFFFFFFFD
4 0xFC00000000000000FCFFFFFFFFFFFFFF 0xFC00000000000000FFBFFFFFFFFFFFFD
4 0xFC00000000000000FFBFFFFFFFFFFFFF 0xFC00000000000000FFCFFFFFFFFFFFFD
4 0xFC00000000000000FFCFFFFFFFFFFFFF 0xFC00000000000000FFFBFFFFFFFFFFFD
4 0xFC00000000000000FFFBFFFFFFFFFFFF 0xFC00000000000000FFFCFFFFFFFFFFFD
4 0xFC00000000000000FFFCFFFFFFFFFFFF 0xFC00000000000000FFFFBFFFFFFFFFFD
4 0xFC00000000000000FFFFBFFFFFFFFFFF 0xFC00000000000000FFFFCFFFFFFFFFFD
4 0xFC00000000000000FFFFCFFFFFFFFFFF 0xFC00000000000000FFFFFBFFFFFFFFFD
4 0xFC00000000000000FFFFFBFFFFFFFFFF 0xFC00000000000000FFFFFCFFFFFFFFFD
4 0xFC00000000000000FFFFFCFFFFFFFFFF 0xFC00000000000000FFFFFFBFFFFFFFFD
4 0xFC00000000000000FFFFFFBFFFFFFFFF 0xFC00000000000000FFFFFFCFFFFFFFFD
4 0xFC00000000000000FFFFFFCFFFFFFFFF 0xFC00000000000000FFFFFFFBFFFFFFFD
4 0xFC00000000000000FFFFFFFBFFFFFFFF 0xFC00000000000000FFFFFFFCFFFFFFFD
4 0xFC00000000000000FFFFFFFCFFFFFFFF 0xFC00000000000000FFFFFFFFBFFFFFFD
4 0xFC00000000000000FFFFFFFFBFFFFFFF 0xFC00000000000000FFFFFFFFCFFFFFFD
4 0xFC00000000000000FFFFFFFFCFFFFFFF 0xFC00000000000000FFFFFFFFFBFFFFFD
4 0xFC00000000000000FFFFFFFFFBFFFFFF 0xFC00000000000000FFFFFFFFFCFFFFFD
4 0xFC00000000000000FFFFFFFFFCFFFFFF 0xFC00000000000000FFFFFFFFFFBFFFFD
4 0xFC00000000000000FFFFFFFFFFBFFFFF 0xFC00000000000000FFFFFFFFFFCFFFFD
4 0xFC00000000000000FFFFFFFFFFCFFFFF 0xFC00000000000000FFFFFFFFFFFBFFFD
4 0xFC00000000000000FFFFFFFFFFFBFFFF 0xFC00000000000000FFFFFFFFFFFCFFFD
4 0xFC00000000000000FFFFFFFFFFFCFFFF 0xFC00000000000000FFFFFFFFFFFFBFFD
4 0xFC00000000000000FFFFFFFFFFFFBFFF 0xFC00000000000000FFFFFFFFFFFFCFFD
4 0xFC00000000000000FFFFFFFFFFFFCFFF 0xFC00000000000000FFFFFFFFFFFFFBFD
4 0xFC00000000000000FFFFFFFFFFFFFBFF 0xFC00000000000000FFFFFFFFFFFFFCFD
4 0xFC00000000000000FFFFFFFFFFFFFCFF 0xFC00000000000000FFFFFFFFFFFFFFBD
4 0xFC00000000000000FFFFFFFFFFFFFFBF 0xFC00000000000000FFFFFFFFFFFFFFCD
4 0xFC00000000000000FFFFFFFFFFFFFFCF 0xFC0001065FFFFFFFFFFFFFFFFFFFFFFF
4 0xFC000106600000000000000100000000 0xFC00010666FFFFFFFFFFFFFFFFFFFFFF
4 0xFC000106670000000000000100000000 0xFC000106677FFFFFFFFFFFFFFFFFFFFF
4 0xFC000106678000000000000100000000 0xFC000106678FFFFFFFFFFFFFFFFFFFFF
4 0xFC000106679000000000000100000000 0xFC0001066800000000000000FFFFFFFE
Prováděcí plány
Byl jsem zvědavý, jak fungují různá řešení navrhovaná zde, a tak jsem se podíval na jejich prováděcí plány. Mějte na paměti, že tyto plány jsou pro malou ukázkovou sadu dat bez jakýchkoli indexů.
Moje obecné řešení pro IPv4 i IPv6:
Podobné řešení od dnoeth :
Řešení od cha který nepoužívá LEAD
funkce: