10

我有一张users桌子,我想定义两个任意用户之间的“朋友”关系。

到目前为止,我为此使用了两种不同的方法:

  1. friends表包含user1user2。搜索用户涉及一个看起来像 的查询
    ... WHERE @userid IN (`user1`,`user2`),这不是非常有效
  2. friends表包含fromto字段。发起好友请求会在该方向创建一行,如果接受,则以相反方向插入第二行。还有一status列表明发生了这种情况,使搜索类似于:
    ... WHERE `user1`=@userid AND `status`=1

我对这两种解决方案都不是特别满意。第一个对这种IN用法感到混乱,第二个似乎臃肿,有两行来定义一个链接。

所以这就是我在这里的原因。对于这样的链接,您有什么建议?请注意,我不需要保存更多信息,我只需要两个相互关联的用户 ID,最好是某种状态,例如ENUM('pending','accepted','blocked'),但这是可选的,具体取决于最佳设计是什么。

4

5 回答 5

8

一般有两种方法:

  1. 将每个朋友对存储一次,首先存储 id 最小的朋友。

    CREATE TABLE
            friend
            (
            l INT NOT NULL,
            g INT NOT NULL,
            PRIMARY KEY
                    (l, g),
            KEY (g)
            )
    
  2. 将每个朋友对存储两次,两种方式:

    CREATE TABLE
            (
            user INT NOT NULL,
            friend INT NOT NULL,
            PRIMARY KEY
                    (user, friend)
            )
    

要存储其他字段,如友谊状态、接受日期等。您通常使用第二个表,原因我将在下面描述。

要检索每个用户的朋友列表,请执行以下操作:

SELECT  CASE @myuserid WHEN l THEN g ELSE l END
FROM    friend
WHERE   l = @myuserid 
        OR
        g = @myuserid

或者

SELECT  g
FROM    friend
WHERE   l = @myuserid
UNION
SELECT  l
FROM    friend
WHERE   g = @myuserid

对于第一个解决方案;和

SELECT  friend
FROM    friend
WHERE   user = @friend

要检查两个用户是否是朋友,请发出以下命令:

SELECT  NULL
FROM    friend
WHERE   (l, g) =
        (
        CASE WHEN @user1 < @user2 THEN @user1 ELSE @user2 END,
        CASE WHEN @user1 > @user2 THEN @user1 ELSE @user2 END
        )

或者

SELECT  NULL
FROM    friend
WHERE   (user, friend) = (@user1, @user2)

存储方面,这两种解决方案几乎相同。第一个(最小/最大)解决方案存储的行数减少了两倍,但是,为了使其快速工作,您应该在 上设置一个二级索引g,事实上,它必须存储g加上表的主键部分不在二级索引(即l)。因此,每条记录有效地存储了两次:一次在表本身中,一次在索引中g

在性能方面,解决方案也几乎相同。但是,第一个需要两次索引查找,然后是索引扫描(对于“所有朋友”),第二个只需一次索引查找,因此对于 L/G 解决方案,I/O 量可能会略多一些。一个单一的索引可能比两个独立的索引更深一层,因此初始搜索可能需要多读一页。与 L/G 相比,这可能会稍微减慢对“双对”解决方案的“他们是朋友”的查询。


至于额外数据的附加表,您很可能需要它,因为它通常比我上面描述的两个查询使用得少得多(通常仅用于历史目的)。

它的布局还取决于您使用的查询类型。比如说,如果你想“显示我最近的十个友谊”,那么你可能希望将时间戳存储在“两对”中,这样​​你就不必进行文件排序等。

于 2013-06-17T14:01:15.083 回答
0

Performance considerations aside, another option might be a "friend" table in which one row represents a friend (does not matter which way around), together with a view which produces two result rows (one in each direction) for any friend row. In use, it would simplify queries because it could be used in the same way as the "two row" solution while only requiring one data row per "friendship".

The only drawback could be performance... depending on how the query optimizer works.

于 2013-06-18T22:38:23.420 回答
0

我试着有创意,这里有一些结果。

画得比说的容易,


图表

一个简单的对表朋友的触发器会做一个很好的服务,订购 (user1,user2) 而不会忘记谁请求友谊。

CREATE TRIGGER `friends_insert` BEFORE INSERT ON friends
FOR EACH ROW BEGIN
  DECLARE X INT UNSIGNED;
  IF NEW.user1 > NEW.user2 THEN
    SET X = NEW.user1;
    SET NEW.user1 = NEW.user2;
    SET NEW.user2 = X;
    SET NEW.invited_by = 1;
  END IF;
END$$

最后,假设用户 U 的 id = x。我们可以说 U 将表用户分为两部分:id < x 的用户和 id > x 的用户。在将元组插入表 Friends 之前我们对其 id 进行排序,因此某些信息不会被显式写入两次。我们通过联合 U 的 id < x 和 id > x 的朋友来获得用户 U (id = x) 的朋友:

