数据类型
问题
数据库中有哪些常用的数据类型?怎么选择合适的类型?
答案
数据类型定义了表中每一列能存储什么样的数据。选择合适的数据类型对于存储效率、查询性能和数据完整性都至关重要。
以下以 MySQL 为主介绍常用数据类型,PostgreSQL 也会补充说明差异。
数值类型
整数类型
| 类型 | 存储空间 | 范围(有符号) | 范围(无符号 UNSIGNED) | 使用场景 |
|---|---|---|---|---|
TINYINT | 1 字节 | -128 ~ 127 | 0 ~ 255 | 布尔值、状态码 |
SMALLINT | 2 字节 | -32768 ~ 32767 | 0 ~ 65535 | 年龄、小范围计数 |
MEDIUMINT | 3 字节 | -8388608 ~ 8388607 | 0 ~ 16777215 | 中等范围 |
INT | 4 字节 | ≈ ±21 亿 | 0 ~ ≈42 亿 | 最常用,主键、计数 |
BIGINT | 8 字节 | ≈ ±922 京 | 0 ~ ≈1844 京 | 雪花 ID、大型系统主键 |
- 主键:单机用
INT,高并发/分布式用BIGINT - 布尔值:MySQL 没有真正的布尔类型,
TINYINT(1)就是BOOLEAN - 不确定就用
INT,因为空间差异很小,过小的类型将来可能需要迁移
浮点数与精确数值
| 类型 | 存储 | 精度 | 使用场景 |
|---|---|---|---|
FLOAT | 4 字节 | 约 7 位有效数字 | 科学计算 |
DOUBLE | 8 字节 | 约 15 位有效数字 | 科学计算 |
DECIMAL(M,D) | 变长 | 精确计算 | 金额、价格 |
-- FLOAT 有精度问题(0.1 + 0.2 ≠ 0.3)
SELECT CAST(0.1 + 0.2 AS FLOAT); -- 可能是 0.30000001
-- DECIMAL 精确计算,金额必须用 DECIMAL
CREATE TABLE orders (
id INT PRIMARY KEY,
amount DECIMAL(10, 2) -- 最多 10 位数字,其中 2 位小数
-- 范围:-99999999.99 ~ 99999999.99
);
浮点数有精度损失,0.1 + 0.2 可能不等于 0.3。金额类字段必须使用 DECIMAL 类型。
字符串类型
| 类型 | 最大长度 | 存储方式 | 使用场景 |
|---|---|---|---|
CHAR(n) | 255 字节 | 固定长度 | MD5 哈希、国家代码、性别 |
VARCHAR(n) | 65535 字节 | 可变长度 | 最常用,姓名、邮箱、地址 |
TEXT | 65535 字节 | 变长大文本 | 文章正文、评论 |
MEDIUMTEXT | 16 MB | 变长大文本 | 长文章 |
LONGTEXT | 4 GB | 变长大文本 | 超大文本 |
ENUM | 1~2 字节 | 枚举值 | 状态、性别等有限选项 |
CREATE TABLE articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL, -- 标题,可变长
slug CHAR(32), -- URL slug,固定长度
content TEXT, -- 正文,大文本
status ENUM('draft', 'published', 'archived') DEFAULT 'draft'
);
VARCHAR(n) 中的 n 表示最大字符数(不是字节数)。UTF-8 下一个中文字符占 3 字节。建议:
- 不要随意设置
VARCHAR(255),根据实际需要设置 - 但也不要太小,后续扩展不方便
日期和时间类型
| 类型 | 格式 | 范围 | 存储 | 使用场景 |
|---|---|---|---|---|
DATE | YYYY-MM-DD | 1000 ~ 9999 年 | 3 字节 | 生日、日期 |
TIME | HH:MM:SS | -838:59:59 ~ 838:59:59 | 3 字节 | 时间段 |
DATETIME | YYYY-MM-DD HH:MM:SS | 1000 ~ 9999 年 | 8 字节 | 创建时间、更新时间 |
TIMESTAMP | YYYY-MM-DD HH:MM:SS | 1970 ~ 2038 年 | 4 字节 | 创建时间、更新时间 |
YEAR | YYYY | 1901 ~ 2155 | 1 字节 | 年份 |
DATETIME vs TIMESTAMP
| 对比 | DATETIME | TIMESTAMP |
|---|---|---|
| 存储 | 8 字节 | 4 字节 |
| 范围 | 1000-01-01 ~ 9999-12-31 | 1970-01-01 ~ 2038-01-19 |
| 时区 | 存储原始值,不转换时区 | 存储 UTC,自动转换时区 |
| 默认值 | 无 | 可设为 CURRENT_TIMESTAMP |
CREATE TABLE logs (
id INT PRIMARY KEY AUTO_INCREMENT,
-- TIMESTAMP 自动追踪创建和更新时间
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
TIMESTAMP 使用 32 位整数存储 Unix 时间戳,最大只能到 2038-01-19 03:14:07 UTC。如果你的数据需要存储 2038 年以后的日期(如合同到期日),请使用 DATETIME。MySQL 8.0.28+ 已支持 64 位 TIMESTAMP 来解决这个问题。
JSON 类型
MySQL 5.7+ 和 PostgreSQL 都支持 JSON 类型,适合存储半结构化数据:
CREATE TABLE user_settings (
user_id INT PRIMARY KEY,
-- JSON 类型存储用户设置
preferences JSON
);
-- 插入 JSON 数据
INSERT INTO user_settings VALUES (1, '{"theme": "dark", "lang": "zh-CN", "notifications": {"email": true, "sms": false}}');
-- 查询 JSON 字段(MySQL)
SELECT
user_id,
preferences->>'$.theme' AS theme, -- 提取值
preferences->>'$.notifications.email' AS email_notify
FROM user_settings;
-- 查询 JSON 字段(PostgreSQL,使用 JSONB 更推荐)
SELECT
user_id,
preferences->>'theme' AS theme,
preferences->'notifications'->>'email' AS email_notify
FROM user_settings;
JSON 适合:
- 配置项、用户偏好等不常查询的数据
- Schema 经常变化的数据
- 不需要在该字段上建索引的数据
不适合存储核心业务数据(如订单金额),因为 JSON 查询性能不如普通列。
二进制类型
| 类型 | 最大长度 | 使用场景 |
|---|---|---|
BINARY(n) | 255 字节 | 固定长度二进制 |
VARBINARY(n) | 65535 字节 | 可变长度二进制 |
BLOB | 65535 字节 | 二进制大对象(图片、文件) |
图片、视频等大文件应该存储在对象存储(如 S3、OSS) 中,数据库只存 URL。在数据库中存储 BLOB 会导致表膨胀、备份困难、查询变慢。
PostgreSQL 特有类型
PostgreSQL 提供了很多强大的数据类型,在某些场景下非常有用:
| 类型 | 说明 | 使用场景 |
|---|---|---|
JSONB | 二进制 JSON,支持索引 | 配置数据、半结构化数据 |
UUID | 通用唯一标识符 | 分布式系统主键 |
ARRAY | 数组类型 | 标签、权限列表 |
INET / CIDR | IP 地址 / 网段 | 网络管理 |
INTERVAL | 时间间隔 | INTERVAL '3 days' |
SERIAL / BIGSERIAL | 自增序列 | 主键 |
TSRANGE / DATERANGE | 范围类型 | 预订时间段、价格区间 |
TSVECTOR | 全文搜索 | 站内搜索 |
-- PostgreSQL 数组类型
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
tags TEXT[] DEFAULT '{}' -- 字符串数组
);
INSERT INTO articles (title, tags) VALUES
('学习 PostgreSQL', ARRAY['database', 'postgresql', 'tutorial']);
-- 查询包含 'database' 标签的文章
SELECT * FROM articles WHERE 'database' = ANY(tags);
-- PostgreSQL 范围类型:酒店房间预订
CREATE TABLE hotel_bookings (
id SERIAL PRIMARY KEY,
room_id INT NOT NULL,
stay DATERANGE NOT NULL, -- 入住日期范围
-- 同一房间的预订不能重叠
EXCLUDE USING gist (room_id WITH =, stay WITH &&)
);
类型转换
隐式转换(自动)
-- MySQL 会自动将字符串转为数字进行比较
SELECT * FROM products WHERE price > '100'; -- '100' 被转为 100
-- ⚠️ 隐式转换可能导致索引失效!
SELECT * FROM users WHERE phone = 13800138000;
-- phone 是 VARCHAR,但查询用了数字!MySQL 会把 phone 列逐行转为数字比较 → 全表扫描
显式转换
-- MySQL
SELECT CAST('123' AS SIGNED); -- 字符串 → 整数
SELECT CAST(123 AS CHAR); -- 整数 → 字符串
SELECT CAST('2024-01-01' AS DATE); -- 字符串 → 日期
SELECT CONVERT('123', SIGNED); -- 另一种语法
-- PostgreSQL(使用 :: 语法更简洁)
SELECT '123'::INTEGER;
SELECT 123::TEXT;
SELECT '2024-01-01'::DATE;
当 WHERE 条件中的数据类型与列类型不匹配时,MySQL 会做隐式类型转换,导致索引失效。例如 VARCHAR 列用数字查询、日期列用字符串比较等。面试中经常考这个问题。
类型选择最佳实践
实用选型口诀:
- 整数用
INT,大数用BIGINT - 金额用
DECIMAL,绝不用FLOAT - 短文本用
VARCHAR,长文本用TEXT - 时间用
DATETIME或TIMESTAMP - 布尔用
TINYINT(1) - 灵活数据用
JSON
常见面试问题
Q1: DECIMAL 和 FLOAT/DOUBLE 的区别?为什么金额要用 DECIMAL?
答案:
FLOAT/DOUBLE 使用 IEEE 754 标准的浮点数表示,存在精度损失:
-- FLOAT 精度问题演示
SELECT CAST(0.1 AS FLOAT) + CAST(0.2 AS FLOAT);
-- 结果可能是 0.30000001192092896,不是 0.3
-- DECIMAL 精确计算
SELECT CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2));
-- 结果精确是 0.30
DECIMAL 使用字符串形式存储,内部按照十进制精确计算,不会有精度丢失。金额、价格、利率等涉及钱的数据必须使用 DECIMAL。
Q2: VARCHAR(100) 和 VARCHAR(200) 存同样的数据,有区别吗?
答案:
存储空间上没有差异,都只占实际长度 + 1~2 字节。但有一些间接影响:
- 内存占用:MySQL 在排序(ORDER BY)、创建临时表时,会按最大长度分配内存。
VARCHAR(200)会比VARCHAR(100)多分配内存 - 约束效果:
VARCHAR(100)会阻止插入超过 100 个字符的数据,起到数据校验作用 - 设计规范:精确的长度定义能更好地表达业务语义
建议:根据业务实际需要设置合理长度,不要一味用最大值。
Q3: DATETIME 和 TIMESTAMP 怎么选?
答案:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 记录创建/更新时间 | TIMESTAMP | 自动管理、节省空间、时区转换 |
| 存储未来日期(如合同到期) | DATETIME | 避免 2038 年问题 |
| 跨时区系统 | TIMESTAMP | 自动 UTC ↔ 本地时区转换 |
| 纯粹记录日期时间 | DATETIME | 不需要时区转换,所见即所得 |
Q4: 如何存储 IP 地址?
答案:
有两种常见方式:
-- 方式 1:VARCHAR(简单直观,但浪费空间)
ip_address VARCHAR(45) -- 兼容 IPv6
-- 方式 2:INT UNSIGNED(省空间,查询快,但仅限 IPv4)
ip_address INT UNSIGNED
-- 转换函数
SELECT INET_ATON('192.168.1.1'); -- 3232235777(字符串 → 整数)
SELECT INET_NTOA(3232235777); -- '192.168.1.1'(整数 → 字符串)
-- MySQL 5.6+ 支持 IPv6
SELECT HEX(INET6_ATON('::1'));
推荐:需要支持 IPv6 用 VARCHAR(45),纯 IPv4 且追求性能用 INT UNSIGNED。
Q5: ENUM 类型有什么优缺点?
答案:
优点:
- 存储空间小(1~2 字节),比 VARCHAR 节省
- 限制取值范围,保证数据合法性
缺点:
- 修改枚举值需要
ALTER TABLE,在大表上很慢 - 不利于国际化
- 移植性差(不同数据库实现不同)
替代方案:使用 TINYINT + 应用层映射,或创建字典表。