GROUP BY 不能直接按网段归并IP,必须用 INET_ATON() 转整数后位运算提取前缀;字符串截断(如 SUBSTRING_INDEX)因字典序和掩码语义缺失而不可靠;生产环境应预存网段字段并建索引。

GROUP BY 本身不能直接按网段归并 IP,必须先将 IP 转为整数、再用位运算提取前缀,否则分组结果不可靠。
为什么不能直接 GROUP BY SUBSTRING_INDEX(ip, '.', 3)
字符串截断看似快,但会出错:'10.1.1.1' 和 '10.1.1.128' 在 /24 网段下属于同一子网,但 SUBSTRING_INDEX(ip, '.', 3) 对两者分别返回 '10.1.1' 和 '10.1.1' —— 这个例子看似碰巧对了,可一旦遇到 '192.168.10.5' 和 '192.168.2.5',字典序上 '192.168.10' > '192.168.2',却完全不是同一网段。更严重的是,它无法表达掩码语义(比如 /22 或 /26),也不兼容 CIDR 表示法。
INET_ATON() + 位运算才是可靠前缀提取方式
MySQL 中标准做法是:INET_ATON(ip) & 0xFFFFFF00 提取 /24 网段前缀(即保留高 24 位,后 8 位清零)。关键点:
- 掩码必须写成十六进制(如
0xFFFFFF00),写成255.255.255.0会报错或被当十进制解析 - 结果仍是整数,聚合后建议用
INET_NTOA()转回可读格式:例如SELECT INET_NTOA(INET_ATON(ip) & 0xFFFFFF00) AS network, COUNT(*) FROM log GROUP BY INET_ATON(ip) & 0xFFFFFF00 - 非法 IP(如
'256.1.1.1')会让INET_ATON()返回NULL,务必加清洗:用WHERE ip REGEXP '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'过滤
千万级表慎用实时计算,预存字段才能走索引
GROUP BY INET_ATON(ip) & 0xFFFF0000 这类写法会让 MySQL 放弃使用 ip 字段上的索引,全表扫描不可避免。真实生产环境要高频查网段,必须冗余字段:
- 写入时就计算好:
network_24 TINYINT UNSIGNED或直接存VARCHAR(15)格式网段(如'192.168.1.0') - 在该字段上建索引:
CREATE INDEX idx_network_24 ON log_table(network_24) - 临时分析可用 CTE 预计算,避免嵌套过深:
WITH ip_net AS (SELECT ip, INET_NTOA(INET_ATON(ip) & 0xFFFFFF00) AS net24 FROM log_table WHERE ... ) SELECT net24, COUNT(*) FROM ip_net GROUP BY net24
跨数据库兼容写法:手算 IP 整数不依赖 INET_ATON()
PostgreSQL、SQL Server、Oracle 均无 INET_ATON(),但都能用字符串拆解手算:
- MySQL 示例:
(SUBSTRING_INDEX(ip, '.', 1) * 16777216 + SUBSTRING_INDEX(SUBSTRING_INDEX(ip, '.', 2), '.', -1) * 65536 + SUBSTRING_INDEX(SUBSTRING_INDEX(ip, '.', 3), '.', -1) * 256 + SUBSTRING_INDEX(ip, '.', -1)) - PostgreSQL 替换为:
(SPLIT_PART(ip, '.', 1)::int * 16777216 + SPLIT_PART(ip, '.', 2)::int * 65536 + SPLIT_PART(ip, '.', 3)::int * 256 + SPLIT_PART(ip, '.', 4)::int) - 结果务必转为
UNSIGNED INT或BIGINT,否则负数溢出(如 MySQL 的INT有符号)
真正难的不是写出那条 GROUP BY,而是决定“这个网段粒度是否固定”——/24 就硬编码 0xFFFFFF00,别试图抽象成参数化 mask;以及是否接受写入时多一次计算来换取查询性能。这两点没想清楚,后面所有优化都是白忙。

