SELECT user1 AS `friend_id` FROM friends
  WHERE user1<@id AND user2=@id
UNION
SELECT user2 AS `friend_id` FROM friends
  WHERE user2>@id AND user1=@id;

这里的主要目标是查询性能。在这两种情况下进行划分将有助于 MySQL 为每种情况使用正确的索引。
[问题和分歧的时间。也许您想要完整的 SQL;它显示在这里]

于 2013-06-20T06:22:32.913 回答
0

你可以试试这样的 SQLFiddle:http ://sqlfiddle.com/#!2/219dae/3/0

这是代码:

架构:

-- This is the users table:
CREATE TABLE users
    (
     u_id int auto_increment, 
     username varchar(20),
     PRIMARY KEY (u_id) 
    );

INSERT INTO users (username)
VALUES ('user1'),
       ('user2'),
       ('user3'),
       ('user4'),
       ('user5');

-- This is the friends table:

CREATE TABLE friends
    (
     f_id int auto_increment,
     r_name varchar(20), -- the name of the user that requests for friendship
     a_name varchar(20), -- the name of the user that answers the friendship request
     status varchar(20), -- the status of the request
     PRIMARY KEY (f_id)
    );

-- below, user1 sends frind requests to user2, user3, user4 and user5; and receives one from user2:

INSERT INTO friends (r_name, a_name, status)
VALUES ('user1','user2', 'pending');

INSERT INTO friends (r_name, a_name, status)
VALUES ('user1','user3', 'pending');

INSERT INTO friends (r_name, a_name, status)
VALUES ('user1','user4', 'pending');

INSERT INTO friends (r_name, a_name, status)
VALUES ('user1','user5', 'pending');

INSERT INTO friends (r_name, a_name, status)
VALUES ('user2','user1', 'pending');

-- user1 accepts user2 request to be his friend:

UPDATE friends
SET status='accepted'
WHERE a_name='user1' AND r_name='user2';

-- user3 accepts user1 request to be his friend:

UPDATE friends
SET status='accepted'
WHERE a_name='user3' AND r_name='user1';

和选择:

-- here we select all friend requests that the user1 received and all friend requests that he made

SELECT r_name, a_name, status FROM users
INNER JOIN friends ON users.username=friends.a_name
WHERE username='user1'

UNION

SELECT r_name, a_name, status FROM users
INNER JOIN friends ON users.username=friends.r_name
WHERE username='user1'
于 2013-06-21T13:12:11.450 回答
0

Consider the following schema:

CREATE TABLE `users` (
  `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(30) NOT NULL,
  PRIMARY KEY (`uid`)
);

INSERT INTO `users` (`uid`, `username`) VALUES
(1, 'h2ooooooo'),
(2, 'water'),
(3, 'liquid'),
(4, 'wet');


CREATE TABLE `friends` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `uid_from` int(10) unsigned NOT NULL,
  `uid_to` int(10) unsigned NOT NULL,
  `status` enum('pending','accepted','blocked') NOT NULL,
  PRIMARY KEY (`id`),
  KEY `uid_from` (`uid_from`),
  KEY `uid_to` (`uid_to`)
);

INSERT INTO `friends` (`id`, `uid_from`, `uid_to`, `status`) VALUES
(1, 1, 3, 'accepted'), -- h2ooooooo sent a friend request to liquid - accepted
(2, 1, 2, 'pending'), -- h2ooooooo sent a friend request to water - pending
(3, 4, 1, 'pending'), -- wet sent a friend request to h2ooooooo - pending
(4, 4, 2, 'pending'), -- wet sent a friend request to water - pending
(5, 3, 4, 'accepted'); -- liquid sent a friend request to wet - accepted

I'd use something like the following:

SELECT
    fu.username as `friend_username`,
    fu.uid as `friend_uid`
FROM
    `users` as `us`
LEFT JOIN
    `friends` as `fr`
ON
    (fr.uid_from = us.uid OR fr.uid_to = us.uid)
LEFT JOIN
    `users` as `fu`
ON
    (fu.uid = fr.uid_from OR fu.uid = fr.uid_to)
WHERE
    fu.uid != us.uid
AND
    fr.status = 'accepted'
AND
    us.username = 'liquid'

Result:

friend_username | friend_uid
----------------|-----------
h2ooooooo       | 1
wet             | 4

Here us would be the user you want to query for friends, and fu would be the users friends. You could easily change the WHERE statement to select the user in whatever whay you want. The status could be changed to pending (and should only join on uid_to) if you want to find friends request that the users hasn't answered.

DEMO ON SQLFIDDLE

The EXPLAIN if we use us.uid to match the user (as it's indexed):

enter image description here

于 2013-06-12T09:16:37.070 回答