【问题标题】:Select record between two IP ranges在两个 IP 范围之间选择记录
【发布时间】:2016-03-23 11:50:14
【问题描述】:

我有一个存储IDNameCodeIPLowIPHigh 的表,例如:

1, Lucas, 804645, 192.130.1.1, 192.130.1.254
2, Maria, 222255, 192.168.2.1, 192.168.2.254
3, Julia, 123456, 192.150.3.1, 192.150.3.254

现在,如果我有一个 IP 地址192.168.2.50,我该如何检索匹配的记录?

编辑

根据 Gordon 的回答(我得到了编译错误),这就是我所拥有的:

select PersonnelPC.*
from (select PersonnelPC.*,
             (
              cast(parsename(iplow, 4)*1000000000 as decimal(12, 0)) +
              cast(parsename(iplow, 3)*1000000 as decimal(12, 0)) +
              cast(parsename(iplow, 2)*1000 as decimal(12, 0)) +
              (parsename(iplow, 1))
             ) as iplow_decimal,
            (
              cast(parsename(iphigh, 4)*1000000000 as decimal(12, 0)) +
              cast(parsename(iphigh, 3)*1000000 as decimal(12, 0)) +
              cast(parsename(iphigh, 2)*1000 as decimal(12, 0)) +
              (parsename(iphigh, 1))
             ) as iphigh_decimal
      from PersonnelPC
     ) PersonnelPC
where 192168002050 between iplow_decimal and iphigh_decimal;

但这给了我一个错误:

Msg 8115, Level 16, State 2, Line 1
Arithmetic overflow error converting expression to data type int.

有什么想法吗?

【问题讨论】:

  • 请描述您迄今为止所做的努力以及这些努力的结果。
  • 您存储了人类友好的地址文本表示,并且没有内置的比较可以理解该格式的 IP 范围语义。改用数字比较,例如How to search efficiently for IP addresses ranges?
  • 一定要测试子网不只是/24的情况
  • 将大值转换为 BIGINT 以避免算术溢出错误。更好的是,将其转换为 decimal(12,0) 以保持一致性。 (cast(192168002050 as decimal(12,0)))

标签: sql sql-server


【解决方案1】:

很痛苦。 SQL Server 具有糟糕的字符串操作功能。但是,它确实提供parsename()。此方法将 IP 地址转换为较大的十进制值以进行比较:

select t.*
from (select t.*,
             (cast(parsename(iplow, 4)*1000000000.0 as decimal(12, 0)) +
              cast(parsename(iplow, 3)*1000000.0 as decimal(12, 0)) +
              cast(parsename(iplow, 2)*1000.0 as decimal(12, 0)) +
              cast(parsename(iplow, 1) as decimal(12, 0))
             ) as iplow_decimal,
             (cast(parsename(iphigh, 4)*1000000000.0 as decimal(12, 0)) +
              cast(parsename(iphigh, 3)*1000000.0 as decimal(12, 0)) +
              cast(parsename(iphigh, 2)*1000.0 as decimal(12, 0)) +
              cast(parsename(iphigh, 1) as decimal(12, 0))
             ) as iphigh_decimal
      from t
     ) t
where 192168002050 between iplow_decimal and iphigh_decimal;

我应该注意,IP 地址通常以 4 字节无符号整数形式存储在数据库中。这使得比较容易得多。 . .尽管您需要复杂的逻辑(通常包含在函数中)将值转换为可读格式。

【讨论】:

  • 有趣地使用 base 10 转换使其可读(我使用 4x4 字节长来存储 IP)。强制转换会使这变慢 - 考虑将 IP 存储为非人类可读的,或者添加一个触发器来更新第二个字段,该字段存储与人类可读 IP 地址匹配的长值。
  • @Gordon - 我做了一个改变,我认为可以解决 OP 隐式转换为int 的问题。如有不妥请回帖。
  • @EdHarper 你能发表你的建议吗?
  • @alwaysVBNET - 我已经有了;此答案的当前版本应包含我的编辑(即将.0 添加到标量乘数)
  • @EdHarper 帮助很大
【解决方案2】:

试试这个检查范围的简单方法

DECLARE @IP NVARCHAR(30)='192.168.500.1'

