【问题标题】:With sql find next available integer within range that is not present in existing integer subset(s)使用 sql 在现有整数子集中不存在的范围内查找下一个可用整数
【发布时间】:2015-05-04 20:29:38
【问题描述】:

问题陈述:

给定一个范围 x -> y 的无符号整数
其中xy 都在0 -> 2n
范围内 和 n0 -> 32(或 64 在其他情况下)
找到最小可用值
不等于 xy
不在现有集合中
其中现有集合是x -> y的任意子集

我正在对数据库中的 IPv4 和 IPv6 子网进行建模。每个子网都由其起始地址和结束地址定义(我通过业务规则确保范围的完整性)。由于 IPv6 太大而无法存储在 bigint 数据类型中,我们将 IP 地址存储为 binary(4)binary(16)

相关数据存储在subnetdhcp_rangeip_address表中:

  • 子网: 子网范围由开始和结束 IP 地址定义并存储在subnet 表中。子网范围的大小始终为 2n(根据 CIDR/网络掩码的定义)。
  • 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,不适用于所有数据排列。递归 CTE 很麻烦,因为它被限制在 32,767 的最大大小(远小于潜在的范围大小)并且非常有可能非常慢。我可能可以克服递归 CTE 的问题,但在以下情况下查询会失败:

  • 当没有分配 IP 地址或 DHCP 范围时:什么都不返回
    应返回子网范围定义的所有 IP 地址
  • 分配多个 DHCP 范围时:返回 DHCP 范围内的 IP

为了帮助解决问题,我创建了一个包含三个子网的SQL Fiddle;每个都有不同的特征:切碎的、空的或大部分是连续的。上面的查询和小提琴中的设置都适用于大部分连续的子网,但对其他子网无效。还有一个GitHub Gist of the schema and example data

我已经努力使用递归和堆叠 CTE 生成数字序列,但如上所述,我担心它们的性能会很差,并且在递归 CTE 人为限制的情况下。 Aaron Bertrand 在他的系列 Generate a set or sequence without loops 中详细介绍了 CTE 的一些替代方案。遗憾的是,数据集对于数字表来说太大了,因为仅为 IPv4 地址空间创建一个需要 32 GB 的磁盘空间(SQL Server 存储 bigint values in 8 bytes)。我更喜欢动态生成序列,但还没有找到好方法。

或者,我尝试通过查看我知道的要使用的地址来为我的查询添加种子:

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_addressdhcp_range 表中没有任何内容时,上述查询不起作用。更糟糕的是,因为它不知道子网范围的边界,dhcp_range 朝向子网范围的上限将人为地限制返回的内容,因为查询无法从边缘的空白空间返回行。表现也不出众。

使用 SQL 或 TSQL 如何确定受其他范围限制的任意整数范围内的下一个最小可用整数值?

【问题讨论】:

  • 你必须在数据库中这样做吗?这看起来确实应该在代码中完成。这些年来,我花了很多时间构建疯狂的复杂 sql 查询,而您开始遇到无法很好调试的性能问题。
  • 如果您有最新的 SQL Server 之一,则有 LAG 和 LEAD 功能。它是什么版本的 SQL Server?
  • @CargoMeister 我没有必须在数据库中执行此操作,但限制集的数量可能很大,我想避免检索所有数据只是为了在应用层做出决定。如果事实证明这是最好的解决方案,那么我一定会这样做。
  • @cha 数据库在 SQL Server 2012 上运行。为了反映我更新了问题的标签。 LAGLEAD 函数是什么?它们在 SQL Server 2012 中可用吗?
  • 他们是。 LAG。试图建立一个小提琴。您的查询无效here。为什么?

标签: sql math sql-server-2012 network-programming range


【解决方案1】:

在这种情况下不需要递归,因为我们有 LEAD 函数。

我会从“缝隙”和“孤岛”的角度来思考问题。

我将首先关注 IPv4,因为使用它们进行算术运算更容易,但 IPv6 的想法是相同的,最后我将展示一个通用的解决方案。

首先,我们有一系列可能的 IP:从 0x000000000xFFFFFFFF

在此范围内有由dhcp_range:dhcp_range.begin_address, dhcp_range.end_address 中的范围(包括)定义的“孤岛”。您可以将分配的 IP 地址列表视为另一组岛,每个岛都有一个元素:ip_address.address, ip_address.address。最后,子网本身是两个孤岛:0x00000000, subnet.ipv4_beginsubnet.ipv4_end, 0xFFFFFFFF

