关键在于将IP归入所属网段:MySQL用INET_ATON(ip) & INET_ATON(mask)得网段起始地址,PostgreSQL用network(ip::inet);需区分IPv4/IPv6、预处理脏数据,并建议新增计算列建索引以提升性能。

用 CIDR 掩码截取 IP 网段做分组
直接对原始 ip 字段 GROUP BY 得到的是单个 IP 的频次,要统计“IP 段”(比如 192.168.1.0/24),关键在于把每个 IP 归入其所属网段——本质是按掩码做位运算截断。MySQL 和 PostgreSQL 都支持整型转换和位运算,但写法不同:
- MySQL:先用
INET_ATON(ip)转为整数,再与掩码做&,例如INET_ATON('192.168.1.100') & INET_ATON('255.255.255.0')→ 得到网段起始地址3232235776,再用INET_NTOA()转回点分十进制 - PostgreSQL:需配合
inet类型和network()函数,如network(ip::inet)可直接提取 CIDR 网段(前提是ip是inet类型或能安全 cast) - 注意:IPv6 不适用
INET_ATON,MySQL 8.0+ 才支持INET6_ATON,且位运算更复杂,建议先过滤或单独处理
WHERE 条件里别漏掉 IPv4/IPv6 混合判断
如果日志表里同时存了 IPv4 和 IPv6(比如 Nginx 的 $remote_addr),直接对 ip 字段应用 INET_ATON 会把所有 IPv6 返回 NULL,导致这些记录在 GROUP BY 中被丢弃——不是数据没了,是计算时被静默过滤了。
- 先确认字段类型:
SELECT DISTINCT LENGTH(ip), ip FROM log_table LIMIT 5,IPv4 长度通常为 7–15,IPv6 一般 ≥ 39 - 稳妥做法是加类型判断:
WHERE ip NOT LIKE '%:%'(简单排除 IPv6),或用IS_IPV4(ip)(MySQL 8.0.22+) - 若必须包含 IPv6,改用
INSTR(ip, ':') = 0分流,再对 IPv4 部分走INET_ATON,IPv6 部分用正则截前缀(如REGEXP_SUBSTR(ip, '^[^:]+(:[^:]+){0,3}') || '::/64'),但性能差,慎用
聚合时用 SUBSTRING_INDEX 或正则快速切前缀(适合简单掩码)
如果只要统计「C 类网段」(即前 3 段,如 192.168.1.x → 192.168.1.0/24),且确定全是 IPv4、格式规范(无空格、无代理头),可用字符串函数绕过位运算,更轻量:
- MySQL:
SUBSTRING_INDEX(ip, '.', 3)直接取前三段,再拼'.0'作为网段标识,例如CONCAT(SUBSTRING_INDEX(ip, '.', 3), '.0') - PostgreSQL:
SPLIT_PART(ip, '.', 1) || '.' || SPLIT_PART(ip, '.', 2) || '.' || SPLIT_PART(ip, '.', 3) || '.0' - 风险点:遇到
10.0.0.1和100.0.0.1这类,SUBSTRING_INDEX结果分别是'10.0.0'和'100.0.0',没问题;但若日志含'192.168.1.1:32123'(带端口),得先用REPLACE或正则清理,否则切出来是'192.168.1:32123'
索引和性能:别指望对函数结果走索引
无论用 INET_ATON(ip) & mask 还是 SUBSTRING_INDEX(ip, '.', 3),只要 GROUP BY 里出现函数表达式,MySQL 就无法使用 ip 字段的普通 B-Tree 索引——全表扫描不可避免。
- 高频查询场景下,建议新增一列
ip_network VARCHAR(15)(IPv4)或ip_network_cidr CIDR(PostgreSQL),写入时就计算好并存入,对该列建索引 - 或者用生成列(MySQL 5.7+ / PG 12+):
ALTER TABLE log_table ADD COLUMN ip_c_class VARCHAR(15) STORED AS (CONCAT(SUBSTRING_INDEX(ip, '.', 3), '.0')),再对ip_c_class建索引 - 临时查一次?没问题;每天跑定时统计?务必预计算,不然查 1000 万行日志可能卡住连接

















