原创

为什么数据库更推荐将 IPv4 存为 32 位整数而不是字符串

先把结论说清楚

如果你的数据库没有更合适的原生网络地址类型,那么 IPv4 存成 32 位整数,通常比存成字符串更合理。原因并不神秘:IPv4 在协议层本来就是 32 位地址;而 MySQL 也明确提供了 INET_ATON() / INET_NTOA() 这种“字符串地址 ↔ 数值地址”的转换函数,并建议把 INET_ATON() 的结果存入 INT UNSIGNED。(RFC Editor)

不过这句话还有一个更准确的版本:如果数据库提供原生网络地址类型,优先用原生类型;如果没有,IPv4 再考虑用 32 位整数。 PostgreSQL 官方文档就直接建议使用 inet / cidr 这类网络地址类型,而不是纯文本类型,因为它们自带输入校验以及专门的运算符和函数。(PostgreSQL)

为什么字符串看起来直观,却不是好存储格式

把 IPv4 写成 '192.168.10.23' 这种字符串,最大的优点是“人眼友好”。但数据库关心的不只是展示,还关心比较、排序、索引和范围扫描。

字符串有三个常见问题。

第一,排序语义容易错。字符串排序是按字符序比较,不是按地址数值比较。也就是说,'192.168.2.100''192.168.2.9' 的前后关系,在字符串世界和在 IP 数值世界里并不一致。这意味着你把 IP 当文本存,很多“看起来合理”的 ORDER BY 结果其实不符合网络地址的语义。这个结论是从 IPv4 的 32 位数值本质直接推出来的。(RFC Editor)

第二,范围查询不自然。IP 段过滤本质上是数值区间问题,例如查 192.168.10.0/24,你真正想表达的是一个连续地址区间。整数列可以天然做 BETWEEN 范围扫描;字符串列虽然也能硬写条件,但表达别扭,而且常常把逻辑从“地址区间”扭曲成“文本模式匹配”。这同样来自 IPv4 的 32 位地址本质,以及 MySQL 提供数值转换函数这一事实。(RFC Editor)

第三,索引和比较成本通常更高。这是一个工程上的推论:固定 4 字节的整数键,通常比变长文本键更紧凑,比较也更简单;而字符串列的存储还会受到字符集和长度前缀影响。MySQL 官方文档也说明了字符串类型是“长度前缀 + 数据”的可变长度存储,而 INT 的存储是固定 4 字节。(MySQL开发者专区)

32 位整数为什么更贴近 IPv4 的真实形态

RFC 791 里写得很清楚:Internet address 是固定长度的四个八位组,也就是 32 位。换句话说,点分十进制只是人类可读表示,真正的 IPv4 地址本体就是一个 32 位值。(RFC Editor)

MySQL 的 INET_ATON() 也是沿着这个思路工作的:它把点分十进制 IPv4 字符串转成一个网络字节序的大端整数;INET_NTOA() 则把整数再转回人类可读的点分十进制字符串。MySQL 文档还特别提醒:存 INET_ATON() 的结果时,应使用 INT UNSIGNED,因为有一半以上的 IPv4 地址范围会超出有符号 INT 的正数上限。INT UNSIGNED 的最大值是 4294967295,正好覆盖 32 位无符号地址空间。(MySQL开发者专区)

这件事很关键。很多文章只说“存成整数”,但没说清楚应该是 INT UNSIGNED。如果你用了有符号 INT,首字节大于 127 的地址就可能存不对,MySQL 官方文档已经明确点名这个坑。(MySQL开发者专区)

一个典型反例:把 IP 存成字符串

下面先给一个“能用但不推荐”的表结构:

