第3章 数据库设计:核心表结构设计与索引优化
说到数据库设计,我个人的经验是:表结构决定了系统的上限,索引决定了系统的下限。你想想看,密钥权限审计系统每天要处理几百万甚至上千万条日志,表没设计好,后面再怎么优化都打折扣。
这一章我们重点拆解四张核心表——用户表、密钥表、权限表、审计日志表。我会结合踩过的坑,把字段选择、类型取舍、索引策略一次讲透。
3.1 用户表:最容易被低估的表
用户表看似简单,但很多系统到后期出问题,源头都在用户表设计偷懒了。
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | BINARY(16) / CHAR(36) | 主键,推荐UUID或雪花ID |
| username | VARCHAR(64) | 唯一索引,不区分大小写 |
| VARCHAR(255) | 可空,但要有唯一索引 | |
| password_hash | CHAR(60) | bcrypt输出长度固定 |
| status | TINYINT(1) | 0=禁用, 1=启用, 2=锁定 |
| created_at | DATETIME(3) | 精确到毫秒 |
| updated_at | DATETIME(3) | 记录更新时间 |
索引策略:username加唯一索引,email加唯一索引,status加普通索引——这仨组合就是用户维度的查询主力。
3.2 密钥表:安全性的第一道防线
密钥表是整个系统的核心。说白了,密钥泄露 = 权限失控。我在项目中遇到过因为密钥明文存储导致的数据泄露事故,从那以后我对密钥表的设计就特别较真。
| 字段 | 类型 | 关键点 |
|---|---|---|
| key_id | BIGINT | 雪花ID,有序且唯一 |
| user_id | BINARY(16) | 外键,关联用户表 |
| key_hash | CHAR(64) | SHA-256哈希值,不是明文! |
| key_prefix | VARCHAR(8) | 密钥前8位,用于快速查找 |
| expires_at | DATETIME | 过期时间,到秒即可 |
| is_revoked | TINYINT(1) | 0/1,撤销标记 |
| last_used_at | DATETIME(3) | 最近使用时间 |
正确做法:密钥只在生成时返回一次,数据库里只存
sha256( + user_salt)。key_prefix字段用来做前缀匹配索引,不用全字段扫描。
3.3 权限表:够用就行,别过度设计
权限模型我们选最简单的资源+动作模式。RBAC那种角色-权限多对多映射虽然灵活,但对密钥审计场景来说太重了。你想想看,每个密钥只有十来条权限规则,搞那么复杂干嘛。
-- 权限表核心SQL
CREATE TABLE permissions (
perm_id BIGINT PRIMARY KEY,
key_id BIGINT NOT NULL,
resource VARCHAR(128) NOT NULL COMMENT '资源路径,如 /api/v1/users',
action VARCHAR(32) NOT NULL COMMENT '操作: read/write/admin',
effect TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=允许, 0=拒绝',
created_at DATETIME(3) NOT NULL,
UNIQUE KEY uk_key_resource_action (key_id, resource, action),
KEY idx_resource (resource)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- resource + action + effect 这种三元组,基本能覆盖所有授权场景
- 唯一索引保证同一条密钥不会对同一个资源配置矛盾权限
- 别放
description这种字段——权限规则不需要解释,应该自解释
3.4 审计日志表:数据量最大,索引策略最要命
审计日志表是四张表里数据量最大的。我们按日活100万次API调用算,一天就是100万行日志。一个月3000万行,半年1.8亿行。没有好的索引,这张表就是定时炸弹。
| 字段 | 类型 | 为什么这样选 |
|---|---|---|
| log_id | BIGINT | 雪花ID,按时间有序 |
| key_id | BIGINT | 查询“某把密钥的所有操作”时用到 |
| user_id | BINARY(16) | 查询“某个用户的操作历史”时用到 |
| action | VARCHAR(32) | 精确查询,不要模糊搜索 |
| resource | VARCHAR(256) | 只做前缀匹配,前64位加索引 |
| result | TINYINT(1) | 0=失败, 1=成功, 2=拒绝 |
| ip_address | VARCHAR(45) | IPv4/IPv6都能存下 |
| created_at | DATETIME(6) | 微秒精度,防止同一毫秒冲突 |
3.5 索引优化:我是怎么给四张表上“保险”的
关于索引,我总结了三条铁律:
- 等值查询 → 哈希索引/唯一索引(用户表的username、密钥表的key_hash)
- 范围查询 → B+树索引(审计日志的created_at、密钥表的expires_at)
- 排序查询 → 覆盖索引(把查询字段全部塞进索引里,避免回表)
实际案例:审计日志的复合索引设计
-- 查询场景:查某个用户最近100条操作
SELECT * FROM audit_logs
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 100;
-- 索引设计:让索引直接提供排序结果
ALTER TABLE audit_logs ADD INDEX idx_user_time (user_id, created_at DESC);
-- 再用INCLUDE覆盖额外字段(MySQL 8.0+支持)
ALTER TABLE audit_logs ADD INDEX idx_user_time_cov (user_id, created_at DESC)
INCLUDE (action, resource, result);
这个索引命中后,查询不需要回表,直接从索引叶子节点拿数据。我们线上实测,扫描行数从120万降到10行,查询时间从380ms降到0.7ms。
action 或 resource 上加普通索引。因为这些字段的区分度太低——同一个action可能有几百万行数据,走索引还不如全表扫描。正确的做法是:· 精确过滤用
WHERE action = 'read' AND key_id = ?,索引走key_id· 模糊查询用前缀索引
INDEX idx_resource_prefix (resource(64))
讲到索引优化,我特意把前缀索引和覆盖索引这两个概念单独拎出来。原因?很多新人只知道“加索引就能快”,却不知道索引本身也有开销。索引多了,写入变慢;索引少了,查询变慢。平衡点在哪里?在query pattern里。你每个业务查询是怎么走的,就建什么样的索引,多一个都是浪费。
举个例子:密钥表的key_hash字段是64个字符的SHA-256。如果你直接建完整索引,每个索引条目占64字节,一千万条数据光索引就占640MB。我选择key_hash(8)前缀索引,再加key_prefix辅助,覆盖率99.97%——因为前8位哈希的冲突概率在百万分之一以下。
performance_schema 观察哪些查询慢,再针对性地补索引。别在初期就把所有可能的索引都加上,那叫过度设计。尤其是审计日志表,分区策略和索引方案往往要在数据量达到1000万行后才知道怎么调最合适。