对于旨在使用这些索引的查询,MySQL 可以有效地使用适当的索引,避免对表进行“扫描”操作。
如果您总是为用户处理完整的成就集、检索整个成就集并存储整个成就集,那么在单列中使用逗号分隔的列表可能是一种可行的方法。
但是......当您想要处理个人成就时,这种设计就会失效。例如,如果您要检索具有特定成就的用户列表。现在,您正在对所有用户的所有成就进行昂贵的全面扫描,进行“字符串搜索”,依赖于格式正确的字符串,而 MySQL 无法使用索引扫描来有效地检索该集合。
因此,根据经验,如果您从不需要单独访问成就,并且从不需要从数据库中的用户中删除成就,并且 从不需要为用户添加个人成就,您将永远将成就作为一个整体提取,并且只将它们作为一个整体存储,进出数据库,逗号分隔的列表是可行的。
我不愿推荐这种方法,因为它永远不会变成那样。不可避免地,您需要一个查询来获取具有特定成就的用户列表。
使用逗号分隔的列表列,您会遇到一些丑陋的 SQL:
SELECT a.user_id
FROM user_achievement_list a
WHERE CONCAT(',',a.list,',') LIKE '%,123,%'
在 MySQL 不能使用索引范围扫描来满足谓词的意义上说是丑陋的; MySQL 必须查看每一个成就列表,然后对每一个成就进行字符串扫描,从头到尾,找出一行是否匹配。
如果您想使用该列表中的单个值来执行连接操作,以“查找”另一个表中的一行,那将是非常痛苦的。那个 SQL 变得非常丑陋。
而且数据完整性的声明式执行是不可能的;您不能定义任何限制添加到列表中的值的外键约束,或者从它出现的每个列表中删除特定 achievement_id 的所有出现。
基本上,您“放弃”了关系数据存储的优势;所以不要指望数据库能够对这种类型的列进行任何工作。就数据库而言,它只是一个数据块,也可能是存储在该列中的 .jpg 图像,MySQL 不会帮助检索或维护该列表的内容。
另一方面,如果您采用存储单个行的设计,每个用户的每个成就作为单独的行,并且您有适当的可用索引,那么数据库在返回列表时可以更有效,并且 SQL 更直接:
SELECT a.user_id
FROM user_achievements a
WHERE a.achievement_id = 123
覆盖索引适用于该查询:
... ON user_achievements (achievement_id, user_id)
以user_id 作为前导列的索引将适用于其他查询:
... ON user_achievements (user_id, achievement_id)
跟进
使用EXPLAIN SELECT ... 查看 MySQL 生成的访问计划。
对于您的示例,检索给定用户的所有成就,MySQL 可以对索引进行范围扫描,以快速定位该用户的行集。 MySQL 不需要查看索引中的每个页面,索引结构为树(至少在 B-Tree 索引的情况下),因此它基本上可以消除它“知道”的一大堆页面您正在寻找的行不能。并且使用索引中的achievement_id,MySQL 可以直接从索引中返回结果集,而无需访问基础表中的页面。 (对于 InnoDB 引擎,PRIMARY KEY 是表的集群键,因此表本身实际上是一个索引。)
使用两列 InnoDB 表 (user_id, achievement_id),将这两列作为复合 PRIMARY KEY,您只需在 (achievement_id, user_id) 上添加一个二级索引。
跟进
问:二级索引是指包含复合(userID,成就ID)表的键的第三列。我的创建表查询看起来像这样
CREATE TABLE `UserFriends`
(`AccountID` BIGINT(20) UNSIGNED NOT NULL
,`FriendAccountID` BIGINT(20) UNSIGNED NOT NULL
,`Key` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT
, PRIMARY KEY (`Key`)
, UNIQUE KEY `AccountID` (`AccountID`, `FriendAccountID`)
);
答:不,我不是指添加第三列。如果表中仅有的两列是另一个表的外键(看起来它们引用同一个表,并且列都是 NOT NULL 并且列的组合存在 UNIQUE 约束......并且有表上没有其他属性,我会考虑根本不使用代理项作为主键。我会将 UNIQUE KEY 设为 PRIMARY KEY。
就个人而言,我会使用 InnoDB,并启用 innodb_file_per_table 选项。我的表定义看起来像这样:
CREATE TABLE user_friend
( account_id BIGINT(20) UNSIGNED NOT NULL COMMENT 'PK, FK ref account.id'
, friend_account_id BIGINT(20) UNSIGNED NOT NULL COMMENT 'PK, FK ref account.id'
, PRIMARY KEY (account_id, friend_account_id)
, UNIQUE KEY user_friend_UX1 (friend_account_id, account_id)
, CONSTRAINT FK_user_friend_user FOREIGN KEY (account_id)
REFERENCES account (id) ON UPDATE CASCADE ON DELETE CASCADE
, CONSTRAINT FK_user_friend_friend FOREIGN KEY (friend_account_id)
REFERENCES account (id) ON UPDATE CASCADE ON DELETE CASCADE
) Engine=InnoDB;