第3章 数据库设计:核心表结构设计与索引优化

说到数据库设计,我个人的经验是:表结构决定了系统的上限,索引决定了系统的下限。你想想看,密钥权限审计系统每天要处理几百万甚至上千万条日志,表没设计好,后面再怎么优化都打折扣。

这一章我们重点拆解四张核心表——用户表、密钥表、权限表、审计日志表。我会结合踩过的坑,把字段选择、类型取舍、索引策略一次讲透。

核心表结构关系图 用户表 (users) user_id (PK/UUID) username (VARCHAR) email (VARCHAR) status (TINYINT) 密钥表 (s) key_id (PK/雪花ID) user_id (FK/UUID) key_hash (CHAR-64) expires_at (DATETIME) is_revoked (TINYINT) 权限表 (permissions) perm_id (PK/雪花ID) key_id (FK/雪花ID) resource (VARCHAR) action (VARCHAR) 审计日志表 (audit_logs) log_id (PK/雪花ID), key_id (FK), user_id (FK) action (VARCHAR), resource (VARCHAR), result (TINYINT) ip_address (VARCHAR-45), created_at (DATETIME-6) 日志表按时间分区 使用DATETIME(6)微秒精度

3.1 用户表:最容易被低估的表

用户表看似简单,但很多系统到后期出问题,源头都在用户表设计偷懒了。

字段类型说明
user_idBINARY(16) / CHAR(36)主键,推荐UUID或雪花ID
usernameVARCHAR(64)唯一索引,不区分大小写
emailVARCHAR(255)可空,但要有唯一索引
password_hashCHAR(60)bcrypt输出长度固定
statusTINYINT(1)0=禁用, 1=启用, 2=锁定
created_atDATETIME(3)精确到毫秒
updated_atDATETIME(3)记录更新时间
我的习惯:主键从来不用自增ID。分布式环境下自增ID很容易撞车,碰过分库分表你就懂了。UUID虽然长了点,但省去中心化发号器的成本。
索引策略:username加唯一索引,email加唯一索引,status加普通索引——这仨组合就是用户维度的查询主力。

3.2 密钥表:安全性的第一道防线

密钥表是整个系统的核心。说白了,密钥泄露 = 权限失控。我在项目中遇到过因为密钥明文存储导致的数据泄露事故,从那以后我对密钥表的设计就特别较真。

字段类型关键点
key_idBIGINT雪花ID,有序且唯一
user_idBINARY(16)外键,关联用户表
key_hashCHAR(64)SHA-256哈希值,不是明文!
key_prefixVARCHAR(8)密钥前8位,用于快速查找
expires_atDATETIME过期时间,到秒即可
is_revokedTINYINT(1)0/1,撤销标记
last_used_atDATETIME(3)最近使用时间
我曾经踩过一个坑:最开始把完整的密钥明文存在key_hash字段里,觉得反正加密过没事。结果日志不小心打到ELK,密钥全暴露了。后来改成只存SHA-256哈希,而且对哈希值加盐。
正确做法:密钥只在生成时返回一次,数据库里只存 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_idBIGINT雪花ID,按时间有序
key_idBIGINT查询“某把密钥的所有操作”时用到
user_idBINARY(16)查询“某个用户的操作历史”时用到
actionVARCHAR(32)精确查询,不要模糊搜索
resourceVARCHAR(256)只做前缀匹配,前64位加索引
resultTINYINT(1)0=失败, 1=成功, 2=拒绝
ip_addressVARCHAR(45)IPv4/IPv6都能存下
created_atDATETIME(6)微秒精度,防止同一毫秒冲突

3.5 索引优化:我是怎么给四张表上“保险”的

关于索引,我总结了三条铁律:

  1. 等值查询 → 哈希索引/唯一索引(用户表的username、密钥表的key_hash)
  2. 范围查询 → B+树索引(审计日志的created_at、密钥表的expires_at)
  3. 排序查询 → 覆盖索引(把查询字段全部塞进索引里,避免回表)

实际案例:审计日志的复合索引设计

-- 查询场景:查某个用户最近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万行后才知道怎么调最合适。