Relations d'amis dans MySQL

8

Je développe une relation d'amitié dans MySQL où la relation d'ami est mutuelle. Si A est l'ami de B, alors B est l'ami de A. Si l'un des utilisateurs met fin à l'amitié, la relation tombe. Je veux savoir quelle voie est la meilleure.

J'ai un système en marche;

user
-----------
userid p.k
name 

friends
-------
userid
friendid
primary key (`userid`,`friendid`),
key `friendid` (`friendid`)

1 2
2 5
1 3


To get all of my friends;
SELECT u.name, f.friendid , IF(f.userid = $userid, f.friendid, f.userid) friendid 
FROM friends f 
    inner join user u  ON ( u.userid = IF(f.userid = $userid, f.friendid, f.userid)) 
WHERE ( f.userid = '$userid' or f.friendid = '$userid' ) 

Cette requête fonctionne bien. Je peux peut-être en ajouter un UNION. La requête est plus compliquée que celle ci-dessous et le tableau contient deux fois moins d'enregistrements que celui ci-dessous.

Une autre façon consiste à conserver les relations sur des lignes distinctes;

1 2
2 1
2 5
5 2
1 3
3 1

SELECT u.name, f.friendid 
FROM friends f inner join user u ON ( u.userid = f.friendid ) 
WHERE f.userid = '$userid'

Cette requête est simple, bien que la table occupe deux fois plus d'espace.

Ma crainte est; en supposant qu'il y a des millions d'utilisateurs; quel chemin fonctionnera plus rapidement?

Quels sont les avantages et les inconvénients des deux façons?

Que dois-je garder à l'esprit ou changer pour ces façons? Et à quels problèmes puis-je faire face dans les deux sens?

kent ilyuk
la source
C'était une bonne question que vous avez posée aujourd'hui. +1 pour votre question.
RolandoMySQLDBA

Réponses:

4

La première chose qui attire mon attention est la configuration de l'index friends.

Vous avez ceci en ce moment:

friends
-------
userid
friendid
primary key (`userid`,`friendid`),
key `friendid` (`friendid`)

Lorsque vous recoupez pour une amitié mutuelle, cela peut entraîner de petites dépenses car l'ID utilisateur peut être récupéré de la table lors de la traversée de l' friendidindex. Vous pourriez peut-être indexer comme suit:

friends
-------
userid
friendid
primary key (`userid`,`friendid`),
unique key `friendid` (`friendid`,`userid`)

Cela peut supprimer tout besoin d'accéder à la table et de rechercher l'index uniquement.

Maintenant, en termes de requêtes, les deux peuvent s'améliorer avec le nouvel index unique. La création de l'index unique élimine également la nécessité d'insérer (A,B)et (B,A)dans la table car (A,B)et (B,A)serait l'index de toute façon. Ainsi, la deuxième requête n'aurait pas à parcourir le tableau pour voir si quelqu'un est un ami de quelqu'un d'autre parce qu'une autre personne a initié l'amitié. De cette façon, si l'amitié est rompue par une seule personne, il n'y a pas d'amitié orpheline à sens unique (cela ressemble beaucoup à la vie de nos jours, n'est-ce pas?)

Votre première requête semble bénéficier davantage de l'index unique. Même avec des millions de lignes, localiser des amis en utilisant uniquement les index éviterait de toucher la table. Néanmoins, comme vous n'avez pas présenté de requête UNION, je voudrais recommander une requête UNION:

SET @givenuserid = ?;
SELECT B.name "Friend's Name"
FROM 
(
    SELECT userid FROM friends WHERE friendid=@givenuserid
    UNION
    SELECT friendid FROM friends WHERE userid=@givenuserid
) A INNER JOIN user B USING (userid);

Cela vous permettra de voir qui sont les amis de chaque ID utilisateur

Pour voir toutes les amitiés, lancez ceci:

SELECT A.userid,A.name,B.friendid,C.name
FROM user A
INNER JOIN friends B ON A.userid=B.userid
INNER JOIN user C on B.friendid=C.userid;

Tout d'abord, voici quelques exemples de données:

mysql> drop database if exists key_ilyuk;
Query OK, 2 rows affected (0.01 sec)

mysql> create database key_ilyuk;
Query OK, 1 row affected (0.00 sec)

mysql> use key_ilyuk
Database changed
mysql> create table user
    -> (
    ->     userid INT NOT NULL AUTO_INCREMENT,
    ->     name varchar(20),
    ->     primary key(userid)
    -> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.04 sec)

mysql> insert into user (name) values
    -> ('rolando'),('pamela'),('dominique'),('carlik'),('diamond');
