【问题标题】:SQL to find IP Address in SubnetSQL在子网中查找IP地址
【发布时间】:2017-10-07 04:36:04
【问题描述】:

我们最近开始使用 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
); 

“网络”列包含带有 IP/子网的行数据(例如:1.0.32.0/19、1.0.64.0/20、1.0.80.0/22)

给定一个 IP 地址,我正在尝试编写一个返回 geoname_id 的 SELECT 语句。

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

我想这样做,而不必将网络列分解为“low_ip”和“high_ip”BIGINT 列。但是,如果这是唯一的方法,我还可以使用一些帮助来了解如何编写全局 UPDATE 语句以从现有数据中添加这些列。

SQL SERVER 2008 需要此功能,因此不能使用任何酷炫的 Postgres 等功能。

谢谢!!

【问题讨论】:

    标签: sql-server


    【解决方案1】:

    从您的网络列中,您已经可以看到网络掩码中的位数,并且借助一点位算术,可以轻松检测用户 ip 是否属于该网络。因此,我建议您将该列拆分为其(二进制)网络 ip 及其 cidr 编号。

    让我解释一下。如果我们以您提供的第一个示例(10.0.32.0/19)为例,我们可以看到它的网络掩码(“/19”位)以二进制表示为 19 个,所有其他位都设置为零:

    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 地址转换为二进制。我无耻地偷了this answer的那个:

    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)。
    • 另外,如果我使用 binary_network 作为我的主要搜索列,我应该索引该列吗?
    • 那行是按位与。与我之前向您展示的二进制示例相同。是的,如果 binary_network 是您的主要搜索列,您几乎肯定应该对其进行索引。
    • @duckbenny 您的回答效果很好,但是 [netmask] 表插入子句中有一个错误。位 [23] 的 binary_mask 值应该是 [0xffffffe00]。向模组道歉,我刚刚加入,我的声誉太低,无法发表评论。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-24
    • 2016-05-05
    • 1970-01-01
    • 2012-11-29
    • 1970-01-01
    • 2015-12-06
    相关资源
    最近更新 更多