SELECT  * FROM 
Branches
WHERE
CAST (PARSENAME(@IP,4) AS INT)>=CAST(PARSENAME(IPLow,4) AS INT) AND CAST(PARSENAME(@IP,3) AS INT)>=CAST(PARSENAME(IPLow,3) AS INT) AND CAST(PARSENAME(@IP,2) AS INT)>=CAST(PARSENAME(IPLow,2) AS INT) AND CAST(PARSENAME(@IP,1) AS INT)>=CAST(PARSENAME(IPLow,1) AS INT)
AND
CAST(PARSENAME( @IP,4) AS INT) <= CAST(PARSENAME(IPHigh ,4) AS INT) AND CAST(PARSENAME(@IP ,3) AS INT) <=CAST(PARSENAME(IPHigh ,3) AS INT) AND CAST(PARSENAME(@IP ,2) AS INT) <=CAST(PARSENAME(IPHigh ,2) AS INT) AND CAST(PARSENAME(@IP ,1) AS INT)<=CAST(PARSENAME(IPHigh ,1) AS INT)

需要根据@Ed Haper 的评论演员表。

【讨论】:

  • 它不会获取任何结果
  • 在其当前形式中,这将不起作用,因为PARSENAME 返回字符串,并且在字符串比较中,50 > 254。您必须将所有调用的输出转换为 PARSENAME 作为数字类型 - 也许是 int - 才能使比较正常工作。另外 - 你确定你的比较是正确的吗?
  • @JaydipJ 对不起,它不起作用。在“192.168.130.1”-“192.168.130.254”范围内尝试“192.168.130.2”
  • @alwaysVBNET 我已根据 EdHarper 评论更新
  • @JaydipJ 使用一些括号修复。好答案+1
【解决方案3】:

使用此功能,您可以将任何 IP 地址转换为每个部分都有 3 位数字的形式。有了这个,您可以进行正常的字母数字比较。如果你愿意,你也可以返回 BIGINT...

CREATE FUNCTION dbo.IPWidth3(@IP VARCHAR(100))
RETURNS VARCHAR(15)
BEGIN
DECLARE @RetVal VARCHAR(15);
WITH Splitted AS
(
    SELECT CAST('<x>' + REPLACE(@IP,'.','</x><x>') + '</x>' AS XML) AS IPSplitted 
)
SELECT @RetVal = STUFF(
        (
        SELECT '.' + REPLACE(STR(Part.value('.','int'),3),' ','0')
        FROM Splitted.IPSplitted.nodes('/x') AS One(Part)
        FOR XML PATH('')
        ),1,1,'') 
FROM Splitted;

RETURN @RetVal;
END
GO

DECLARE @IP VARCHAR(100)='192.43.2.50';
SELECT dbo.IPWidth3(@IP);

结果

192.043.002.050

为了反映 Ed Harper 的评论,同样的函数返回 DECIMAL(12,0)

CREATE FUNCTION dbo.IP_as_Number(@IP VARCHAR(100))
RETURNS DECIMAL(12,0)
BEGIN
DECLARE @RetVal DECIMAL(12,0);
WITH Splitted AS
(
    SELECT CAST('<x>' + REPLACE(@IP,'.','</x><x>') + '</x>' AS XML) AS IPSplitted 
)
SELECT @RetVal = 
        CAST((
        SELECT REPLACE(STR(Part.value('.','int'),3),' ','0')
        FROM Splitted.IPSplitted.nodes('/x') AS One(Part)
        FOR XML PATH('')
        ) AS DECIMAL(12,0))
FROM Splitted;

RETURN @RetVal;
END
GO

DECLARE @IP VARCHAR(100)='192.43.2.50';
SELECT dbo.IP_as_Number(@IP);

【讨论】:

  • 如果您使用这种方法,请注意 .Net IPAddress.Parse 方法会将用前导零填充的 IPv4 地址部分解释为八进制值 - 因此您需要注意这些值的含义暴露于消费代码。这可能会影响其他语言。
  • @EdHarper,谢谢,不知道这个......我编辑了我的答案并添加了一个返回 DECIMAL(12,0) 的版本
【解决方案4】:

使用下面的 4 列获取 ipLow / IPHigh。您可以使用这些列来比较 Ips。

DECLARE@ip VARCHAR(50)='192.168.0.81' 
SELECT (SUBSTRING((@ip), 0,
patindex('%.%',
(@ip))))

