Skip to content

项目上现在需要存储 IP 地址,数据库应该用什么类型来存储? ​

  • 标签:后端、场景题

🏛 业务架构师 ​

存储 IP 地址,我的标准答案是用无符号整型 INT UNSIGNED(4 字节)存 IPv4,配合 MySQL 的转换函数,而不是直觉上的字符串。

推荐做法

sql
ip_start INT UNSIGNED NOT NULL,
-- 写入
INSERT INTO t(ip) VALUES (INET_ATON('192.168.1.100'));
-- 读取
SELECT INET_NTOA(ip) FROM t;

为什么不用 VARCHAR(15)

  • 空间:15 字节 vs 4 字节,省 70%+,IP 字段通常出现在亿级日志表里,这是实打实的存储成本
  • 查询性能:字符串比较是逐字符的且受字典序影响('9.x' > '10.x' 的坑),整数比较走索引又快又正确
  • 范围查询:封禁网段(192.168.0.0/16)用整数区间 BETWEEN 一次索引命中,字符串要写前缀 LIKE,索引直接失效

IPv6 的处理:地址 128 位,INT 装不下,用 VARBINARY(16) + INET6_ATON/NTOA,MySQL 5.6+ 原生支持。新系统我会直接按 IPv6 方案建表——国内 IPv6 普及率已过半, retrospective 改造表结构的成本远高于建表时多留 12 字节。

业务场景提醒:IP 在业务里主要用于风控(异地登录识别、黑名单封禁)和审计溯源,两个场景都依赖高频等值/区间查询,整数化是查询成本和风控响应速度的基础。另外要提醒团队:IP 存储属于个人信息,合规上要有脱敏策略和留存期限设计,技术选型时一并落到方案里。

🏗 系统架构师 ​

我会从存储效率、索引效率、IPv6 兼容三个维度选型,先给结论再讲依据。

方案对比

  • VARCHAR(15):可读性最好,但最多 15 字节(IPv4),索引空间大、按 IP 范围查询(BETWEEN '10.0.0.1' AND '10.0.0.255')语义不可靠——字符串比较和数值比较在进位处不一致,这是隐蔽的正确性坑。
  • INT UNSIGNED(4 字节):IPv4 的天然表示,用 INET_ATON() / INET_NTOA() 转换,等值和范围查询都走数值语义,索引最小最快。纯 IPv4 场景我的首选。
  • VARBINARY(16) / BINARY(16):INET6_ATON() 直接支持 IPv6(16 字节),MySQL 5.6+ 推荐,是新系统的默认选择——2026 年 IPv6 流量占比过半,新表设计必须向后兼容。
sql
CREATE TABLE t (
  ip VARBINARY(16) NOT NULL,
  KEY idx_ip (ip)
);
INSERT INTO t VALUES (INET6_ATON('2001:db8::1'));
SELECT INET6_NTOA(ip) FROM t
 WHERE ip BETWEEN INET6_ATON('10.0.0.0') AND INET6_ATON('10.0.0.255');

量级视角 10 亿级访问日志表,VARCHAR(15) 平均 12 字节 + 变长开销 vs INT 4 字节,索引和缓冲池占用差 3 倍,直接决定内存里能缓存多少热数据。转换函数的计算开销是纳秒级,可以忽略。

架构延伸:查 IP 归属地这类需求不落在 MySQL,而是加载 IP 段库(离线导入的有序区间表或内存二分),DB 只做精确点查;海量 IP 分析走 ClickHouse 的 IPv4/IPv6 原生类型,那又是另一套选型。

💻 全栈开发专家 ​

我统一用 INT UNSIGNED 存 IPv4(4 字节),配合 MySQL 内置函数转换,IPv6 用 VARBINARY(16)。

方案

sql
CREATE TABLE t_ip (
    ip INT UNSIGNED NOT NULL,
    ...
) ;

-- 写入
INSERT INTO t_ip(ip) VALUES (INET_ATON('192.168.1.100'));
-- 读取
SELECT INET_NTOA(ip) FROM t_ip;
-- 范围查询(网段)依然可走索引
SELECT * FROM t_ip WHERE ip BETWEEN INET_ATON('192.168.0.0') AND INET_ATON('192.168.0.255');

若要同时兼容 IPv4/IPv6,直接 VARBINARY(16) + INET6_ATON()/INET6_NTOA()(MySQL 8 起原生支持)。

为什么不用 VARCHAR(15)

  • 15 字节 vs 4 字节,索引体积差近 4 倍。
  • 字符串比较是逐字节的,'9.x' > '10.x' 这种字典序和数值序不一致,范围统计会出错,还依赖格式统一(多写少个零就完蛋)。
  • INT UNSIGNED 可比较、可聚合(转网段、算归属地二分查找都方便)。

踩过的坑

  • Java 侧 INET_ATON 结果要用 long 接(无符号 int 超出 int 正数范围会被标成负数),MyBatis 映射成 Long 再转。
  • 归属地查询量大时,我会把 IP 段库预载进内存或 Redis 二分查找,不实时查库。