【问题标题】:Comma separated list on MySQL databaseMySQL数据库上的逗号分隔列表
【发布时间】:2014-04-03 14:08:47
【问题描述】:

我正在为我的数据库中的用户实现一个朋友列表,该列表将存储朋友帐户 ID。

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

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

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

【问题讨论】:

    标签: mysql database


    【解决方案1】:

    对于旨在使用这些索引的查询,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;
    

    【讨论】:

    • 感谢您的精彩帖子,作为对我最重要的问题的跟进,从表格中找到一个成就或所有成就是 O(n) 吗?表中将有多个 accountID 条目,因此我不确定数据库如何像对主键(必须是唯一的)一样优化此搜索。
    • @Josh Brittain:使用适当的覆盖索引,检索甚至比使用主键更有效。 (这取决于您要返回的行数;并且以“索引顺序”返回它们可以避免“使用文件排序”操作,这对于大型集合来说可能会很昂贵。
    • 二级索引是指包含复合(用户ID,成就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'));
    猜你喜欢
    • 2013-07-03
    • 2013-06-07
    • 2023-04-06
    • 1970-01-01
    • 2010-10-14
    • 2013-06-25
    • 2011-06-23
    • 1970-01-01
    相关资源
    最近更新 更多