,
substring((REPLACE(@ip, (SUBSTRING((@ip), 0,
patindex('%.%',
(@ip)) + 1)),
'')),
0,
patindex('%.%',
((REPLACE(@ip, (SUBSTRING((@ip), 0,
patindex('%.%',
(@ip)) + 1)),
''))))),
SUBSTRING((SUBSTRING(@ip, LEN((SUBSTRING((@ip), 0,
patindex('%.%',
(@ip))))) + 2 + LEN(substring((REPLACE(@ip, (SUBSTRING((@ip), 0,
patindex('%.%',
(@ip)) + 1)),
'')),
0,
patindex('%.%',
((REPLACE(@ip, (SUBSTRING((@ip), 0,
patindex('%.%',
(@ip)) + 1)),
'')))))) + 1,
LEN(@IP) - 1 - LEN(reverse(SUBSTRING(reverse(@ip), 0,
patindex('%.%',
reverse(@ip))))))), 0,
PATINDEX('%.%',
(SUBSTRING(@ip, LEN((SUBSTRING((@ip), 0,
patindex('%.%',
(@ip))))) + 2 + LEN(substring((REPLACE(@ip, (SUBSTRING((@ip), 0,
patindex('%.%',
(@ip)) + 1)),
'')),
0,
patindex('%.%',
((REPLACE(@ip, (SUBSTRING((@ip), 0,
patindex('%.%',
(@ip)) + 1)),
'')))))) + 1,
LEN(@IP) - 1 - LEN(reverse(SUBSTRING(reverse(@ip), 0,
patindex('%.%',
reverse(@ip))))))

))),
reverse(SUBSTRING(reverse(@ip), 0,
patindex('%.%',
reverse(@ip))))

