Dans ce cas, aucune récursivité n'est nécessaire, car nous avons LEAD
fonction.
Je vais réfléchir au problème en termes de "lacunes" et d'"îlots".
Je vais d'abord me concentrer sur IPv4, car il est plus facile de faire de l'arithmétique avec eux, mais l'idée pour IPv6 est la même et à la fin je montrerai une solution générique.
Pour commencer, nous avons une gamme complète d'IP possibles :de 0x00000000
à 0xFFFFFFFF
.
À l'intérieur de cette plage, il y a des "îlots" définis par les plages (inclusives) dans dhcp_range
:dhcp_range.begin_address, dhcp_range.end_address
. Vous pouvez considérer la liste des adresses IP attribuées comme un autre ensemble d'îlots, qui ont chacun un élément :ip_address.address, ip_address.address
. Enfin, le sous-réseau lui-même est composé de deux îles :0x00000000, subnet.ipv4_begin
et subnet.ipv4_end, 0xFFFFFFFF
.
Nous savons que ces îles ne font pas superposition, ce qui nous facilite la vie. Les îles peuvent être parfaitement adjacentes les unes aux autres. Par exemple, lorsque vous avez peu d'adresses IP attribuées consécutivement, l'écart entre elles est nul. Parmi toutes ces îles, nous devons trouver le premier écart, qui a au moins un élément, c'est-à-dire un écart non nul, c'est-à-dire que l'île suivante commence à une certaine distance après la fin de l'île précédente.
Donc, nous allons assembler toutes les îles en utilisant UNION
(CTE_Islands
) puis parcourez-les tous dans l'ordre end_address
(ou begin_address
, utilisez le champ qui contient un index) et utilisez LEAD
pour jeter un coup d'œil et obtenir l'adresse de départ de l'île suivante. À la fin, nous aurons une table, où chaque ligne avait end_address
de l'île actuelle et begin_address
de l'île suivante (CTE_Diff
). Si la différence entre eux est supérieure à un, cela signifie que "l'écart" est suffisamment large et nous renverrons l'end_address
de l'île actuelle plus 1.
La première adresse IP disponible pour le sous-réseau donné
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;
L'ensemble de résultats contiendra une ligne s'il y a au moins une adresse IP disponible et ne contiendra aucune ligne s'il n'y a pas d'adresse IP disponible.
For parameter 1 result is `0xAC101129`.
For parameter 2 result is `0xC0A81B1F`.
For parameter 3 result is `0xC0A8160C`.
Voici un lien vers SQLFiddle
. Cela n'a pas fonctionné avec le paramètre, donc j'ai codé en dur 1
là. Changez-le dans UNION en un autre ID de sous-réseau (2 ou 3) pour essayer d'autres sous-réseaux. De plus, il n'affichait pas de résultat dans varbinary
correctement, donc je l'ai laissé comme bigint. Utilisez, par exemple, la calculatrice Windows pour le convertir en hexadécimal afin de vérifier le résultat.
Si vous ne limitez pas les résultats au premier écart par TOP(1)
, vous obtiendrez une liste de toutes les plages d'adresses IP disponibles (écarts).
Liste de toutes les plages d'adresses IP disponibles pour un sous-réseau donné
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;
Résultat. SQL Fiddle avec un résultat en bigint simple, pas en hexadécimal, et avec un ID de paramètre codé en dur.
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
La première adresse IP disponible pour chaque sous-réseau
Il est facile d'étendre la requête et de renvoyer la première adresse IP disponible pour tous les sous-réseaux, plutôt que de spécifier un sous-réseau particulier. Utilisez CROSS APPLY
pour obtenir la liste des îles pour chaque sous-réseau, puis ajoutez PARTITION BY subnet_sk
dans le LEAD
fonction.
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
Ensemble de résultats
subnet_sk NextAvailableIPAddress
1 0xAC101129
2 0xC0A81B1F
3 0xC0A8160C
Voici SQLFiddle
. J'ai dû supprimer la conversion en varbinary
dans SQL Fiddle, car il affichait les résultats de manière incorrecte.
Solution générique pour IPv4 et IPv6
Toutes les plages d'adresses IP disponibles pour tous les sous-réseaux
SQL Fiddle avec exemples de données IPv4 et IPv6, fonctions et requête finale
Vos exemples de données pour IPv6 n'étaient pas tout à fait corrects - la fin du sous-réseau 0xFC00000000000000FFFFFFFFFFFFFFFF
était inférieur à vos plages DHCP, j'ai donc changé cela en 0xFC0001066800000000000000FFFFFFFF
. De plus, vous aviez à la fois IPv4 et IPv6 dans le même sous-réseau, ce qui est fastidieux à gérer. Pour les besoins de cet exemple, j'ai un peu modifié votre schéma - au lieu d'avoir un ipv4_begin / end
explicite et ipv6_begin / end
dans subnet
Je l'ai fait juste ip_begin / end
comme varbinary(16)
(comme pour vos autres tables). J'ai également supprimé address_family
, sinon c'était trop gros pour SQL Fiddle.
Fonctions arithmétiques
Pour que cela fonctionne pour IPv6, nous devons comprendre comment ajouter/soustraire 1
vers/depuis binary(16)
. Je ferais fonctionner CLR pour cela. Si vous n'êtes pas autorisé à activer le CLR, cela est possible via le T-SQL standard. J'ai créé deux fonctions qui renvoient une table, plutôt qu'un scalaire, car elles peuvent ainsi être alignées par l'optimiseur. Je voulais faire une solution générique, donc la fonction accepterait varbinary(16)
et fonctionne à la fois pour IPv4 et IPv6.
Voici la fonction T-SQL pour incrémenter varbinary(16)
par un. Si le paramètre ne fait pas 16 octets, je suppose qu'il s'agit d'IPv4 et je le convertis simplement en bigint
ajouter 1
puis retour à binary
. Sinon, je divise binary(16)
en deux parties de 8 octets chacune et les caster en bigint
. bigint
est signé, mais nous avons besoin d'un incrément non signé, nous devons donc vérifier quelques cas.
Le else
partie est la plus courante - nous incrémentons simplement la partie basse de un et ajoutons le résultat à la partie haute d'origine.
Si la partie basse est 0xFFFFFFFFFFFFFFFF
, puis nous définissons la partie basse sur 0x0000000000000000
et reporter le drapeau, c'est-à-dire incrémenter la partie haute de un.
Si la partie basse est 0x7FFFFFFFFFFFFFFF
, puis nous définissons la partie basse sur 0x8000000000000000
explicitement, car une tentative d'incrémentation de ce bigint
la valeur provoquerait un débordement.
Si le nombre entier est 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
nous définissons le résultat sur 0x00000000000000000000000000000000
.
La fonction pour décrémenter de un est similaire.
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
Toutes les plages d'adresses IP disponibles pour tous les sous-réseaux
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;
Ensemble de résultats
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
Plans d'exécution
J'étais curieux de voir comment les différentes solutions suggérées ici fonctionnent, alors j'ai regardé leurs plans d'exécution. Gardez à l'esprit que ces plans concernent le petit échantillon de données sans aucun index.
Ma solution générique pour IPv4 et IPv6 :
Solution similaire par dnoeth :
Solution par cha qui n'utilise pas LEAD
fonction :