MySQL数据库上的逗号分隔列表

时间:2014-04-03 00:41:42

标签: mysql database

我正在为我的数据库中的用户实现朋友列表,其中列表将存储朋友accountID。

我已经在我的数据库中有类似的结构,我有一个单独的表,其中有一对accountID到achievementID,但我对这种方法的关注是效率低,因为如果有100万用户有100个成就每个表中有1亿条目。然后尝试为具有特定帐户ID的用户获得每项成就将是对该表的线性扫描(我认为)。

我正在考虑为我的朋友列表提供逗号分隔的accountID字符串,我意识到将数据作为字符串处理会有多烦人,但至少可以保证log(n)搜索时间对于将accountID作为主键,第二列作为列表字符串的用户。

我对这两种不同结构的搜索时间有误吗?

1 个答案:

答案 0 :(得分:6)

MySQL可以有效地使用适当的索引,用于设计使用这些索引的查询,避免扫描"在桌子上操作。

如果您始终为用户处理完整的成就集,检索整个集并存储整个集,那么单个列中的逗号分隔列表可能是一种可行的方法。

HOWEVER ...... 当您想要处理个人成就时,设计就会崩溃。例如,如果要检索具有特定成就的用户列表。现在,您正在为所有用户执行所有成就的昂贵全面扫描,执行"字符串搜索",依赖于格式正确的字符串,并且MySQL无法使用索引扫描来有效地检索该集合。

所以,经验法则,如果你从不需要单独访问成就,从不需要从数据库中的用户中删除成就,并且从不需要为用户添加个人成就,您将仅仅将成就作为整个集合拉出,并且仅将其存储为整个集合,进出数据库,逗号分隔列表是可行的。


我毫不犹豫地推荐这种方法,因为它从未如此。不可避免地,您需要一个查询来获取具有特定成就的用户列表。

使用逗号分隔的列表列,您将进入一些丑陋的SQL:

SELECT a.user_id
  FROM user_achievement_list a
 WHERE CONCAT(',',a.list,',') LIKE '%,123,%'
从某种意义上讲,MySQL不能使用索引范围扫描来满足谓词; MySQL必须查看每个单独的成就列表,然后从头到尾对每个成员进行字符串扫描,以查明行是否匹配。

如果你想使用该列表中的各个值来进行连接操作,那么它就是彻头彻尾的难以忍受的,以及#34;查找"另一个表中的一行。 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索引的情况下),因此它基本上可以消除整个页面的大量页面#34 ;知道&#34;您正在寻找的行不可能。并且在索引中也有achievement_id,MySQL可以直接从索引返回结果集,而无需访问基础表中的页面。 (对于InnoDB引擎,PRIMARY KEY是表的簇密钥,因此表本身实际上是一个索引。)

使用两列InnoDB表(user_id, achievement_id),将这两列作为复合PRIMARY KEY,您只需要在(achievement_id, user_id)上添加一个辅助索引。


<强>后续

:通过二级索引,是指包含复合(userID,achievementID)表的键的第3列。我的create table查询看起来像这样

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`)
);

A:不,我不是指添加第三列。如果表中只有两列是另一个表的外键(看起来它们引用同一个表,并且列都是NOT NULL并且列的组合有一个UNIQUE约束......并且没有其他属性,我会考虑不使用代理作为主键。我会将UNIQUE 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;