我们知道这些岛屿重叠,这让我们的生活更轻松。岛屿可以彼此完美相邻。例如,当您连续分配的 IP 地址很少时,它们之间的差距为零。 在所有这些岛屿中,我们需要找到第一个空隙,它至少有一个元素,即非零空隙,即下一个岛屿在前一个岛屿结束后的一段距离处开始。

所以,我们将使用UNION (CTE_Islands) 将所有岛屿放在一起,然后按照end_address 的顺序遍历所有岛屿(或begin_address,使用上面有索引的字段)并使用LEAD 窥视并获取下一个岛屿的起始地址。最后我们将有一个表格,其中每一行都有当前岛的end_address 和下一个岛的begin_address (CTE_Diff)。如果它们之间的差异大于1,则意味着“差距”足够大,我们将返回当前岛屿的end_address加1。

给定子网的第一个可用 IP 地址

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;

如果至少有一个可用的 IP 地址,结果集将包含一行,如果没有可用的 IP 地址,则根本不包含行。

For parameter 1 result is `0xAC101129`.
For parameter 2 result is `0xC0A81B1F`.
For parameter 3 result is `0xC0A8160C`.

这是SQLFiddle 的链接。它不适用于参数,所以我在那里硬编码1。在 UNION 中将其更改为其他子网 ID(2 或 3)以尝试其他子网。此外,它没有正确显示varbinary 的结果,所以我将其保留为bigint。例如,使用 windows 计算器将其转换为十六进制以验证结果。

如果您不将结果限制在TOP(1) 的第一个间隙,您将获得所有可用 IP 范围(间隙)的列表。

给定子网的所有可用 IP 地址范围列表

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;

结果。 SQL Fiddle 结果为简单的 bigint,不是十六进制,并且带有硬编码的参数 ID。

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

每个子网的第一个可用 IP 地址

很容易扩展查询并返回所有子网的第一个可用 IP 地址,而不是指定一个特定的子网。使用CROSS APPLY 获取每个子网的岛屿列表,然后将PARTITION BY subnet_sk 添加到LEAD 函数中。

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

结果集

subnet_sk    NextAvailableIPAddress
1            0xAC101129
2            0xC0A81B1F
3            0xC0A8160C

这里是SQLFiddle。我不得不在 SQL Fiddle 中删除对varbinary 的转换,因为它显示的结果不正确。

IPv4 和 IPv6 通用解决方案

所有子网的所有可用 IP 地址范围

SQL Fiddle with sample IPv4 and IPv6 data, functions and final query

您的 IPv6 示例数据不太正确 - 子网结尾 0xFC00000000000000FFFFFFFFFFFFFFFF 小于您的 dhcp 范围,因此我将其更改为 0xFC0001066800000000000000FFFFFFFF。此外,您在同一子网中同时拥有 IPv4 和 IPv6,这处理起来很麻烦。为了这个例子,我稍微改变了你的模式——而不是在subnet 中明确地使用ipv4_begin / endipv6_begin / end,而是将ip_begin / end 设置为varbinary(16)(与其他表相同)。我还删除了address_family,否则对于 SQL Fiddle 来说太大了。

算术函数

为了使它适用于 IPv6,我们需要弄清楚如何在 binary(16) 中添加/减去 1。我会为它创建 CLR 函数。如果不允许您启用 CLR,则可以通过标准 T-SQL。我创建了两个返回表而不是标量的函数,因为这样它们可以被优化器内联。我想做一个通用的解决方案,所以该函数将接受 varbinary(16) 并适用于 IPv4 和 IPv6。

这是将varbinary(16) 加一的 T-SQL 函数。如果参数不是 16 字节长,我假设它是 IPv4,只需将其转换为 bigint 以添加 1,然后再返回 binary。否则,我将binary(16) 分成两部分,每部分长 8 个字节,然后将它们转换为bigintbigint 是有符号的,但我们需要无符号增量,所以我们需要检查几个案例。

else 部分是最常见的 - 我们只需将低部分加一并将结果附加到原始高部分。

如果低位是0xFFFFFFFFFFFFFFFF,那么我们将低位设置为0x0000000000000000并继续标志,即将高位加一。

如果低位是0x7FFFFFFFFFFFFFFF,那么我们将低位显式设置为0x8000000000000000,因为尝试增加此bigint 值会导致溢出。

