2010-10-28 7 views
1

J'ai une base de données MySQL avec le schéma typique pour les éléments de marquage:SQL à des éléments lots re-tag

item (1->N) item_tag (N->1) tag 

Chaque balise a un nom et un compte de combien d'éléments ont cette étiquette -à-dire:

item 
(
item_id (UNIQUE KEY) 
) 

item_tag 
(
item_id (NON-UNIQUE INDEXED), 
tag_id (NON-UNIQUE INDEXED) 
) 

tag 
(
tag_id (UNIQUE KEY) 
name 
count 
) 

J'ai besoin d'écrire une routine de maintenance pour ré-étiqueter en lot une ou plusieurs balises existantes sur une seule balise nouvelle ou existante. Je dois m'assurer qu'après le retag, aucun article ne porte de balises en double et j'ai besoin de mettre à jour les comptes de chaque enregistrement de balise pour refléter le nombre d'articles réels utilisant cette balise.

Vous cherchez des suggestions sur la façon de mettre en œuvre cette efficace ...

Répondre

0

C'est ce que j'ai à ce jour, ce qui semble fonctionner, mais je n'ai pas assez de données encore savoir comment Eh bien, il effectue. Commentaires bienvenus.

Quelques notes:

  • Devait ajouter un champ d'identification unique à la table item_tags obtenir le travail de nettoyage de l'étiquette en double.
  • Ajout de la prise en charge des alias de balise afin d'enregistrer les balises de nouveau balises.
  • Je ne l'ai pas mentionné auparavant, mais chaque élément a également un indicateur publié et seuls les éléments publiés devraient affecter le champ de comptage sur les balises.
  • Le code utilise C#, subsonic + linq + "coding horror", mais est assez explicite.

Le code:

