2011-01-21 3 views
0

Supposons que j'implémente des articles avec des balises d'article. J'utilise SQL Server 2008.Requête Many-t-Many la plus efficace

TABLE Articles 
ArtID INT 
... 

TABLE Tags 
TagID INT 
TagText VARCHAR(10) 

TABLE ArticleTags 
ArtID INT 
TagID INT 

J'essaie de trouver la façon la plus efficace d'interroger tous les articles avec un balises spécifiques. Voici deux options, les deux que j'ai lues sont les plus efficaces.

Méthode A:

SELECT a.* FROM Articles 
WHERE EXISTS (
    SELECT * FROM ArticleTags at 
    INNER JOIN Tags t ON at.TagID = t.TagID 
    WHERE at.ArtID = a.ID 
    AND t.TagText IN ('abc', 'def') 
) 

Méthode B:

SELECT a.* FROM Articles a 
INNER JOIN ArticleTags at ON a.ArtID = at.ArtID 
INNER JOIN Tags t ON at.TagID = t.TagID 
WHERE t.TagText IN ('abc', 'def') 
GROUP BY a.ArtID 

Peut-experts SQL suggèrent qui est plus efficace et pourquoi? Ou peut-être que je suis sur la mauvaise voie.

+0

Jetez un oeil aux plans de requête pour les deux. Cela devrait vous dire quel est le meilleur pour votre situation particulière. – Oded

+0

Avez-vous configuré la recherche en texte intégral sur tagText? – Patrick

+0

Vous devriez aller à cette question SO, http://stackoverflow.com/questions/1200295/sql-join-vs-in-performance, et jetez un oeil à la réponse @Quassnoi.Et puis vous pouvez aller au lien qu'il a fourni pour une explication très complète et détaillée et des exemples de ce comportement.http: //explainextended.com/2009/06/16/in-vs-join-vs-exists/ – Lamak

Répondre

2

Comme presque toutes les questions de performance SQL, la réponse est la requête, la réponse est le schéma de données. Quels index avez-vous, c'est ce qui détermine les performances de vos requêtes.

Généralement les relations plusieurs-à-plusieurs nécessitent deux index complémentaires, l'un (ID1, ID2) et l'autre (ID2, ID1). L'un d'eux est groupé, peu importe lequel. permet donc de créer un DB de test (100k articles, 1K étiquettes, 1-10 étiquettes par article):

:setvar dbname testdb 
:setvar articles 1000000 
:setvar tags 1000 
:setvar articletags 10 
:on error exit 

set xact_abort on; 
go 

use master; 
go 

if db_id('$(dbname)') is not null 
begin 
    alter database [$(dbname)] set single_user with rollback immediate; 
    drop database [$(dbname)]; 
end 
go 

create database [$(dbname)]; 
go 

use [$(dbname)]; 
go 

create TABLE Articles (
    ArtID INT not null identity(1,1), 
    name varchar(100) not null, 
    filler char(500) not null default replicate('X', 500), 
    constraint pk_Articles primary key clustered (ArtID)); 
go 

create table Tags (
    TagID INT not null identity(1,1), 
    TagText VARCHAR(10) not null, 
    constraint pk_Tags primary key clustered (TagID), 
    constraint unq_Tags_Text unique (TagText)); 
go 

create TABLE ArticleTags (
    ArtID INT not null, 
    TagID INT not null, 
    constraint fk_Articles 
     foreign key (ArtID) 
     references Articles (ArtID), 
    constraint fk_Tags 
     foreign key (TagID) 
     references Tags (TagID), 
    constraint pk_ArticleTags 
     primary key clustered (ArtID, TagID)); 
go 

create nonclustered index ndxArticleTags_TagID 
    on ArticleTags (TagID, ArtID); 
go   

-- populate articles 
set nocount on; 
declare @i int =0, @name varchar(100); 
begin transaction 
while @i < $(articles) 
begin 
    set @name = 'Name ' + cast(@i as varchar(10)); 
    insert into Articles (name) values (@name); 
    set @i += 1; 
    if @i %1000 = 0 
    begin 
     commit; 
     raiserror (N'Inserted %d articles', 0, 1, @i); 
     begin transaction; 
    end 
end 
commit 
go 


-- populate tags 
set nocount on; 
declare @i int =0, @text varchar(100); 
begin transaction 
while @i < $(tags) 
begin 
    set @text = 'Tag ' + cast(@i as varchar(10)); 
    insert into Tags (TagText) values (@text); 
    set @i += 1; 
    if @i %1000 = 0 
    begin 
     commit; 
     raiserror (N'Inserted %d tags', 0, 1, @i); 
     begin transaction; 
    end 
