在子网中查找IP地址的SQL

3

我们最近开始使用MaxMind Geolite数据库来查找基于IP的城市。有很多关于将数据导入SQL Server的说明(我已经完成了)。现在我需要找出如何在子网中搜索给定的IP。

数据库架构:

CREATE TABLE GeoIP (   
    network varchar(20) not null,
    geoname_id varchar(20) not null,
    registered_country_geoname_id varchar(20) not null,
    represented_country_geoname_id varchar(20) not null,
    is_anonymous_proxy int,
    is_satellite_provider int,
    postal_code varchar(20),
    latitude Decimal(9,6),
    longitude Decimal(9,6),
    accuracy_radius int
); 

'network'列的行数据是IP地址/子网掩码(例如:1.0.32.0/19、1.0.64.0/20、1.0.80.0/22)。

给定一个单独的IP地址,我试图编写一个SELECT语句来返回geoname_id。

Ex: SELECT geoname_id FROM GeoIP where @user_ip in {some expression}

我希望能够在不将网络列拆分成'low_ip'和'high_ip' BIGINT列的情况下完成此操作。但是,如果这是唯一的方法,我也需要一些帮助来编写全局UPDATE语句以从现有数据中添加这些列。
需要适用于SQL SERVER 2008,因此无法使用任何Postgres等功能。
谢谢!
1个回答

5
从您的网络栏中,您已经可以看到网络掩码中的位数,并且通过一些位运算,轻松检测用户IP地址是否在该网络范围内。因此,建议您将该列拆分为它的(二进制)网络IP和其CIDR数字。
让我解释一下。如果我们以您提供的第一个示例(10.0.32.0/19)为例,我们可以看到它的网络掩码("/19"位)用19个1表示,所有其他位设置为零。
11111111 11111111 11100000 00000000

让我们以1.0.32.56为例,这是一个用户IP地址:

00000001 00000000 00100000 00111000

你可以看到,如果你将/19子网掩码与用户IP进行按位与操作,最终得到的结果是:

00000001 00000000 00100000 00000000

...将转换为点分十进制表示形式1.0.32.0。看起来熟悉吗?

无论如何,这是我解决您问题的方法。首先,我们需要使用udf将IP地址转换为二进制。我不要脸地从这个答案中窃取了一个:

CREATE FUNCTION dbo.fnBinaryIPv4(@ip AS VARCHAR(15)) RETURNS BINARY(4)
AS
BEGIN
    DECLARE @bin AS BINARY(4)

    SELECT @bin = CAST( CAST( PARSENAME( @ip, 4 ) AS INTEGER) AS BINARY(1))
            + CAST( CAST( PARSENAME( @ip, 3 ) AS INTEGER) AS BINARY(1))
            + CAST( CAST( PARSENAME( @ip, 2 ) AS INTEGER) AS BINARY(1))
            + CAST( CAST( PARSENAME( @ip, 1 ) AS INTEGER) AS BINARY(1))

    RETURN @bin
END
GO

我发现在一个小的查找表中拥有所有的子网掩码也很有帮助:
CREATE TABLE netmask (
    bits TINYINT PRIMARY KEY,
    binary_mask BINARY(4) NOT NULL
)

INSERT INTO netmask (bits, binary_mask) VALUES
    ( 0, 0x00000000), ( 1, 0x80000000), ( 2, 0xc0000000), ( 3, 0xe0000000),
    ( 4, 0xf0000000), ( 5, 0xf8000000), ( 6, 0xfc000000), ( 7, 0xfe000000),
    ( 8, 0xff000000), ( 9, 0xff800000), (10, 0xffc00000), (11, 0xffe00000),
    (12, 0xfff00000), (13, 0xfff80000), (14, 0xfffc0000), (15, 0xfffe0000),
    (16, 0xffff0000), (17, 0xffff8000), (18, 0xffffc000), (19, 0xffffe000),
    (20, 0xfffff000), (21, 0xfffff800), (22, 0xfffffc00), (23, 0xfffffe00),
    (24, 0xffffff00), (25, 0xffffff80), (26, 0xffffffc0), (27, 0xffffffe0),
    (28, 0xfffffff0), (29, 0xfffffff8), (30, 0xfffffffc), (31, 0xfffffffe),
    (32, 0xffffffff)

接下来,我们创建两个新列并填充它们:
ALTER TABLE GeoIP
ADD binary_network BINARY(4), network_bits TINYINT
GO

UPDATE GeoIP
SET binary_network = dbo.fnBinaryIPv4(SUBSTRING(network, 0, PATINDEX('%/%', network))),
    network_bits = CAST(SUBSTRING(network, PATINDEX('%/%', network) + 1, 3) AS TINYINT)

现在我们可以将您的查询重写为:

DECLARE @binary_user_ip BIGINT
SELECT @binary_user_ip = dbo.fnBinaryIPv4(@user_ip)

SELECT geoname_id
FROM GeoIP g
    JOIN netmask n ON g.network_bits = n.bits
WHERE @binary_user_ip & n.binary_mask = g.binary_network

注意 - 这仅适用于IPv4。如果您想检测IPv6子网,则一般方法相同,但字符串转换和算术运算会更加复杂。

这个很好用。你能解释一下这行代码的作用吗?(其中@binary_user_ip & n.binary_mask = g.binary_network) - Mike Smith
如果我将binary_network用作主要搜索列,那么我应该为该列创建索引吗? - Mike Smith
那一行是按位与。和我之前给你展示的二进制例子一样。如果binary_network是您的主要搜索列,那么几乎肯定应该对其进行索引。 - duckbenny
2
您的答案很好,但是 [netmask] 表插入子句中有一个错误。 二进制掩码位 [23] 的值应为 [0xfffffe00]。抱歉向管理员询问,我刚刚加入,声望还不够高以发表评论。 - Seth Aaronson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接