面试官:「在 MySQL 中存储 IP 地址用什么数据类型?」
你:「用 VARCHAR(15) 存字符串吧,直观...」
面试官:「那查询 IP 段 192.168.1.0 - 192.168.1.255 的访问记录呢?」
你:「用 LIKE '192.168.1.%' ...」
面试官:「数据量有几千万条呢?」
你:「......」
如果你遇到过这种尴尬场面,那这篇文章一定要收藏起来!
目录
- 一、先说结论
- 二、三种存储方案对比
- 三、原理层面:为什么整数存储更高效?
- [四、MySQL 内置转换函数](#四、MySQL 内置转换函数 "#%E5%9B%9Bmysql-%E5%86%85%E7%BD%AE%E8%BD%AC%E6%8D%A2%E5%87%BD%E6%95%B0")
- 五、存储空间与查询性能对比
- [六、IPv4 vs IPv6 统一存储方案](#六、IPv4 vs IPv6 统一存储方案 "#%E5%85%ADipv4-vs-ipv6-%E7%BB%9F%E4%B8%80%E5%AD%98%E5%82%A8%E6%96%B9%E6%A1%88")
- 七、实际应用场景选择
- 八、代码实战示例
- 九、常见误区避坑
- 十、面试回答模板
一、先说结论
MySQL 存储 IP 地址有三种主流方案,各有优缺点:
| 存储方案 | 数据类型 | 示例值 | 存储空间 |
|---|---|---|---|
| 字符串 | VARCHAR(15) | '192.168.1.1' | 11-15 字节 |
| 整数 | INT UNSIGNED | 3232235777 | 4 字节 |
| 二进制 | VARBINARY(16) | 0xC0A80101 | 4-16 字节 |
推荐方案:
- IPv4 :使用
INT UNSIGNED+INET_ATON/INET_NTOA转换 - IPv6 :使用
VARBINARY(16)+INET6_ATON/INET6_NTOA转换
二、三种存储方案对比
方案一:VARCHAR(15) 存储字符串
优点:
✅ 直观、可读性好、人类友好
✅ 存储格式即为显示格式,无需转换
✅ 便于人工查看和调试
缺点:
❌ 存储空间大(11-15 字节)
❌ 比较运算慢(字符串逐字节比较)
❌ 无法做数值范围查询
❌ 无法利用索引高效定位
方案二:INT UNSIGNED 存储整数
优点:
✅ 存储空间小(仅 4 字节)
✅ 比较运算快(单次 CPU 指令)
✅ 可做范围查询和排序
✅ 可利用索引高效定位
✅ MySQL 提供转换函数
缺点:
❌ 不可读,需要转换函数
❌ 只能存储 IPv4
方案三:VARBINARY(16) 存储二进制
优点:
✅ 支持 IPv4 和 IPv6 统一存储
✅ 存储空间适中(4-16 字节)
✅ 可利用索引高效查询
✅ 安全性相对较好
缺点:
❌ 不可读,需要转换函数
❌ 需要使用专用转换函数
三、原理层面:为什么整数存储更高效?
3.1 字符串比较 vs 数值比较
flowchart LR
subgraph 字符串比较
S1["'192.168.1.1'"] --> S2["逐字节比较ASCII码"]
S2 --> S3["需要比较11次"]
S3 --> S4["最后一位才区分"]
end
subgraph 数值比较
N1["3232235777"] --> N2["单次CPU指令"]
N2 --> N3["直接判断大小"]
N3 --> N4["可利用索引"]
end
字符串比较 "192.168.1.1" vs "192.168.1.2":
- 逐字节比较 ASCII 码
- 需要比较 11 次才能得出结果
- "1.1" 和 "1.2" 的最后一位才区分
数值比较 3232235777 vs 3232235778:
- 单次 CPU 指令完成
- 直接判断大小关系
- 可利用索引高效定位
3.2 INET_ATON 转换原理
ini
IP: 192.168.1.1
计算过程:
192 * 256³ + 168 * 256² + 1 * 256¹ + 1 * 256⁰
= 3221225472 + 1107296256 + 256 + 1
= 3232235777
二进制表示:
11000000 10101000 00000001 00000001
十六进制:0xC0A80101
四、MySQL 内置转换函数
4.1 IPv4 转换函数
sql
-- 字符串 → 整数
INET_ATON('192.168.1.1')
-- 结果:3232235777
-- 整数 → 字符串
INET_NTOA(3232235777)
-- 结果:'192.168.1.1'
4.2 IPv6 转换函数
sql
-- 字符串 → 二进制
INET6_ATON('2001:0db8:85a3::8a2e:0370:7334')
-- 返回 VARBINARY 类型
-- 二进制 → 字符串
INET6_NTOA(binary_col)
-- 返回可读的 IPv6 地址
4.3 IP 验证函数
sql
-- 判断是否为有效 IPv4
IS_IPV4('192.168.1.1')
-- 返回 1(真)或 0(假)
-- 判断是否为有效 IPv6
IS_IPV6('::1')
-- 返回 1 或 0
五、存储空间与查询性能对比
| 指标 | VARCHAR(15) | INT UNSIGNED | VARBINARY(16) |
|---|---|---|---|
| IPv4 存储空间 | 15 字节 | 4 字节 | 4 字节 |
| IPv6 存储空间 | 45 字节 | 不支持 | 16 字节 |
| 比较速度 | 最慢 | 最快 | 快 |
| 索引效率 | 低 | 高 | 高 |
| 可读性 | 高 | 低 | 低 |
| 范围查询 | 需 LIKE | 支持 BETWEEN | 支持 |
性能差距有多大?
假设有 1000 万条数据:
- VARCHAR 查询可能需要 数秒
- INT 查询通常只需 几十毫秒
六、IPv4 vs IPv6 统一存储方案
如果你的系统需要同时支持 IPv4 和 IPv6,推荐使用统一的 VARBINARY(16) 方案:
sql
-- 统一使用 VARBINARY(16) 存储
CREATE TABLE access_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
ip_address VARBINARY(16) NOT NULL,
access_time DATETIME,
url VARCHAR(255),
INDEX idx_ip (ip_address)
);
-- 插入 IPv4
INSERT INTO access_log (ip_address, access_time, url)
VALUES (INET6_ATON('192.168.1.1'), NOW(), '/index');
-- 插入 IPv6
INSERT INTO access_log (ip_address, access_time, url)
VALUES (INET6_ATON('::1'), NOW(), '/index');
-- 查询时转换显示
SELECT
id,
INET6_NTOA(ip_address) AS ip_address,
access_time,
url
FROM access_log;
-- IP 段查询
SELECT * FROM access_log
WHERE ip_address = INET6_ATON('192.168.1.1');
七、实际应用场景选择
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 用户 IP 记录 | INT UNSIGNED | 量大、查询频繁、需范围统计 |
| 日志分析 | VARCHAR(45) | 快速入库、后期离线分析 |
| 白名单配置 | INT UNSIGNED | 固定配置、查询为主 |
| 混合 IPv4/IPv6 | VARBINARY(16) | 统一格式、兼容未来 |
八、代码实战示例
8.1 创建表结构
sql
-- 创建用户访问记录表
CREATE TABLE user_visits (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
ip_v4 INT UNSIGNED NOT NULL, -- IPv4 存储
ip_v6 VARBINARY(16), -- IPv6 存储
visit_time DATETIME DEFAULT CURRENT_TIMESTAMP,
-- 可对整数 IP 建索引
INDEX idx_ip (ip_v4),
INDEX idx_user_time (user_id, visit_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
8.2 插入数据
sql
-- 使用转换函数插入数据
INSERT INTO user_visits (user_id, ip_v4, ip_v6) VALUES
(1001, INET_ATON('192.168.1.100'), NULL),
(1002, INET_ATON('10.0.0.1'), NULL),
(1003, INET_ATON('172.16.0.1'), INET6_ATON('::1'));
8.3 查询数据
sql
-- 查询并转换为可读格式
SELECT
user_id,
INET_NTOA(ip_v4) AS ip_address, -- 整数转字符串
INET6_NTOA(ip_v6) AS ipv6_address,
visit_time
FROM user_visits
WHERE ip_v4 BETWEEN INET_ATON('192.168.0.0')
AND INET_ATON('192.168.255.255');
8.4 IP 段统计
sql
-- 统计 IP 段访问量(按 /24 网段分组)
SELECT
INET_NTOA(ip_v4 DIV 256 * 256) AS ip_segment,
COUNT(*) AS visit_count
FROM user_visits
GROUP BY ip_v4 DIV 256
ORDER BY visit_count DESC
LIMIT 10;
九、常见误区避坑
| 误区 | 正确理解 |
|---|---|
| ❌ 用 INT 存储 IP | ✅ 必须用 INT UNSIGNED,IPv4 最大值 3232235777 超出会溢出 |
| ❌ 查询时直接用字符串比较 | ✅ 必须用 INET_ATON 转换:WHERE ip = INET_ATON('192.168.1.1') |
| ❌ IPv4 和 IPv6 混用同一字段 | ✅ 分别使用 INT UNSIGNED 和 VARBINARY(16),或统一用 VARBINARY(16) |
| ❌ VARCHAR 更安全 | ✅ IP 不是敏感信息,VARBINARY 并不更安全,只是性能较差 |
| ❌ 字符串比较效率还行 | ✅ 大数据量下,字符串比较无法利用索引,性能差距巨大 |
十、面试回答模板
当面试官问「MySQL 存储 IP 用什么数据类型」时,可以这样回答:
面试回答示例:
MySQL 存储 IP 地址有三种方案:
- VARCHAR(15):直观可读,但存储空间大、查询慢
- INT UNSIGNED:存储空间小(4字节)、查询快、可建索引
- VARBINARY(16):支持 IPv4/IPv6 统一存储
我推荐使用 INT UNSIGNED + INET_ATON/INET_NTOA 存储 IPv4,原因有三点:
- 存储空间小,比 VARCHAR 节省约 70%
- 比较运算快,单次 CPU 指令完成
- 可利用索引高效查询,支持范围查询
IPv6 则使用 VARBINARY(16) + INET6_ATON/INET6_NTOA。
总结
IP 地址存储推荐方案:
scss
┌────────────────────────────────────────────────────────────┐
│ IP 存储方案选择 │
├────────────────────────────────────────────────────────────┤
│ │
│ IPv4 → INT UNSIGNED + INET_ATON/INET_NTOA │
│ ✅ 4字节 | ✅ 查询快 | ✅ 可建索引 │
│ │
│ IPv6 → VARBINARY(16) + INET6_ATON/INET6_NTOA │
│ ✅ 16字节 | ✅ 统一格式 │
│ │
│ 混合 → VARBINARY(16) + INET6_ATON/INET6_NTOA │
│ ✅ 兼容 IPv4/IPv6 │
│ │
└────────────────────────────────────────────────────────────┘