如果整数是0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,我们将结果设置为0x00000000000000000000000000000000

减一的功能类似。

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

所有子网的所有可用 IP 地址范围

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;

结果集

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

执行计划

我很想知道这里建议的不同解决方案是如何工作的,所以我查看了他们的执行计划。请记住,这些计划适用于没有任何索引的小样本数据集。

我对 IPv4 和 IPv6 的通用解决方案:

dnoeth 的类似解决方案:

cha 不使用LEAD 函数的解决方案:

【讨论】:

  • 弗拉基米尔 我直到星期一才离开办公室,但我会先试一试。诚然,此时我还没有尝试修改您的查询,但如果我想获取所有可用的 IP 地址,如何实现?
  • @ahsteele,我在答案中提到了它。在具有子网ID 参数的第一个变体中,删除TOP(1),您将获得所有可用范围的列表。不是个人 IP 地址,而是范围。我会用例子更新答案。
  • 这就是我在 iPhone 上阅读和评论您的答案所得到的。 #尴尬
  • @ahsteele,我在答案中添加了示例和 SQL Fiddle。
  • 感谢您的更新。就 IPv6 而言,这是我们将数据拆分为我们可以处理的块的方式:stackoverflow.com/a/28648716/61654。不确定这是否有助于您的 IPv6 解决方案。
【解决方案2】:

经过深思熟虑,我相信像这样简单的查询就可以了:

with a as(
  -- next ip address
  select n.next_address, i.subnet_sk
  from ip_address i
  CROSS APPLY (SELECT convert(binary(4), convert(bigint, i.address) + 1) AS next_address) as n
  where n.next_address NOT IN (SELECT address FROM ip_address)
  AND EXISTS (SELECT 1 FROM subnet s WHERE s.subnet_sk = i.subnet_sk and n.next_address > s.ipv4_begin and n.next_address < s.ipv4_end)

  UNION -- use UNION here, not UNION ALL to remove duplicates

  -- first ip address for completely unassigned subnets
  SELECT next_address, subnet_sk
  FROM subnet 
  CROSS APPLY (SELECT convert(binary(4), convert(bigint, ipv4_begin) + 1) AS next_address) n
  where n.next_address NOT IN (SELECT address FROM ip_address)

  UNION -- use UNION here, not UNION ALL to remove duplicates

  -- next ip address from dhcp ranges
  SELECT next_address, subnet_sk
  FROM dhcp_range
  CROSS APPLY (SELECT convert(binary(4), convert(bigint, end_address) + 1) AS next_address) n
  where n.next_address NOT IN (SELECT address FROM ip_address)
)
SELECT min(next_address), subnet_sk
FROM a WHERE NOT exists(SELECT 1 FROM dhcp_range dhcp
         WHERE a.subnet_sk = dhcp.subnet_sk and a.next_address
            between dhcp.begin_address
                and dhcp.end_address)
GROUP BY subnet_sk

它适用于 IPV4,但可以轻松扩展为 IPV6

SQLFiddle

每个子网的结果:

           subnet_sk
---------- -----------
0xAC101129 1
0xC0A81B1F 2
0xC0A8160C 3

(3 row(s) affected)

在我看来,它应该非常快。请检查一下

【讨论】:

  • 遗憾的是,您在这里所拥有的有点离题。我相信这是因为我的问题存在一些问题,所以我回去完善了它。我将建立一个 SQL Fiddle 来说明我的数据集,但目前不能,因为它已关闭。任何进一步的帮助将不胜感激。
  • 我已经更新了答案。正如您正确指出的那样,SQLFiddle 当前已关闭。我只在答案的开头添加了查询。根据您的数据检查它们是否更快
  • 由于这些不是递归 CTE,它们的速度非常快。可悲的是,它们并不完全正确。我发现了两个问题:1) 返回位于 DHCP 范围内的地址,2) 当没有将 IP 地址或 DHCP 范围分配给子网时,找不到任何地址。上述查询中没有考虑子网范围。这解释了第二个问题,但我不确定为什么会返回 DHCP 范围内的地址。
  • dhcp_range 表的用途是什么?我认为它包含您希望找到可用 IP 地址的实际 DHCP 范围,即您有一个范围 192.168.1.1 - 192.168.1.254 并且当前有一些 IP 租约处于活动状态。我虽然你想在这个范围内找到第一个可用的 IP 地址。你能详细说明一下吗?
  • dhcp_range 描述了将由 DHCP 服务器分发的地址范围。因此,这些范围内的地址无法分配。 ip_address 表的地址已经分配,​​因此也不可用。