end 
commit 
go 

-- populate article-tags 
set nocount on; 
declare @i int =0, @a int = 1, @cnt int, @tag int; 
set @cnt = rand() * $(articletags) + 1; 
set @tag = rand() * $(tags) + 1; 
begin transaction 
while @a < $(articles) 
begin 
    insert into ArticleTags (ArtID, TagID) values (@a, @tag); 
    set @cnt -= 1; 
    set @tag += rand()*10+1; 
    if $(tags)<[email protected] 
    begin 
     set @tag = 1; 
    end 
    if @cnt = 0 
    begin 
     set @cnt = rand() * $(articletags) + 1; 
     set @tag = rand() * $(tags) + 1; 
     set @a += 1; 
    end 
    set @i += 1; 
    if @i %1000 = 0 
    begin 
     commit; 
     raiserror (N'Inserted %d article-tags', 0, 1, @i); 
     begin transaction; 
    end 
end 
commit 
raiserror (N'Final: %d article-tags', 0, 1, @i); 
go 

Maintenant nous allons comparer les deux requêtes:

set statistics io on; 
set statistics time on; 

select a.ArtID 
from Articles a 
where exists (
    select * 
    from ArticleTags at 
    join Tags t on at.TagID = t.TagID 
    where at.ArtID = a.ArtID 
    and t.TagText in ('Tag 10', 'Tag 12')); 

SELECT a.ArtID FROM Articles a 
INNER JOIN ArticleTags at ON a.ArtID = at.ArtID 
INNER JOIN Tags t ON at.TagID = t.TagID 
WHERE t.TagText IN ('Tag 10', 'Tag 12') 
GROUP BY a.ArtID  

Résultat:

Table 'Articles'. Scan count 0, logical reads 3561, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'ArticleTags'. Scan count 2, logical reads 13, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'Tags'. Scan count 2, logical reads 4, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 

Table 'Articles'. Scan count 0, logical reads 3561, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'ArticleTags'. Scan count 2, logical reads 13, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'Tags'. Scan count 2, logical reads 4, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 

Surprise! (Eh bien pas vraiment). Ils sont IDENTIQUE. En fait, ils ont exactement le même plan d'exécution.

+0

Merci Remus. Une question: Quel SGBD avez-vous utilisé ci-dessus? –

+0

SQL Server 2008 R2 sur mon ordinateur portable –

+0

Je suppose que la raison de la question peut être à cause de la [SQLCMD] (http://www.simple-talk.com/sql/sql-tools/the-sqlcmd-workbench /) syntaxe? –

0

En bref: pas de différence. Les deux seront traduits dans le même plan d'exécution.

Modifier: n'a pas remarqué le GROUP BY. De cette façon, la requête ne sera probablement pas compilée. Supprimez la clause GROUP BY ou listez tous les champs de la table comme GROUP BY Id, Name, ...

+0

GROUP BY va compiler _if_ ArtID définit une ligne unique. – ThomasMcLeod

+3

@ThomasMcLeod Seulement dans MySQL pas SQL Server! –

+0

@Martin: bon point, +1 – ThomasMcLeod

1

Votre méthode B a une clause GROUP BY, mais vous renvoyez toutes les colonnes des articles, même probablement des colonnes non agrégeables. Cela jetterait une erreur. Le GROUP BY est probablement inutile.

Sans le GROUP BY, les requêtes ont à peu près le même plan d'exécution. Toutefois, la méthode B est une instruction de requête SQL plus standard.

Edit: DISTINCT est généralement préférable à un GROUP BY dans ce cas, et a la même fonction

SELECT DISTINCT 
    a.* 
FROM 
    Articles a 
INNER JOIN 
    ArticleTags at 
ON 
    a.ArtID = at.ArtID 
INNER JOIN 
    Tags t 
ON 
    at.TagID = t.TagID 
WHERE 
    t.TagText IN ('abc', 'def') 
+0

Sans le GROUP BY, il retournerait le même article plusieurs fois si l'article contenait plus d'une étiquette recherchée. Ce ne serait pas le résultat que je veux et monopoliserait aussi la bande passante. –

+0

Je vois, c'est bien aussi longtemps que vous pouvez vous assurer que ArtID est unique. Je mettrais une contrainte PRIMARY KEY dessus. – ThomasMcLeod

+0

ArtID serait certainement le PK. –

1

Je créerais une vue indexée sur la base des 3 tables sur les colonnes artid et tagText. De cette façon, vous pouvez utiliser:

SELECT * 
FROM Articles 
WHERE artID IN 
(SELECT artID 
FROM ArticleTagTextView 
WHERE TagText IN ('abc', 'def')) 
Questions connexes