CREATE TABLE access_log_ip_string (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    user_id BIGINT UNSIGNED NOT NULL,
    ip_text VARCHAR(15) NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_ip_text (ip_text),
    KEY idx_user_created (user_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这个设计的问题不是“不能存”,而是 数据库无法把这个列当成地址来理解。它只知道这里是一段文本。

比如下面这个排序:

SELECT ip_text
FROM access_log_ip_string
ORDER BY ip_text
LIMIT 10;

它得到的是文本序顺序,不是 IP 数值顺序。你想查一个网段时,也很容易写出不自然甚至错误的条件。

更合理的 MySQL 方案:存成 INT UNSIGNED

如果你的目标仅仅是存 IPv4,那么一个更实用的表结构是这样:

CREATE TABLE access_log_ipv4_int (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    user_id BIGINT UNSIGNED NOT NULL,
    ip_num INT UNSIGNED NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_ip_num (ip_num),
    KEY idx_user_created (user_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

写入时,不直接塞字符串,而是让数据库把点分十进制转换成整数:

INSERT INTO access_log_ipv4_int (user_id, ip_num)
VALUES
    (1001, INET_ATON('192.168.10.23')),
    (1002, INET_ATON('10.0.5.9')),
    (1003, INET_ATON('172.16.3.44'));

读取时,再把整数转回字符串展示:

SELECT
    id,
    user_id,
    INET_NTOA(ip_num) AS ip_text,
    created_at
FROM access_log_ipv4_int
ORDER BY id DESC;

这个模型的好处是,存储层按数值保存,展示层按字符串输出。数据库内部和业务界面各自用最适合自己的表示法。MySQL 官方文档正是围绕这套转换函数设计了 INET_ATON()INET_NTOA()。(MySQL开发者专区)

为什么整数方案在查询上更顺手

1. 排序结果符合地址语义

SELECT INET_NTOA(ip_num) AS ip_text
FROM access_log_ipv4_int
ORDER BY ip_num;

这里的排序结果就是按 IPv4 的真实数值排序,而不是按字符字典序排序。

2. 范围查询非常自然

例如查 192.168.10.0/24 这个网段中的地址:

SELECT
    id,
    user_id,
    INET_NTOA(ip_num) AS ip_text
FROM access_log_ipv4_int
WHERE ip_num BETWEEN INET_ATON('192.168.10.0') AND INET_ATON('192.168.10.255')
ORDER BY ip_num;

这类查询本质是数值区间过滤,用整数表达几乎是“直译”。如果你把 IP 存成字符串,往往会忍不住写成 LIKE '192.168.10.%'。这种写法看似省事,但它描述的是文本前缀,不是网络地址区间;一旦需求变成更复杂的 CIDR、排序或上下界计算,字符串方案就会迅速变脆。这个判断是基于 IPv4 的 32 位数值本质,以及 MySQL 提供数值转换函数做出的工程结论。(RFC Editor)

3. 索引键通常更小、更稳定

INT 在 MySQL 中是固定 4 字节;字符串列则是“长度前缀 + 数据”,并且实际字节数会受字符集影响。对于大量日志、风控、审计、访问记录这类表,索引键越紧凑,通常越有利于页密度、缓存命中和扫描效率。这里不需要神化“整数一定更快”,但在单纯的 IPv4 主机地址存储上,整数列确实更贴近数据库想要处理的对象。(MySQL开发者专区)

字符串方案最容易踩的几个坑

下面这张表能更快看出两种方案的差异:

维度 字符串存储 32 位整数存储
语义 人类可读 机器可计算
排序 文本字典序 地址数值序
范围查询 往往写成文本匹配 直接做数值区间
索引键 变长文本 固定 4 字节
输入校验 容易混入非标准写法 可在转换阶段校验
展示成本 需要 INET_NTOA()

最值得强调的是输入校验。MySQL 文档提到,INET_ATON() 对某些短格式地址(比如 '127.1')的处理并不稳定,因此不要依赖这种短格式。更稳妥的做法是:应用层先做严格 IPv4 校验,只接受标准点分十进制四段写法,再调用 INET_ATON() 入库。(MySQL开发者专区)

更准确的行业经验:PostgreSQL 里别纠结整数,直接用 inet

到这里你可能会问:既然字符串不好,整数不错,那是不是所有数据库都该统一存整数?

不是。

在 PostgreSQL 里,更好的答案通常不是“字符串 vs 整数”,而是 直接用 inet。PostgreSQL 官方文档明确说,网络地址更适合用 inet / cidr 这类原生类型,而不是纯文本,因为原生类型自带输入错误检查和专门的操作符、函数。文档还给出了存储大小:inet 对 IPv4/IPv6 分别是 7 或 19 字节。(PostgreSQL)

也就是说,“IPv4 存 32 位整数”主要是 MySQL 这类没有同等级原生网络类型时的务实方案。如果你在 PostgreSQL 里还坚持自己造一个 INT 列,反而是在放弃数据库已经给你的语义能力。

IPv6 来了以后,32 位整数方案为什么不能硬撑

这一点必须单独说,因为很多旧系统会在这里犯路径依赖。

IPv4 是 32 位,所以 INT UNSIGNED 正好能装下;但 IPv6 是 128 位。MySQL 文档也说明了这一点:INET6_ATON() 返回的是二进制表示,IPv6 用 VARBINARY(16),IPv4 用 VARBINARY(4)。这不是语法细节,而是在提醒你:IPv6 根本不是 32 位整数能承载的对象。(MySQL开发者专区)

如果你的系统未来要支持双栈,常见做法反而是直接统一成二进制列,例如:

CREATE TABLE access_log_ip_binary (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    user_id BIGINT UNSIGNED NOT NULL,
    ip_bin VARBINARY(16) NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_ip_bin (ip_bin),
    KEY idx_user_created (user_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

写入和读取可以这样做:

INSERT INTO access_log_ip_binary (user_id, ip_bin)
VALUES
    (1001, INET6_ATON('192.168.10.23')),
    (1002, INET6_ATON('2001:db8::1'));

SELECT
    id,
    user_id,
    INET6_NTOA(ip_bin) AS ip_text,
    created_at
FROM access_log_ip_binary;

所以,32 位整数不是“IP 地址的终极存法”,而是“IPv4-only 场景下的高性价比存法”。一旦需求越过 IPv4 边界,就该升级模型,而不是继续在 INT UNSIGNED 上打补丁。(MySQL开发者专区)

什么时候应该用整数,什么时候不该

适合用 32 位整数存 IPv4 的场景:

  • 系统只处理 IPv4。
  • 数据量大,IP 常用于过滤、排序、范围查询或索引。
  • 所用数据库没有比整数更合适的原生网络类型。
  • 业务展示层可以接受“入库转整数、出库转字符串”的模式。

不适合继续坚持 32 位整数的场景:

  • 已经要支持 IPv6。
  • 需要保存网段语义、掩码语义,而不只是单个主机地址。
  • 数据库本身已经提供 inet / cidr 这类类型。
  • 你希望把校验、规范化和网络运算尽量交给数据库原生能力。

总结

“为什么数据库推荐把 IPv4 存成 32 位整数而不是字符串”,本质上不是因为“字符串不好看”,而是因为 IPv4 天生就是 32 位值。把它存成整数,更接近协议本体,也更利于排序、范围查询、索引和计算。RFC 791 定义了 IPv4 的 32 位本质,而 MySQL 的 INET_ATON() / INET_NTOA() 进一步把这种建模方式制度化了。(RFC Editor)

但这条经验不能机械套用。更准确的实践应该是:

  • MySQL / 无原生网络类型场景:IPv4 优先考虑 INT UNSIGNED。(MySQL开发者专区)
  • PostgreSQL:优先考虑 inet / cidr,不要退化成纯文本。(PostgreSQL)
  • 涉及 IPv6:不要再用 32 位整数,改用原生网络类型或 VARBINARY(16) 一类二进制方案。(MySQL开发者专区)
正文到此结束
评论插件初始化中...
Loading...