【解决方案3】:

这是我通常尝试用 +1/-1 上的简单累积和来解决的问题。

ip_address:ip对ip_address不可用,但以ip_address + 1开头可用

子网:ip 不适用于 ipv4_end,但可用 ipv4_begin + 1 声明

dhcp_range:ip在begin_address后不可用,但从end_address + 1开始可用

现在将所有按 ip 地址排序的 +1/-1 相加,只要它大于零,它就是一系列免费提示的开始,现在下一行的 ip 是使用范围的开始。

SELECT
   subnet_sk
  ,ip_begin
  ,ip_end
FROM
 (
   SELECT
      subnet_sk
     ,ip AS ip_begin
    -- ,x
     ,LEAD(ip)
      OVER (ORDER BY ip, x) - 1 AS ip_end
     ,SUM(x)
      OVER (ORDER BY ip, x 
            ROWS UNBOUNDED PRECEDING) AS avail
   FROM
   (
      SELECT
         subnet_sk, CAST(ipv4_begin AS BIGINT)+1 AS ip, 1 AS x 
      FROM subnet
   --   WHERE subnet_sk = 1

      UNION ALL

      SELECT
         subnet_sk, CAST(ipv4_end AS BIGINT), -1 
      FROM subnet
   --   WHERE subnet_sk = 1

      UNION ALL

      SELECT
         subnet_sk, CAST(begin_address AS BIGINT), -1
      FROM dhcp_range
   --   WHERE subnet_sk = 1

      UNION ALL

      SELECT
         subnet_sk, CAST(end_address AS BIGINT)+1, 1 
      FROM dhcp_range
   --   WHERE subnet_sk = 1

      UNION ALL

      SELECT
         subnet_sk, CAST(address AS BIGINT), -1 
      FROM ip_address
   --   WHERE subnet_sk = 1

      UNION ALL

      SELECT
         subnet_sk, CAST(address AS BIGINT)+1, 1 
      FROM ip_address
   --   WHERE subnet_sk = 1
   ) AS dt
 ) AS dt
WHERE avail > 0

这将返回所有可用范围,对于单个子网,只需取消注释 WHERE 条件:fiddle

【讨论】:

  • 我真的很喜欢这个答案。让我对这个问题有了不同的看法。与注册 DHCP 范围相比,注册静态范围可能更有意义。
  • 你有一个可以适应 IPv6 的排列吗?例如,在对 IPv6 地址执行数学运算时,我们之前已将它们切碎:stackoverflow.com/a/28648716/61654。我试图创建一个SQL Fiddle with IPv6 demo data,但无法构建架构,所以如果你想在本地尝试,这里是一个Gist of the schema creation to include IPv6 data
  • 对不起,我没有详细研究过IPv6,所以我不知道如何排序/比较它们或加/减1的规则。
【解决方案4】:

我有点不清楚您的数据到底是什么样子。问题陈述虽然表述得很好,但似乎与查询关系不大。

让我假设dhcp_range 有数据。您想要的查询是:

SELECT COALESCE(MIN(dr.end_address) + 1, 0)
FROM dhcp_range dr
WHERE NOT EXISTS (SELECT 1
                  FROM dhcp_range dr2
                  WHERE dr.end_address + 1 BETWEEN dr.start_address AND dr.end_address
                 );

【讨论】:

  • 查询是解决问题的另一种方法。我试图用来解决问题陈述的 SQL 从未真正起作用。我发帖是为了说明对逆问题的思考。解释表格(当我在电脑前将其编辑成问题):范围在subnet 表中定义。 dhcp_range 是一个限制集,可以有多个范围。另一个是在ip_address 表中单独分配的地址。因此,给定一个特定子网,找到可分配的最小地址,受 0...N 个 DHCP 范围和 0...N 个 IP 地址的限制。
  • Gordon 我调整了这个问题,希望能更明确一点。让我知道是否需要进一步澄清。
猜你喜欢
  • 2020-11-22
  • 2021-12-23
  • 1970-01-01
  • 2014-12-19
  • 2021-01-07
  • 2021-08-08
  • 1970-01-01
  • 1970-01-01
  • 2017-02-06
相关资源
最近更新 更多