Je me demandais quel était le meilleur moyen d'implémenter un système de tags, comme celui utilisé sur SO. J'y pensais, mais je ne peux pas trouver une bonne solution évolutive.
Je pensais avoir une solution de base à 3 tables: avoir une table tags
, une table articles
et une table tag_to_articles
.
Est-ce la meilleure solution à ce problème ou existe-t-il des alternatives? En utilisant cette méthode, la table deviendrait extrêmement longue, et je suppose que ce n'est pas très efficace pour chercher. D'un autre côté, il n'est pas important que la requête s'exécute rapidement.
Je pense que vous trouverez intéressant ce billet de blog: Tags: schémas de base de données
Le problème: vous voulez avoir un schéma de base de données où vous pouvez marquer un signet (ou un article de blog ou quoi que ce soit) avec autant de balises que vous voulez . Ensuite, vous souhaitez exécuter des requêtes pour contraindre les signets à un union ou intersection de tags. Vous souhaitez également exclure (par exemple: moins) certaines balises du résultat de la recherche.
Dans cette solution, le schéma n'a qu'une seule table, il est dénormalisé. Ce type s'appelle «solution MySQLicious» car MySQLicious importe les données del.icio.us dans une table dotée de cette structure.
Intersection (AND) Requête pour “search + webservice + semweb”:
SELECT *
FROM `delicious`
WHERE tags LIKE "%search%"
AND tags LIKE "%webservice%"
AND tags LIKE "%semweb%"
Union (OR) Requête pour "search | webservice | semweb":
SELECT *
FROM `delicious`
WHERE tags LIKE "%search%"
OR tags LIKE "%webservice%"
OR tags LIKE "%semweb%"
Minus Requête pour "search + webservice-semweb"
SELECT *
FROM `delicious`
WHERE tags LIKE "%search%"
AND tags LIKE "%webservice%"
AND tags NOT LIKE "%semweb%"
Scuttle organise ses données dans deux tables. Cette table «scCategories» est la table «tag» et possède une clé étrangère pour la table «bookmark».
Intersection (AND) Requête pour "signet + webservice + semweb":
SELECT b.*
FROM scBookmarks b, scCategories c
WHERE c.bId = b.bId
AND (c.category IN ('bookmark', 'webservice', 'semweb'))
GROUP BY b.bId
HAVING COUNT( b.bId )=3
Tout d'abord, toutes les combinaisons signet-étiquette sont recherchées, où l'étiquette est "signet", "webservice" ou "semweb" (c.category IN ("signet", "webservice", "semweb")), puis uniquement les signets les trois balises recherchées sont prises en compte (HAVING COUNT (b.bId) = 3).
Union (OR) Requête pour "bookmark | webservice | semweb": Il suffit de laisser de côté la clause HAVING pour obtenir l'union:
SELECT b.*
FROM scBookmarks b, scCategories c
WHERE c.bId = b.bId
AND (c.category IN ('bookmark', 'webservice', 'semweb'))
GROUP BY b.bId
Minus (Exclusion) Requête pour "signet + webservice-semweb", c’est-à-dire: signet ET webservice ET PAS semweb.
SELECT b. *
FROM scBookmarks b, scCategories c
WHERE b.bId = c.bId
AND (c.category IN ('bookmark', 'webservice'))
AND b.bId NOT
IN (SELECT b.bId FROM scBookmarks b, scCategories c WHERE b.bId = c.bId AND c.category = 'semweb')
GROUP BY b.bId
HAVING COUNT( b.bId ) =2
Si vous omettez HAVING COUNT, la requête relative à «bookmark | webservice-semweb» est activée.
Toxi est venu avec une structure à trois tables. Via le tableau “tagmap”, les signets et les balises sont liés de n à m. Chaque balise peut être utilisée avec différents signets et inversement. Ce schéma de base de données est également utilisé par wordpress . Les requêtes sont quasiment identiques à celles de la solution «scuttle».
Intersection (AND) Requête pour "signet + webservice + semweb"
SELECT b.*
FROM tagmap bt, bookmark b, tag t
WHERE bt.tag_id = t.tag_id
AND (t.name IN ('bookmark', 'webservice', 'semweb'))
AND b.id = bt.bookmark_id
GROUP BY b.id
HAVING COUNT( b.id )=3
Union (OR) Requête pour "bookmark | webservice | semweb"
SELECT b.*
FROM tagmap bt, bookmark b, tag t
WHERE bt.tag_id = t.tag_id
AND (t.name IN ('bookmark', 'webservice', 'semweb'))
AND b.id = bt.bookmark_id
GROUP BY b.id
Minus (Exclusion) Requête pour "signet + webservice-semweb", c’est-à-dire: signet ET webservice ET PAS semweb.
SELECT b. *
FROM bookmark b, tagmap bt, tag t
WHERE b.id = bt.bookmark_id
AND bt.tag_id = t.tag_id
AND (t.name IN ('Programming', 'Algorithms'))
AND b.id NOT IN (SELECT b.id FROM bookmark b, tagmap bt, tag t WHERE b.id = bt.bookmark_id AND bt.tag_id = t.tag_id AND t.name = 'Python')
GROUP BY b.id
HAVING COUNT( b.id ) =2
Si vous omettez HAVING COUNT, la requête relative à «bookmark | webservice-semweb» est lancée.
Rien de mal avec votre solution à trois tables.
Une autre option consiste à limiter le nombre de balises pouvant être appliquées à un article (comme 5 dans SO) et à les ajouter directement à la table des articles.
La normalisation de la base de données présente des avantages et des inconvénients, tout comme le câblage dans une seule table présente des avantages et des inconvénients.
Rien ne dit que vous ne pouvez pas faire les deux. Il est contraire aux paradigmes de bases de données relationnelles de répéter des informations, mais si l'objectif est la performance, vous devrez peut-être casser les paradigmes.
L'implémentation que vous proposez avec trois tables fonctionnera pour le marquage.
Le débordement de pile utilise cependant une implémentation différente. Ils stockent les balises dans la colonne varchar de la table posts en texte brut et utilisent l'indexation de texte intégral pour récupérer les publications qui correspondent aux balises. Par exemple posts.tags = "algorithm system tagging best-practices"
. Je suis sûr que Jeff en a déjà parlé quelque part, mais j'oublie où.
La solution proposée est la meilleure solution, si ce n’est la seule possible, à laquelle je puisse penser pour traiter la relation multiple entre étiquettes et articles. Donc, mon vote est «oui, c'est toujours le meilleur». Je serais intéressé par toute alternative cependant.
Je voudrais suggérer une optimisation de MySQLicious pour de meilleures performances. Avant cela, les inconvénients de la solution Toxi (3 table) sont:
Si vous avez des millions de questions et qu'il y a 5 balises dans chacune, il y aura 5 millions d'entrées dans le tableau tagmap. Nous devons donc d’abord filtrer 10 000 entrées de tagmap en fonction de la recherche par étiquette, puis filtrer à nouveau les questions correspondantes de ces 10 000. Donc, tout en filtrant si l'identifiant artistique est numérique, c'est correct, mais s'il s'agit d'une sorte d'UUID (32 varchar), le filtrage nécessite une comparaison plus importante bien qu'il soit indexé.
Ma solution:
Chaque fois qu'une nouvelle balise est créée, ayez counter ++ (base 10) et convertissez ce compteur en base64. Maintenant, chaque nom de balise aura un identifiant base64. et passez cet identifiant à l'interface utilisateur avec name . De cette façon, vous aurez un maximum de deux identifiants de caractères jusqu'à ce que 4095 tags soient créés dans notre système. Maintenant, concaténez ces multiples balises dans chaque colonne de balises du tableau de questions. Ajoutez également un délimiteur et triez-le.
Donc, la table ressemble à ceci
Pendant l'interrogation, interrogez sur id au lieu du nom de balise réel . Comme il s'agit de TRIÉ, la condition and
sur la balise sera plus efficace (LIKE '%|a|%|c|%|f|%
).
Notez que le séparateur simple espace n'est pas suffisant et que nous avons besoin du double délimiteur pour différencier les balises telles que sql
et mysql
car LIKE "%sql%"
renverra également les résultats mysql
. Devrait être LIKE "%|sql|%"
Je sais que la recherche n’est pas indexée, mais vous avez peut-être indexé sur d’autres colonnes liées à un article tel que author/dateTime, sinon cela mènera à un balayage complet du tableau.
Enfin, avec cette solution, aucune jointure interne n'est requise, où des millions d'enregistrements doivent être comparés à 5 millions d'enregistrements en condition de jointure.
Si votre base de données prend en charge les tableaux indexables (tels que PostgreSQL, par exemple), je vous recommanderais une solution entièrement dénormalisée: des balises de stockage comme un tableau de chaînes sur la même table. Sinon, une table secondaire mappant des objets sur des balises est la meilleure solution. Si vous devez stocker des informations supplémentaires sur les balises, vous pouvez utiliser une table de balises distincte, mais il est inutile d'introduire une seconde jointure pour chaque recherche de balise.
CREATE TABLE Tags (
tag VARHAR(...) NOT NULL,
bid INT ... NOT NULL,
PRIMARY KEY(tag, bid),
INDEX(bid, tag)
)
Remarques:
AUTO_INCREMENT
PK. Par conséquent, il vaut mieux que Scuttle.LIKE
avec le caractère joker lead; faux hits sur les sous-chaînes)Discussions associées (pour MySQL):
many: nombreuses optimisations de tables de mappage
listes ordonnées