【讨论】:

    【解决方案5】:

    考虑类似这个例子,将地址转换为数字。

    CREATE FUNCTION dbo.IPAddressAsNumber (@IPAddress AS varchar(15))
    RETURNS bigint
    BEGIN
    RETURN
     CONVERT (bigint,
      CONVERT(varbinary(1), CONVERT(int, PARSENAME(@IPAddress, 4))) +
      CONVERT(varbinary(1), CONVERT(int, PARSENAME(@IPAddress, 3))) +
      CONVERT(varbinary(1), CONVERT(int, PARSENAME(@IPAddress, 2))) +
      CONVERT(varbinary(1), CONVERT(int, PARSENAME(@IPAddress, 1))) )
    END
    

    这样您就可以使用 BETWEEN 等标准运算符来查找表中范围内的行

    DECLARE @t table (ID int, Name varchar(50), Code int, IPLow varchar(15), IPHigh varchar(15))
    INSERT INTO @t VALUES 
     (1, 'Lucas', 804645, '192.130.1.1', '192.130.1.254'),
     (2, 'Maria', 222255, '192.168.2.1', '192.168.2.254'),
     (3, 'Julia', 123456, '192.150.3.1', '192.150.3.254')
    
    SELECT * FROM @t
    WHERE dbo.IPAddressAsNumber('192.168.2.50')
     BETWEEN dbo.IPAddressAsNumber(IPLow) AND dbo.IPAddressAsNumber(IPHigh)
    

    该方案本质上使用 PARSENAME 来隔离地址的每一部分,将每一部分转换为 SQL 二进制字符串,将字符串连接在一起得到一个表示地址的 SQL 二进制字符串,并将结果显示为 bigint。

    在十六进制值的文本表示中,可以将这视为将 192(0xC0) + 168(0xA8) + 2(0x02) + 50(0x32) 的 4 个部分组合成 0xC0A80232。当您将该组合字符串转换为其二进制数字(0 和 1)时,您最终会得到一些可以被认为是网络堆栈在地址路由和子网掩码表中使用的二进制形式的地址。当您将其转换为无符号整数(或在本例中为 bigint)形式的数字时,您会得到 3232236082。

    有趣的是,这个方案为您提供了一个“数字”,可以通过多种方式代替原始地址。例如,您可以 ping 数字 2130706433 而不是地址 127.0.0.1 - Windows 中的名称解析器会将其转换为类似于使用 DNS 查找主机名地址的方式。

    为了完整起见,这里还有一个函数可以用来将数字形式转换回标准字符串形式

    CREATE FUNCTION dbo.IPAddressFromNumber (@IPNumber AS bigint)
    RETURNS varchar(15)
    BEGIN
    RETURN
     CONVERT (varchar(15),
      CONVERT(varchar(3), CONVERT(int, SUBSTRING(CONVERT(varbinary(4), @IPNumber), 1,1))) + '.' +
      CONVERT(varchar(3), CONVERT(int, SUBSTRING(CONVERT(varbinary(4), @IPNumber), 2,1))) + '.' +
      CONVERT(varchar(3), CONVERT(int, SUBSTRING(CONVERT(varbinary(4), @IPNumber), 3,1))) + '.' +
      CONVERT(varchar(3), CONVERT(int, SUBSTRING(CONVERT(varbinary(4), @IPNumber), 4,1))) )
    END
    

    【讨论】:

      【解决方案6】:
      select *
      from ip a
      join ip_details b
      on a.ip_address >= b.ip_start
      and a.ip_address <= b.ip_end;
      

      其中,表“a”包含 IP 地址列表,表“b”包含 IP 范围。

      我们可以直接比较字符串,而不是将ip地址转换为数字,它会逐字节比较。

      这对我有用(PostgreSQL)。

      【讨论】:

        【解决方案7】:

        我按照 Gordon 的回答进行思考,然后意识到您实际上并不需要弄乱数字。如果您对地址的每个部分进行零填充,则可以进行字符串比较:

        DECLARE @search varchar(50) = '192.168.2.50';
        WITH DATA AS (
            SELECT * FROM ( values 
                    (1, 'Lucas', '192.130.1.1', '192.130.1.254'),
                    (2, 'Maria', '192.168.2.1', '192.168.2.254'),
                    (3, 'Julia', '192.150.3.1', '192.150.3.254')
            ) AS tbl (ID,Name,IPLow,IPHigh)
        )
        SELECT *
        FROM DATA
        WHERE REPLACE(STR(PARSENAME( @search, 4 ), 3, 0), ' ', '0')
            + REPLACE(STR(PARSENAME( @search, 3 ), 3, 0), ' ', '0')
            + REPLACE(STR(PARSENAME( @search, 2 ), 3, 0), ' ', '0')
            + REPLACE(STR(PARSENAME( @search, 1 ), 3, 0), ' ', '0')
        
            BETWEEN
        
              REPLACE(STR(PARSENAME( IPLow, 4 ), 3, 0), ' ', '0')
            + REPLACE(STR(PARSENAME( IPLow, 3 ), 3, 0), ' ', '0')
            + REPLACE(STR(PARSENAME( IPLow, 2 ), 3, 0), ' ', '0')
            + REPLACE(STR(PARSENAME( IPLow, 1 ), 3, 0), ' ', '0')
        
            AND
        
              REPLACE(STR(PARSENAME( IPHigh, 4 ), 3, 0), ' ', '0')
            + REPLACE(STR(PARSENAME( IPHigh, 3 ), 3, 0), ' ', '0')
            + REPLACE(STR(PARSENAME( IPHigh, 2 ), 3, 0), ' ', '0')
            + REPLACE(STR(PARSENAME( IPHigh, 1 ), 3, 0), ' ', '0')
        

        当然,为了简单起见,您可以将其放在 UDF 中,但要注意大型查询对性能的影响。

        CREATE FUNCTION dbo.IP_Comparable(@IP varchar(50))
        RETURNS varchar(50)
        WITH SCHEMABINDING
        BEGIN
            RETURN REPLACE(STR(PARSENAME( @IP, 4 ), 3, 0), ' ', '0')
                 + REPLACE(STR(PARSENAME( @IP, 3 ), 3, 0), ' ', '0')
                 + REPLACE(STR(PARSENAME( @IP, 2 ), 3, 0), ' ', '0')
                 + REPLACE(STR(PARSENAME( @IP, 1 ), 3, 0), ' ', '0')
        END
        GO
        
        DECLARE @search varchar(50) = '192.168.2.50';
        WITH DATA AS (
            SELECT * FROM ( values 
                (1, 'Lucas', '192.130.1.1', '192.130.1.254'),
                (2, 'Maria', '192.168.2.1', '192.168.2.254'),
                (3, 'Julia', '192.150.3.1', '192.150.3.254')
            ) AS tbl (ID,Name,IPLow,IPHigh)
        )
        SELECT *
        FROM DATA
        WHERE dbo.IP_Comparable(@search) BETWEEN dbo.IP_Comparable(IPLow) AND dbo.IP_Comparable(IPHigh)
        

        这将避免整数溢出问题。

        【讨论】:

          【解决方案8】:

          取决于您正在寻找的最高记录或最低记录。

          select * from table where IPlow like '192.168.2.50' or IPHigh like '192.168.2.50'
          

          【讨论】:

          • 显然范围是未知的,应该从数据中可用的内容中搜索。否则就太容易了。
          • 投反对票,因为这种方法不能解决问题。
          猜你喜欢
          • 2017-02-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-02-02
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多