Query OK, 5 rows affected (0.01 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> create table friends
    -> (
    ->     userid INT NOT NULL,
    ->     friendid INT NOT NULL,
    ->     primary key (userid,friendid),
    ->     unique key (friendid,userid)
    -> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.03 sec)

mysql> insert into friends values (1,2),(2,5),(1,3);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> select * from user;
+--------+-----------+
| userid | name      |
+--------+-----------+
|      1 | rolando   |
|      2 | pamela    |
|      3 | dominique |
|      4 | carlik    |
|      5 | diamond   |
+--------+-----------+
5 rows in set (0.00 sec)

mysql> select * from friends;
+--------+----------+
| userid | friendid |
+--------+----------+
|      1 |        2 |
|      1 |        3 |
|      2 |        5 |
+--------+----------+
3 rows in set (0.00 sec)

mysql>

Regardons toutes les relations

mysql> SELECT A.userid,A.name,B.friendid,C.name
    -> FROM user A
    -> INNER JOIN friends B ON A.userid=B.userid
    -> INNER JOIN user C on B.friendid=C.userid
    -> ;
+--------+---------+----------+-----------+
| userid | name    | friendid | name      |
+--------+---------+----------+-----------+
|      1 | rolando |        2 | pamela    |
|      1 | rolando |        3 | dominique |
|      2 | pamela  |        5 | diamond   |
+--------+---------+----------+-----------+
3 rows in set (0.00 sec)

mysql>

Examinons les 5 ID utilisateur et voyons si les relations sont correctement affichées

mysql> SET @givenuserid = 1;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT B.name "Friend's Name"
    -> FROM
    -> (
    ->     SELECT userid FROM friends WHERE friendid=@givenuserid
    ->     UNION
    ->     SELECT friendid FROM friends WHERE userid=@givenuserid
    -> ) A INNER JOIN user B USING (userid);
+---------------+
| Friend's Name |
+---------------+
| pamela        |
| dominique     |
+---------------+
2 rows in set (0.00 sec)

mysql> SET @givenuserid = 2;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT B.name "Friend's Name"
    -> FROM
    -> (
    ->     SELECT userid FROM friends WHERE friendid=@givenuserid
    ->     UNION
    ->     SELECT friendid FROM friends WHERE userid=@givenuserid
    -> ) A INNER JOIN user B USING (userid);
+---------------+
| Friend's Name |
+---------------+
| rolando       |
| diamond       |
+---------------+
2 rows in set (0.00 sec)

mysql> SET @givenuserid = 3;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT B.name "Friend's Name"
    -> FROM
    -> (
    ->     SELECT userid FROM friends WHERE friendid=@givenuserid
    ->     UNION
    ->     SELECT friendid FROM friends WHERE userid=@givenuserid
    -> ) A INNER JOIN user B USING (userid);
+---------------+
| Friend's Name |
+---------------+
| rolando       |
+---------------+
1 row in set (0.01 sec)

mysql> SET @givenuserid = 4;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT B.name "Friend's Name"
    -> FROM
    -> (
    ->     SELECT userid FROM friends WHERE friendid=@givenuserid
    ->     UNION
    ->     SELECT friendid FROM friends WHERE userid=@givenuserid
    -> ) A INNER JOIN user B USING (userid);
Empty set (0.00 sec)

mysql> SET @givenuserid = 5;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT B.name "Friend's Name"
    -> FROM
    -> (
    ->     SELECT userid FROM friends WHERE friendid=@givenuserid
    ->     UNION
    ->     SELECT friendid FROM friends WHERE userid=@givenuserid
    -> ) A INNER JOIN user B USING (userid);
+---------------+
| Friend's Name |
+---------------+
| pamela        |
+---------------+
1 row in set (0.00 sec)

mysql>

Ils me semblent tous corrects.

Maintenant, utilisons notre deuxième requête pour voir si elle correspond ...

mysql> SET @givenuserid = 1;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT u.name, f.friendid
    -> FROM friends f inner join user u ON ( u.userid = f.friendid )
    -> WHERE f.userid = @givenuserid;
+-----------+----------+
| name      | friendid |
+-----------+----------+
| pamela    |        2 |
| dominique |        3 |
+-----------+----------+
2 rows in set (0.00 sec)

mysql> SET @givenuserid = 2;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT u.name, f.friendid
    -> FROM friends f inner join user u ON ( u.userid = f.friendid )
    -> WHERE f.userid = @givenuserid;
+---------+----------+
| name    | friendid |
+---------+----------+
| diamond |        5 |
+---------+----------+
1 row in set (0.00 sec)

mysql> SET @givenuserid = 3;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT u.name, f.friendid
    -> FROM friends f inner join user u ON ( u.userid = f.friendid )
    -> WHERE f.userid = @givenuserid;
Empty set (0.00 sec)

mysql> SET @givenuserid = 4;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT u.name, f.friendid
    -> FROM friends f inner join user u ON ( u.userid = f.friendid )
    -> WHERE f.userid = @givenuserid;
Empty set (0.00 sec)

mysql> SET @givenuserid = 5;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT u.name, f.friendid
    -> FROM friends f inner join user u ON ( u.userid = f.friendid )
    -> WHERE f.userid = @givenuserid;
Empty set (0.00 sec)

mysql>

Pourquoi ne correspond pas? C'est parce que je n'ai pas chargé le (B,A)pour tous (A,B). Permettez-moi de charger les (B,A)relations et de réessayer votre deuxième requête.

mysql> insert into friends values (2,1),(5,2),(3,1);
Query OK, 3 rows affected (0.02 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> SET @givenuserid = 1;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT u.name, f.friendid
    -> FROM friends f inner join user u ON ( u.userid = f.friendid )
    -> WHERE f.userid = @givenuserid;
+-----------+----------+
| name      | friendid |
+-----------+----------+
| pamela    |        2 |
| dominique |        3 |
+-----------+----------+
2 rows in set (0.00 sec)

mysql> SET @givenuserid = 2;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT u.name, f.friendid
    -> FROM friends f inner join user u ON ( u.userid = f.friendid )
    -> WHERE f.userid = @givenuserid;
+---------+----------+
| name    | friendid |
+---------+----------+
| rolando |        1 |
| diamond |        5 |
+---------+----------+
2 rows in set (0.00 sec)

mysql> SET @givenuserid = 3;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT u.name, f.friendid
    -> FROM friends f inner join user u ON ( u.userid = f.friendid )
    -> WHERE f.userid = @givenuserid;
+---------+----------+
| name    | friendid |
+---------+----------+
| rolando |        1 |
+---------+----------+
1 row in set (0.00 sec)

mysql> SET @givenuserid = 4;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT u.name, f.friendid
    -> FROM friends f inner join user u ON ( u.userid = f.friendid )
    -> WHERE f.userid = @givenuserid;
Empty set (0.00 sec)

mysql> SET @givenuserid = 5;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT u.name, f.friendid
    -> FROM friends f inner join user u ON ( u.userid = f.friendid )
    -> WHERE f.userid = @givenuserid;
+--------+----------+
| name   | friendid |
+--------+----------+
| pamela |        2 |
+--------+----------+
1 row in set (0.00 sec)

mysql>

Ils ne correspondent toujours pas. C'est parce que votre deuxième requête ne vérifie qu'un côté.

Vérifions votre première requête par rapport à chaque valeur avec seulement (A, B) et non (B, A):

mysql> SET @givenuserid = 1;
SELECT u.name, f.friendid userid, IF(f.userid = @givenuserid, f.friendid, f.userid) friendid
FROM friends f
    inner join user u  ON ( u.userid = IF(f.userid = @givenuserid, f.friendid, f.userid))
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT u.name, f.friendid userid, IF(f.userid = @givenuserid, f.friendid, f.userid) friendid
    -> FROM friends f
    ->     inner join user u  ON ( u.userid = IF(f.userid = @givenuserid, f.friendid, f.userid))
    -> WHERE ( f.userid = @givenuserid or f.friendid = @givenuserid  );
+-----------+--------+----------+
| name      | userid | friendid |
+-----------+--------+----------+
| pamela    |      2 |        2 |
| dominique |      3 |        3 |
+-----------+--------+----------+
2 rows in set (0.00 sec)

mysql> SET @givenuserid = 2;
FROM friends f
    inner join user u  ON ( u.userid = IF(f.userid = @givenuserid, f.friendid, f.userid))
WHERE ( f.userid = @givenuserid or f.friendid = @givenuserid  );
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT u.name, f.friendid userid, IF(f.userid = @givenuserid, f.friendid, f.userid) friendid
    -> FROM friends f
    ->     inner join user u  ON ( u.userid = IF(f.userid = @givenuserid, f.friendid, f.userid))
    -> WHERE ( f.userid = @givenuserid or f.friendid = @givenuserid  );
+---------+--------+----------+
| name    | userid | friendid |
+---------+--------+----------+
| rolando |      2 |        1 |
| diamond |      5 |        5 |
+---------+--------+----------+
2 rows in set (0.00 sec)

mysql> SET @givenuserid = 3;
SELECT u.name, f.friendid userid, IF(f.userid = @givenuserid, f.friendid, f.userid) friendid
FROM friends f
    inner join user u  ON ( u.userid = IF(f.userid = @givenuserid, f.friendid, f.userid))
WHERE ( f.userid = @givenuserid or f.friendid = @givenuserid  );
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT u.name, f.friendid userid, IF(f.userid = @givenuserid, f.friendid, f.userid) friendid
    -> FROM friends f
    ->     inner join user u  ON ( u.userid = IF(f.userid = @givenuserid, f.friendid, f.userid))
    -> WHERE ( f.userid = @givenuserid or f.friendid = @givenuserid  );
+---------+--------+----------+
| name    | userid | friendid |
+---------+--------+----------+
| rolando |      3 |        1 |
+---------+--------+----------+
1 row in set (0.00 sec)

mysql> SET @givenuserid = 4;
FROM friends f
    inner join user u  ON ( u.userid = IF(f.userid = @givenuserid, f.friendid, f.userid))
WHERE ( f.userid = @givenuserid or f.friendid = @givenuserid  );
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT u.name, f.friendid userid, IF(f.userid = @givenuserid, f.friendid, f.userid) friendid
    -> FROM friends f
    ->     inner join user u  ON ( u.userid = IF(f.userid = @givenuserid, f.friendid, f.userid))
    -> WHERE ( f.userid = @givenuserid or f.friendid = @givenuserid  );
Empty set (0.01 sec)

mysql> SET @givenuserid = 5;
FROM friends f
Query OK, 0 rows affected (0.00 sec)

    inner join user u  ON ( u.userid = IF(f.userid = @givenuserid, f.friendid, f.userid))
mysql> SELECT u.name, f.friendid userid, IF(f.userid = @givenuserid, f.friendid, f.userid) friendid
    -> FROM friends f
    ->     inner join user u  ON ( u.userid = IF(f.userid = @givenuserid, f.friendid, f.userid))
    -> WHERE ( f.userid = @givenuserid or f.friendid = @givenuserid  );
+--------+--------+----------+
| name   | userid | friendid |
+--------+--------+----------+
| pamela |      5 |        2 |
+--------+--------+----------+
1 row in set (0.00 sec)

mysql>

Votre premier fonctionne bien. Je suis sûr qu'il bénéficie de l'index unique comme je l'ai dit plus tôt, mais à mon humble avis je pense que l'UNION est plus simple. Avec cet index unique, il semblerait qu'il s'agisse de six sur une douzaine et demie de l'autre en termes d'exécution et de sortie.

Vous devrez comparer votre première requête à ma suggestion UNION et voir.

C'était une bonne question que vous avez posée aujourd'hui. +1 pour votre question.

RolandoMySQLDBA
la source
J'ai fait quelques tests pour voir à quelle vitesse est la configuration actuelle. Je n'ai pas changé le schéma des tables. Première requête 1 000 000 de lignes (table utilisateur) 2 045 007 lignes (table d'amis - une ligne pour chaque relation. Les amitiés sont créées de manière aléatoire pour 10 000 utilisateurs). La première requête prend 0,01094 seconde pour renvoyer 600 lignes. La même requête modifiée avec UNION prend 0,0086 pour renvoyer 600 lignes. Deuxième requête 1 000 000 lignes (table utilisateur) 4 048 781 lignes (table friends_twoway - deux lignes pour chaque relation) La deuxième requête de mon premier message prend 0,0090 s. pour renvoyer 600 lignes. Que pensez-vous de ces résultats?
kent ilyuk
Après un tas de tests, je vais changer les paramètres de table, ajouter différents index comme vous l'avez suggéré.
kent ilyuk
Lors de votre premier test, .0086 (avec UNION) est meilleur que .01094 (sans UNION). En fait, c'est 27,21% plus rapide. Les performances de votre première requête avec deux fois plus de données sont plus lentes .0004 s. Même avec les nombres donnés, je pencherais toujours en faveur d'UNION avec seulement les données et créant un index unique parce que les index seraient pleinement utilisés dans la requête et laisseraient les données seules.
RolandoMySQLDBA
J'ai remplacé friendid-key par une clé unique ( friendid, userid) et maintenant les résultats sont à peu près .00794 Est-ce aussi rapide que possible? En regardant les résultats, pensez-vous que la première façon est meilleure (une ligne pour chaque relation)? Parce que c'est deux fois moins d'espace que le second et les résultats sont à peu près les mêmes avec les configurations actuelles.
kent ilyuk
Dans votre cas particulier, moins de données sont bonnes car elles reposent sur les index. Les index sont gonflés mais dans un but bénéfique. Ce concept est appelé couvrant les indices, dont le but est d'index créés dont WHERE, GROUP BYet les ORDER BYclauses résultent des données sont lues à partir des index uniquement. Voici quelques bons liens qui justifient l'utilisation des clés uniques et primaires comme index de couverture: 1) peter-zaitsev.livejournal.com/6949.html , 2) mysqlperformanceblog.com/2006/11/23/… , 3) ronaldbradford .com / blog / tag / couverture-index
RolandoMySQLDBA