public static void Retag(string new_tag, List<string> old_tags) 
{ 
    // Check new tag name is valid 
    if (!Utils.IsValidTag(new_tag)) 
    { 
     throw new RuleException("NewTag", string.Format("Invalid tag name - {0}", new_tag)); 
    } 

    // Start a transaction 
    using (var scope = new SimpleTransactionScope(megDB.GetInstance().Provider)) 
    { 
     // Get the new tag 
     var newTag = tag.SingleOrDefault(x => x.name == new_tag); 

     // If the new tag is an alias, remap to the alias instead 
     if (newTag != null && newTag.alias != null) 
     { 
      newTag = tag.SingleOrDefault(x => x.tag_id == newTag.alias.Value); 
     } 

     // Get the old tags 
     var oldTags = new List<tag>(); 
     foreach (var old_tag in old_tags) 
     { 
      // Ignore same tag 
      if (string.Compare(old_tag, new_tag, true)==0) 
       continue; 
      var oldTag = tag.SingleOrDefault(x => x.name == old_tag); 
      if (oldTag != null) 
       oldTags.Add(oldTag); 
     } 

     // Redundant? 
     if (oldTags.Count == 0) 
      return; 

     // Simple rename? 
     if (oldTags.Count == 1 && newTag == null) 
     { 
      oldTags[0].name = new_tag; 
      oldTags[0].Save(); 
      scope.Complete(); 
      return; 
     } 

     // Create new tag? 
     if (newTag == null) 
     { 
      newTag = new tag(); 
      newTag.name = new_tag; 
      newTag.Save(); 
     } 

     // Build a comma separated list of old tag id's for use in sql 'IN' clause 
     var sql_old_tags = string.Join(",", (from t in oldTags select t.tag_id.ToString()).ToArray()); 

     // Step 1 - Retag, allowing duplicates for now 
     var sql = @" 
      UPDATE item_tags 
       SET [email protected] 
       WHERE tag_id IN (" + sql_old_tags + @"); 
      "; 

     // Step 2 - Delete the duplicates 
     sql += @" 
      DELETE t1 
       FROM item_tags t1, item_tags t2 
       WHERE t1.tag_id=t2.tag_id 
        AND t1.item_id=t2.item_id 
        AND t1.item_tag_id > t2.item_tag_id; 
      "; 

     // Step 3 - Update the use count of the destination tag 
     sql += @" 
      UPDATE tags 
       SET tags.count= 
        (
         SELECT COUNT(items.item_id) 
         FROM items 
         INNER JOIN item_tags ON item_tags.item_id = items.item_id 
         WHERE items.published=1 AND [email protected] 
        ) 
       WHERE 
        [email protected]; 
      "; 

     // Step 4 - Zero the use counts of the old tags and alias the old tag to the new tag 
     sql += @" 
      UPDATE tags 
       SET tags.count=0, 
        [email protected] 
       WHERE tag_id IN (" + sql_old_tags + @"); 
      "; 

     // Do it! 
     megDB.CodingHorror(sql, newTag.tag_id, newTag.tag_id, newTag.tag_id, newTag.tag_id).Execute(); 
     scope.Complete(); 
    } 
+0

bien votre première erreur primaire et est que vous ne le faites pas côté serveur - je n'ai pas analysé votre code plus loin que cela, donc je ne peux plus commenter. –

+0

Je ne suis pas certain de comprendre. Il y a quelques bits à l'avant qui sont faits dans le code - mais ils sont mineurs (obtenir l'identifiant d'un couple de tags). La plus grande partie du travail consiste en seulement 4 instructions SQL - elles ne sont pas en boucle ou quoi que ce soit. –

0

Un index/contrainte sur la table item_tag peut empêcher les étiquettes en double; ou créez la table avec une clé primaire composite en utilisant à la fois item_id et tag_id.

Quant aux comptes, laissez tomber la colonne count de la table tag et créer une vue pour obtenir les résultats:

CREATE VIEW tag_counts AS SELECT tag_id, name, COUNT(*) AS count GROUP BY tag_id, name

Ensuite, votre compte est toujours à jour.

+0

j'avais considéré cela pour maintenir les chefs d'accusation mais comment cela se produire sur un grand nombre d'articles? Étant donné que le rééchantillonnage par lots est rare et que l'on retrouve fréquemment les décomptes, une colonne dénombrée est, à mon avis, justifiée. –

+0

Si vous allez avoir des millions de tags avec des mises à jour peu fréquentes et des lectures fréquentes, alors oui, calculer le nombre à chaque fois n'est pas la meilleure solution. Je pensais juste que je le mentionnerais. Une alternative pourrait être une vue matérialisée, mais je ne suis pas sûr que cela vous aidera puisque vous devez toujours les rafraîchir. (Je ne sais pas non plus si MySQL les supporte, j'utilise principalement SQL Server) :) – Tony

1

si je vous ai bien compris, vous pouvez essayer quelque chose comme ceci:

/* new tag/item table clustered PK optimised for group by tag_id 
    or tag_id = ? queries !! */ 

drop table if exists tag_item; 
create table tag_item 
(
tag_id smallint unsigned not null, 
item_id int unsigned not null, 
primary key (tag_id, item_id), -- clustered PK innodb only 
key (item_id) 
) 
engine=innodb; 

-- populate new table with distinct tag/items 

insert ignore into tag_item 
select tag_id, item_id from item_tag order by tag_id, item_id; 

-- update counters 

update tag inner join 
(
select 
tag_id, 
count(*) as counter 
from 
tag_item 
group by 
tag_id 
) c on tag.tag_id = c.tag_id 
set 
tag.counter = c.counter; 
+0

Intéressant - serait-ce plus rapide qu'une simple mise à jour du tag avec un compte select (*)? (en supposant que les indices appropriés soient présents). –

+1

ma compréhension était ...en raison de l'absence d'intégrité de l'entité, à savoir un PK sur la table item_tag, qu'il avait des données en double (1,2), (1,3), (1,2) <- oops et aussi besoin d'une re-cartographie de certaines balises (que j'ai omis de mon exemple car je suis paresseux) donc un nouveau compte ne le coupera tout simplement pas. Si vous êtes préoccupé par mon choix de PK groupé pour tag_item, vous pouvez consulter ma réponse ici http://stackoverflow.com/questions/3534597/rewriting-mysql-select-to-reduce-time-and-writing- tmp-to-disk/3535735 # 3535735 –

Questions connexes