In questo caso non è necessaria la ricorsione, perché abbiamo LEAD
funzione.
Penserò al problema in termini di "lacune" e "isole".
Mi concentrerò inizialmente su IPv4, perché è più facile fare aritmetica con loro, ma l'idea per IPv6 è la stessa e alla fine mostrerò una soluzione generica.
Per cominciare, abbiamo una gamma completa di IP possibili:da 0x00000000
a 0xFFFFFFFF
.
All'interno di questo intervallo ci sono "isole" definite dagli intervalli (inclusi) in dhcp_range
:dhcp_range.begin_address, dhcp_range.end_address
. Puoi pensare all'elenco degli indirizzi IP assegnati come a un altro insieme di isole, che hanno un elemento ciascuna:ip_address.address, ip_address.address
. Infine, la sottorete stessa è composta da due isole:0x00000000, subnet.ipv4_begin
e subnet.ipv4_end, 0xFFFFFFFF
.
Sappiamo che queste isole non sovrapposizione, il che rende la nostra vita più facile. Le isole possono essere perfettamente adiacenti l'una all'altra. Ad esempio, quando hai pochi indirizzi IP allocati consecutivamente, il gap tra loro è zero. Tra tutte queste isole dobbiamo trovare il primo gap, che ha almeno un elemento, cioè gap diverso da zero, cioè l'isola successiva inizia a una certa distanza dopo la fine dell'isola precedente.
Quindi, metteremo insieme tutte le isole usando UNION
(CTE_Islands
) e quindi esaminarli tutti nell'ordine di end_address
(o begin_address
, usa il campo che contiene l'indice) e usa LEAD
per sbirciare avanti e ottenere l'indirizzo di partenza della prossima isola. Alla fine avremo una tabella, in cui ogni riga aveva end_address
dell'isola corrente e begin_address
dell'isola successiva (CTE_Diff
). Se la differenza tra loro è più di uno, significa che il "gap" è abbastanza ampio e restituiremo il end_address
dell'isola attuale più 1.
Il primo indirizzo IP disponibile per la sottorete specificata
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;
Il set di risultati conterrebbe una riga se è disponibile almeno un indirizzo IP e non conterrebbe affatto righe se non ci sono indirizzi IP disponibili.
For parameter 1 result is `0xAC101129`.
For parameter 2 result is `0xC0A81B1F`.
For parameter 3 result is `0xC0A8160C`.
Ecco un link a SQLFiddle
. Non ha funzionato con il parametro, quindi ho codificato 1
là. Modificarlo in UNION in un altro ID di sottorete (2 o 3) per provare altre sottoreti. Inoltre, non mostrava il risultato in varbinary
correttamente, quindi l'ho lasciato come bigint. Usa, ad esempio, la calcolatrice di Windows per convertirlo in esadecimale per verificare il risultato.
Se non limiti i risultati al primo spazio vuoto di TOP(1)
, otterrai un elenco di tutti gli intervalli IP disponibili (gap).
Elenco di tutti gli intervalli di indirizzi IP disponibili per una determinata sottorete
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;
Risultato. SQL Fiddle con risultato come bigint semplice, non in esadecimale e con ID parametro codificato.
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
Il primo indirizzo IP disponibile per ogni sottorete
È facile estendere la query e restituire il primo indirizzo IP disponibile per tutte le sottoreti, invece di specificare una particolare sottorete. Usa CROSS APPLY
per ottenere l'elenco delle isole per ciascuna sottorete e quindi aggiungere PARTITION BY subnet_sk
nel LEAD
funzione.
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
Risultato impostato
subnet_sk NextAvailableIPAddress
1 0xAC101129
2 0xC0A81B1F
3 0xC0A8160C
Ecco SQLFiddle
. Ho dovuto rimuovere la conversione in varbinary
in SQL Fiddle, perché mostrava i risultati in modo errato.
Soluzione generica per IPv4 e IPv6
Tutti gli intervalli di indirizzi IP disponibili per tutte le sottoreti
SQL Fiddle con dati IPv4 e IPv6 di esempio, funzioni e query finale
I tuoi dati di esempio per IPv6 non erano del tutto corretti:la fine della sottorete 0xFC00000000000000FFFFFFFFFFFFFFFF
era inferiore ai tuoi intervalli DHCP, quindi l'ho cambiato in 0xFC0001066800000000000000FFFFFFFF
. Inoltre, avevi sia IPv4 che IPv6 nella stessa sottorete, il che è complicato da gestire. Per il bene di questo esempio ho leggermente modificato il tuo schema, invece di avere ipv4_begin / end
esplicito e ipv6_begin / end
in subnet
L'ho fatto solo ip_begin / end
come varbinary(16)
(come per gli altri tuoi tavoli). Ho anche rimosso address_family
, altrimenti era troppo grande per SQL Fiddle.
Funzioni aritmetiche
Per farlo funzionare per IPv6 dobbiamo capire come aggiungere/sottrarre 1
a/da binary(16)
. Farei la funzione CLR per questo. Se non sei autorizzato ad abilitare CLR, è possibile tramite T-SQL standard. Ho creato due funzioni che restituiscono una tabella, anziché scalare, perché in questo modo possono essere integrate dall'ottimizzatore. Volevo creare una soluzione generica, quindi la funzione avrebbe accettato varbinary(16)
e funziona sia per IPv4 che per IPv6.
Ecco la funzione T-SQL per incrementare varbinary(16)
di uno. Se il parametro non è lungo 16 byte, presumo che sia IPv4 e lo converto semplicemente in bigint
per aggiungere 1
e poi di nuovo a binary
. Altrimenti, divido binary(16)
in due parti lunghe 8 byte ciascuna e lanciale in bigint
. bigint
è firmato, ma abbiamo bisogno di un incremento non firmato, quindi dobbiamo controllare alcuni casi.
Il else
parte è più comune:incrementiamo semplicemente la parte bassa di uno e aggiungiamo il risultato alla parte alta originale.
Se la parte bassa è 0xFFFFFFFFFFFFFFFF
, quindi impostiamo la parte bassa su 0x0000000000000000
e riporta la bandiera, cioè incrementa di uno la parte alta.
Se la parte bassa è 0x7FFFFFFFFFFFFFFF
, quindi impostiamo la parte bassa su 0x8000000000000000
esplicitamente, perché un tentativo di incrementare questo bigint
valore causerebbe un overflow.
Se il numero intero è 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
impostiamo il risultato su 0x00000000000000000000000000000000
.
La funzione per decrementare di uno è simile.
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
Tutti gli intervalli di indirizzi IP disponibili per tutte le sottoreti
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;
Risultato impostato
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
Piani di esecuzione
Ero curioso di vedere come funzionano le diverse soluzioni suggerite qui, quindi ho esaminato i loro piani di esecuzione. Tieni presente che questi piani riguardano il piccolo campione di dati senza indici.
La mia soluzione generica sia per IPv4 che per IPv6:
Soluzione simile di dnoeth :
Soluzione di cha che non utilizza LEAD
funzione: