Skip to content

PostgreSQL 网络地址函数和操作符

概述

PostgreSQL 提供了强大的网络地址处理能力,支持 IPv4、IPv6 地址以及 MAC 地址的存储和操作。本文详细介绍网络地址类型的操作符和函数,帮助您在实际项目中高效处理网络相关数据。

INFO

网络地址类型概览

PostgreSQL 支持以下网络地址类型:

  • inet:存储 IPv4 或 IPv6 主机地址,可选子网掩码
  • cidr:存储 IPv4 或 IPv6 网络地址
  • macaddr:存储 MAC 地址(6 字节)
  • macaddr8:存储 MAC 地址(8 字节,EUI-64 格式)

IP 地址操作符

子网包含性操作符

在网络管理、访问控制和安全策略中,判断 IP 地址是否属于特定子网是常见需求。PostgreSQL 提供了一套完整的包含性操作符。

严格包含操作符 <<>>

业务场景:网络访问控制

假设您在设计一个企业内网访问控制系统,需要判断客户端 IP 是否属于特定的办公区域网段。

sql
-- 检查单个IP是否属于子网
SELECT
    inet '192.168.1.5' << inet '192.168.1/24' as "IP在子网内",
    inet '192.168.0.5' << inet '192.168.1/24' as "IP不在子网内",
    inet '192.168.1/24' << inet '192.168.1/24' as "相同网段";

-- 结果:
-- IP在子网内 | IP不在子网内 | 相同网段
-- t         | f          | f
sql
-- 企业网络访问控制表
CREATE TABLE network_access_rules (
    rule_id SERIAL PRIMARY KEY,
    rule_name VARCHAR(100),
    allowed_network CIDR,
    access_level VARCHAR(20)
);

INSERT INTO network_access_rules VALUES
(1, '总部办公网', '192.168.1.0/24', '完全访问'),
(2, '研发部门', '192.168.10.0/24', '研发访问'),
(3, '访客网络', '192.168.100.0/24', '受限访问');

-- 检查客户端访问权限
SELECT
    rule_name,
    access_level,
    '192.168.1.25'::inet << allowed_network as "访问允许"
FROM network_access_rules
WHERE '192.168.1.25'::inet << allowed_network;

-- 结果:
-- rule_name | access_level | 访问允许
-- 总部办公网  | 完全访问      | t

分析过程:

  • << 操作符检查左侧地址是否严格包含在右侧网络中
  • 严格包含意味着不能相等,必须是真子集关系
  • 在访问控制场景中,这有助于区分具体主机和网段本身

包含或等于操作符 <<=>>=

sql
-- 网络层级管理示例
WITH network_hierarchy AS (
    SELECT * FROM (VALUES
        ('企业总网', '192.168.0.0/16'::cidr),
        ('办公区A', '192.168.1.0/24'::cidr),
        ('办公区B', '192.168.2.0/24'::cidr),
        ('服务器区', '192.168.10.0/24'::cidr)
    ) AS t(name, network)
)
SELECT
    a.name as "子网",
    b.name as "父网",
    a.network <<= b.network as "包含关系"
FROM network_hierarchy a, network_hierarchy b
WHERE a.name != b.name AND a.network <<= b.network;

-- 结果:
-- 子网    | 父网     | 包含关系
-- 办公区A  | 企业总网  | t
-- 办公区B  | 企业总网  | t
-- 服务器区 | 企业总网  | t

使用 `<<=` 和 `>>=` 操作符可以建立网络层级关系,这在网络拓扑管理和路由规划中非常有用。

重叠检测操作符 &&

sql
-- 网络冲突检测
CREATE TABLE planned_networks (
    project_id INT,
    project_name VARCHAR(50),
    network_range CIDR
);

INSERT INTO planned_networks VALUES
(1, '新办公楼A', '192.168.50.0/24'),
(2, '新办公楼B', '192.168.51.0/24'),
(3, '扩展网段', '192.168.50.128/25'); -- 与办公楼A有重叠

-- 检测网络规划冲突
SELECT
    a.project_name as "项目A",
    b.project_name as "项目B",
    a.network_range,
    b.network_range,
    a.network_range && b.network_range as "存在重叠"
FROM planned_networks a, planned_networks b
WHERE a.project_id < b.project_id
AND a.network_range && b.network_range;

-- 结果:
-- 项目A    | 项目B   | network_range  | network_range    | 存在重叠
-- 新办公楼A | 扩展网段 | 192.168.50.0/24| 192.168.50.128/25| t

按位运算操作符

按位运算在网络地址计算、子网划分和地址转换中发挥重要作用。

按位非运算 ~

sql
-- 计算网络地址的反码(用于某些网络计算)
SELECT
    '192.168.1.6'::inet as "原地址",
    ~ '192.168.1.6'::inet as "按位非",
    host(~ '192.168.1.6'::inet) as "反码主机部分";

-- 结果:
-- 原地址      | 按位非        | 反码主机部分
-- 192.168.1.6 | 63.87.254.249 | 63.87.254.249

按位与运算 & - 提取网络部分

sql
-- 从IP地址中提取特定部分
SELECT
    '192.168.1.157'::inet as "原始IP",
    '192.168.1.157'::inet & '255.255.255.0'::inet as "网络部分",
    '192.168.1.157'::inet & '0.0.0.255'::inet as "主机部分";

-- 结果:
-- 原始IP       | 网络部分      | 主机部分
-- 192.168.1.157| 192.168.1.0   | 0.0.0.157

实际应用:子网分析工具

sql
-- 创建IP分析函数
CREATE OR REPLACE FUNCTION analyze_ip(ip_addr inet, subnet_mask inet)
RETURNS TABLE(
    original_ip inet,
    network_part inet,
    host_part inet,
    subnet_class text
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        ip_addr,
        ip_addr & subnet_mask,
        ip_addr & (~ subnet_mask),
        CASE
            WHEN family(ip_addr) = 4 THEN
                CASE
                    WHEN (ip_addr & '255.0.0.0'::inet) = '10.0.0.0'::inet THEN 'A类私有'
                    WHEN (ip_addr & '255.240.0.0'::inet) = '172.16.0.0'::inet THEN 'B类私有'
                    WHEN (ip_addr & '255.255.0.0'::inet) = '192.168.0.0'::inet THEN 'C类私有'
                    ELSE '公网地址'
                END
            ELSE 'IPv6地址'
        END;
END;
$$ LANGUAGE plpgsql;

-- 使用示例
SELECT * FROM analyze_ip('192.168.1.100'::inet, '255.255.255.0'::inet);

-- 结果:
-- original_ip    | network_part   | host_part | subnet_class
-- 192.168.1.100  | 192.168.1.0    | 0.0.0.100 | C类私有

按位或运算 | - 地址合并

sql
-- 生成广播地址或合并地址段
SELECT
    '192.168.1.0'::inet as "网络地址",
    '0.0.0.255'::inet as "主机掩码",
    '192.168.1.0'::inet | '0.0.0.255'::inet as "广播地址";

-- 结果:
-- 网络地址     | 主机掩码      | 广播地址
-- 192.168.1.0  | 0.0.0.255     | 192.168.1.255

算术运算操作符

地址偏移运算 +-

在 DHCP 地址分配、负载均衡器配置等场景中,需要对 IP 地址进行算术运算。

sql
-- DHCP地址池管理示例
CREATE TABLE dhcp_pools (
    pool_id SERIAL PRIMARY KEY,
    pool_name VARCHAR(50),
    network_base INET,
    pool_start INTEGER,
    pool_end INTEGER
);

INSERT INTO dhcp_pools VALUES
(1, '办公区DHCP池', '192.168.1.0', 10, 200);

-- 生成可用IP地址列表
WITH RECURSIVE ip_range AS (
    SELECT
        pool_name,
        network_base + pool_start as current_ip,
        pool_start as current_offset,
        pool_end
    FROM dhcp_pools WHERE pool_id = 1

    UNION ALL

    SELECT
        pool_name,
        network_base + (current_offset + 1),
        current_offset + 1,
        pool_end
    FROM ip_range, dhcp_pools
    WHERE dhcp_pools.pool_id = 1
    AND current_offset < pool_end
    AND current_offset < pool_start + 5  -- 限制显示前5个
)
SELECT pool_name, current_ip, current_offset as "偏移量"
FROM ip_range;

-- 结果:
-- pool_name   | current_ip    | 偏移量
-- 办公区DHCP池 | 192.168.1.10  | 10
-- 办公区DHCP池 | 192.168.1.11  | 11
-- 办公区DHCP池 | 192.168.1.12  | 12
-- 办公区DHCP池 | 192.168.1.13  | 13
-- 办公区DHCP池 | 192.168.1.14  | 14
-- 办公区DHCP池 | 192.168.1.15  | 15

地址差值计算

sql
-- 计算IP地址范围大小
SELECT
    '192.168.1.100'::inet as "起始地址",
    '192.168.1.200'::inet as "结束地址",
    '192.168.1.200'::inet - '192.168.1.100'::inet as "地址数量",
    ('192.168.1.200'::inet - '192.168.1.100'::inet) + 1 as "包含边界的总数";

-- 结果:
-- 起始地址      | 结束地址      | 地址数量 | 包含边界的总数
-- 192.168.1.100 | 192.168.1.200 | 100     | 101

IP 地址函数

地址格式化函数

abbrev() - 创建缩写显示

sql
-- 比较不同格式的地址显示
SELECT
    '10.1.0.0/32'::inet as "原始格式",
    abbrev('10.1.0.0/32'::inet) as "inet缩写",
    '10.1.0.0/16'::cidr as "CIDR原始",
    abbrev('10.1.0.0/16'::cidr) as "CIDR缩写";

-- 结果:
-- 原始格式     | inet缩写  | CIDR原始    | CIDR缩写
-- 10.1.0.0/32  | 10.1.0.0  | 10.1.0.0/16 | 10.1/16

host()text() - 地址文本转换

sql
-- 从完整地址信息中提取主机部分
CREATE TABLE server_inventory (
    server_name VARCHAR(50),
    ip_address INET
);

INSERT INTO server_inventory VALUES
('Web服务器1', '192.168.1.10/24'),
('数据库服务器', '192.168.1.20/24'),
('文件服务器', '192.168.1.30/24');

SELECT
    server_name,
    ip_address as "完整地址",
    host(ip_address) as "主机IP",
    text(ip_address) as "完整文本格式"
FROM server_inventory;

-- 结果:
-- server_name | 完整地址        | 主机IP       | 完整文本格式
-- Web服务器1   | 192.168.1.10/24 | 192.168.1.10 | 192.168.1.10/24
-- 数据库服务器 | 192.168.1.20/24 | 192.168.1.20 | 192.168.1.20/24
-- 文件服务器   | 192.168.1.30/24 | 192.168.1.30 | 192.168.1.30/24

网络计算函数

broadcast() - 计算广播地址

sql
-- 网络广播地址计算(用于网络唤醒等功能)
SELECT
    '192.168.1.5/24'::inet as "任意主机",
    broadcast('192.168.1.5/24'::inet) as "广播地址",
    '10.0.0.1/8'::inet as "大型网络",
    broadcast('10.0.0.1/8'::inet) as "大型网络广播";

-- 结果:
-- 任意主机        | 广播地址         | 大型网络   | 大型网络广播
-- 192.168.1.5/24  | 192.168.1.255/24 | 10.0.0.1/8 | 10.255.255.255/8

netmask()hostmask() - 掩码计算

sql
-- 子网掩码和主机掩码计算
SELECT
    '192.168.23.20/30'::inet as "原地址",
    netmask('192.168.23.20/30'::inet) as "子网掩码",
    hostmask('192.168.23.20/30'::inet) as "主机掩码",
    masklen('192.168.23.20/30'::inet) as "掩码长度";

-- 结果:
-- 原地址            | 子网掩码        | 主机掩码    | 掩码长度
-- 192.168.23.20/30  | 255.255.255.252 | 0.0.0.3     | 30

实际应用:子网规划计算器

sql
CREATE OR REPLACE FUNCTION subnet_calculator(
    base_network INET,
    required_hosts INTEGER
)
RETURNS TABLE(
    network_address CIDR,
    subnet_mask INET,
    host_mask INET,
    broadcast_addr INET,
    first_usable INET,
    last_usable INET,
    total_hosts BIGINT,
    usable_hosts BIGINT
) AS $$
DECLARE
    required_bits INTEGER;
    subnet_bits INTEGER;
BEGIN
    -- 计算需要的主机位数
    required_bits := CEIL(LOG(2, required_hosts + 2));  -- +2 for network and broadcast
    subnet_bits := 32 - required_bits;

    -- 调整网络地址的掩码
    base_network := set_masklen(base_network, subnet_bits);

    RETURN QUERY
    SELECT
        network(base_network),
        netmask(base_network),
        hostmask(base_network),
        broadcast(base_network),
        network(base_network) + 1,
        broadcast(base_network) - 1,
        2::BIGINT ^ required_bits,
        (2::BIGINT ^ required_bits) - 2;
END;
$$ LANGUAGE plpgsql;

-- 使用示例:为100台主机规划子网
SELECT * FROM subnet_calculator('192.168.1.0/24'::inet, 100);

-- 结果:
-- network_address | subnet_mask     | host_mask   | broadcast_addr   | first_usable  | last_usable   | total_hosts | usable_hosts
-- 192.168.1.0/25  | 255.255.255.128 | 0.0.0.127   | 192.168.1.127/25 | 192.168.1.1   | 192.168.1.126 | 128         | 126

network()set_masklen() - 网络地址操作

sql
-- 网络地址标准化和掩码调整
SELECT
    '192.168.1.5/24'::inet as "主机地址",
    network('192.168.1.5/24'::inet) as "网络地址",
    set_masklen('192.168.1.5/24'::inet, 16) as "调整掩码";

-- 对于CIDR类型
SELECT
    '192.168.1.0/24'::cidr as "原网络",
    set_masklen('192.168.1.0/24'::cidr, 16) as "扩大网络";

-- 结果:
-- 主机地址        | 网络地址        | 调整掩码
-- 192.168.1.5/24  | 192.168.1.0/24  | 192.168.1.5/16

-- 原网络          | 扩大网络
-- 192.168.1.0/24  | 192.168.0.0/16

inet_merge() - 网络合并

sql
-- 计算包含多个网络的最小网络(网络聚合)
WITH branch_networks AS (
    SELECT * FROM (VALUES
        ('分公司A', '192.168.1.0/24'::inet),
        ('分公司B', '192.168.2.0/24'::inet),
        ('分公司C', '192.168.4.0/24'::inet),
        ('分公司D', '192.168.5.0/24'::inet)
    ) AS t(branch, network)
)
SELECT
    string_agg(branch, ', ') as "分公司列表",
    inet_merge(
        '192.168.1.0/24'::inet,
        '192.168.5.0/24'::inet
    ) as "聚合网络";

-- 结果:
-- 分公司列表              | 聚合网络
-- 分公司A, 分公司B, 分公司C, 分公司D | 192.168.0.0/21

family()inet_same_family() - 地址族检测

sql
-- 混合IPv4/IPv6环境下的地址处理
CREATE TABLE network_devices (
    device_id SERIAL PRIMARY KEY,
    device_name VARCHAR(50),
    ipv4_address INET,
    ipv6_address INET
);

INSERT INTO network_devices VALUES
(1, '路由器A', '192.168.1.1/24', '2001:db8::1/64'),
(2, '交换机B', '192.168.1.2/24', '2001:db8::2/64'),
(3, '服务器C', '192.168.1.10/24', NULL);

SELECT
    device_name,
    ipv4_address,
    family(ipv4_address) as "IPv4族",
    ipv6_address,
    CASE
        WHEN ipv6_address IS NOT NULL THEN family(ipv6_address)
        ELSE NULL
    END as "IPv6族",
    CASE
        WHEN ipv6_address IS NOT NULL THEN
            inet_same_family(ipv4_address, ipv6_address)
        ELSE NULL
    END as "同族检测"
FROM network_devices;

-- 结果:
-- device_name | ipv4_address    | IPv4族 | ipv6_address     | IPv6族 | 同族检测
-- 路由器A      | 192.168.1.1/24  | 4      | 2001:db8::1/64   | 6      | f
-- 交换机B      | 192.168.1.2/24  | 4      | 2001:db8::2/64   | 6      | f
-- 服务器C      | 192.168.1.10/24 | 4      | NULL             | NULL   | NULL

MAC 地址函数

trunc() - MAC 地址截断

MAC 地址的前 3 字节(24 位)通常标识制造商(OUI - Organizationally Unique Identifier)。

sql
-- MAC地址制造商标识提取
CREATE TABLE network_cards (
    card_id SERIAL PRIMARY KEY,
    card_model VARCHAR(50),
    mac_address MACADDR
);

INSERT INTO network_cards VALUES
(1, 'Intel 网卡', '12:34:56:78:90:ab'),
(2, 'Broadcom 网卡', 'aa:bb:cc:dd:ee:ff'),
(3, 'Realtek 网卡', '00:11:22:33:44:55');

SELECT
    card_model,
    mac_address as "完整MAC",
    trunc(mac_address) as "制造商OUI"
FROM network_cards;

-- 结果:
-- card_model    | 完整MAC           | 制造商OUI
-- Intel 网卡    | 12:34:56:78:90:ab | 12:34:56:00:00:00
-- Broadcom 网卡 | aa:bb:cc:dd:ee:ff | aa:bb:cc:00:00:00
-- Realtek 网卡  | 00:11:22:33:44:55 | 00:11:22:00:00:00

macaddr8_set7bit() - EUI-64 转换

在 IPv6 环境中,经常需要将 MAC 地址转换为 EUI-64 格式以生成链路本地地址。

sql
-- IPv6 链路本地地址生成示例
WITH mac_addresses AS (
    SELECT '00:1a:2b:3c:4d:5e'::macaddr8 as original_mac
)
SELECT
    original_mac as "原始MAC",
    macaddr8_set7bit(original_mac) as "EUI-64格式",
    -- 生成IPv6链路本地地址前缀(实际应用中需要额外处理)
    'fe80::' ||
    replace(
        replace(macaddr8_set7bit(original_mac)::text, ':', ''),
        'fffe', ':fffe:'
    ) as "IPv6链路本地地址示例"
FROM mac_addresses;

-- 结果:
-- 原始MAC              | EUI-64格式                    | IPv6链路本地地址示例
-- 00:1a:2b:3c:4d:5e    | 02:1a:2b:ff:fe:3c:4d:5e      | fe80::021a:2b:fffe:3c4d5e

综合应用案例

案例 1:网络资产管理系统

sql
-- 创建完整的网络资产管理表结构
CREATE TABLE network_segments (
    segment_id SERIAL PRIMARY KEY,
    segment_name VARCHAR(100),
    network_range CIDR,
    vlan_id INTEGER,
    description TEXT
);

CREATE TABLE network_assets (
    asset_id SERIAL PRIMARY KEY,
    asset_name VARCHAR(100),
    asset_type VARCHAR(50),
    ip_address INET,
    mac_address MACADDR,
    segment_id INTEGER REFERENCES network_segments(segment_id)
);

-- 插入示例数据
INSERT INTO network_segments VALUES
(1, '管理网段', '192.168.1.0/24', 10, '网络设备管理'),
(2, '办公网段', '192.168.10.0/24', 20, '员工办公设备'),
(3, '服务器网段', '192.168.100.0/24', 30, '服务器和应用');

INSERT INTO network_assets VALUES
(1, '核心交换机', '交换机', '192.168.1.1/24', '00:11:22:33:44:55', 1),
(2, 'Web服务器', '服务器', '192.168.100.10/24', 'aa:bb:cc:dd:ee:ff', 3),
(3, '员工PC-001', '工作站', '192.168.10.100/24', '12:34:56:78:90:ab', 2);

-- 网络资产分析查询
SELECT
    ns.segment_name,
    ns.network_range,
    na.asset_name,
    na.asset_type,
    host(na.ip_address) as "设备IP",
    na.mac_address,
    trunc(na.mac_address) as "制造商OUI",
    -- 检查IP是否在正确的网段
    na.ip_address << ns.network_range as "IP匹配网段",
    -- 计算该设备在网段中的位置
    na.ip_address - network(ns.network_range) as "网段内偏移"
FROM network_assets na
JOIN network_segments ns ON na.segment_id = ns.segment_id
ORDER BY ns.segment_id, na.asset_id;

案例 2:网络安全监控

sql
-- 创建网络访问日志表
CREATE TABLE access_logs (
    log_id SERIAL PRIMARY KEY,
    timestamp TIMESTAMP DEFAULT NOW(),
    source_ip INET,
    destination_ip INET,
    action VARCHAR(20),
    protocol VARCHAR(10)
);

-- 创建安全策略表
CREATE TABLE security_policies (
    policy_id SERIAL PRIMARY KEY,
    policy_name VARCHAR(100),
    source_network CIDR,
    destination_network CIDR,
    action VARCHAR(20) -- ALLOW, DENY, LOG
);

INSERT INTO security_policies VALUES
(1, '内网互访', '192.168.0.0/16', '192.168.0.0/16', 'ALLOW'),
(2, '拒绝外网访问内网', '0.0.0.0/0', '192.168.0.0/16', 'DENY'),
(3, '允许访问DMZ', '192.168.0.0/16', '10.0.1.0/24', 'ALLOW');

-- 模拟访问日志
INSERT INTO access_logs (source_ip, destination_ip, action, protocol) VALUES
('192.168.1.100', '192.168.1.200', 'ALLOW', 'TCP'),
('203.0.113.100', '192.168.1.50', 'DENY', 'TCP'),
('192.168.10.50', '10.0.1.10', 'ALLOW', 'HTTP');

-- 安全分析:检查违规访问
WITH policy_violations AS (
    SELECT
        al.*,
        sp.policy_name,
        sp.action as expected_action,
        -- 检查源IP是否匹配策略
        al.source_ip << sp.source_network as source_match,
        -- 检查目标IP是否匹配策略
        al.destination_ip << sp.destination_network as dest_match
    FROM access_logs al
    CROSS JOIN security_policies sp
    WHERE al.source_ip << sp.source_network
    AND al.destination_ip << sp.destination_network
)
SELECT
    timestamp,
    host(source_ip) as "源IP",
    host(destination_ip) as "目标IP",
    policy_name as "匹配策略",
    expected_action as "期望动作",
    action as "实际动作",
    CASE
        WHEN expected_action != action THEN '违规'
        ELSE '正常'
    END as "状态"
FROM policy_violations
ORDER BY timestamp DESC;

性能优化建议

WARNING

性能注意事项

  1. 索引使用:在 INET/CIDR 列上创建 GiST 索引以优化包含性查询
  2. 数据类型选择:根据具体需求选择 INET 或 CIDR
  3. 查询优化:避免在大表上进行全表扫描的网络运算
sql
-- 创建网络地址索引
CREATE INDEX idx_network_assets_ip ON network_assets USING GIST (ip_address inet_ops);
CREATE INDEX idx_network_segments_range ON network_segments USING GIST (network_range inet_ops);

-- 优化的查询示例
EXPLAIN (ANALYZE, BUFFERS)
SELECT asset_name
FROM network_assets
WHERE ip_address << '192.168.1.0/24'::cidr;

总结

PostgreSQL 的网络地址函数和操作符为网络应用开发提供了强大的工具集。通过合理使用这些功能,可以:

  • 简化网络逻辑:直接在数据库层面处理复杂的网络计算
  • 提高性能:利用专门的索引和优化算法
  • 增强安全性:实现精确的网络访问控制
  • 便于维护:将网络逻辑集中在数据库中,减少应用层复杂性

在实际项目中,建议结合具体业务需求,选择合适的数据类型和操作符,并注意性能优化和索引策略的应用。