2012-03-30 3 views
7

Sto sviluppando un'applicazione che memorizza le immagini e i relativi metadati. Ho riscontrato problemi durante l'esecuzione di una determinata query utilizzando NHibernate. La query sta prendendo un tempo proibitivo (sulla mia macchina qualcosa come 31 secondi), anche se la stessa query richiede solo una frazione di secondo se eseguita in SQL Server Management Studio.La query richiede molto tempo nell'app client, ma è veloce in SQL Server Management Studio

Ho ridotto e extraced il problema ad una piccola applicazione di prova:

Entità:

Tag, composto da Id (stringa, il valore della variabile stessa)

public class Tag 
{ 
    public virtual string Id { get; set; } 
} 

Immagine, costituito da Id (int), Nome (stringa) e Tag (numero molti-a-molti, serie Tag)

public class Image 
{ 
    private Iesi.Collections.Generic.ISet<Tag> tags = new HashedSet<Tag>(); 

    public virtual int Id { get; set; } 

    public virtual string Name { get; set; } 

    public virtual IEnumerable<Tag> Tags 
    { 
     get { return tags; } 
    } 

    public virtual void AddTag(Tag tag) 
    { 
     tags.Add(tag); 
    } 
} 

sto usando "la mappatura dal codice" con le seguenti mappature:

public class TagMapping : ClassMapping<Tag> 
{ 
    public TagMapping() 
    { 
     Id(x => x.Id, map => map.Generator(Generators.Assigned)); 
    } 
} 

public class ImageMapping : ClassMapping<Image> 
{ 
    public ImageMapping() 
    { 
     Id(x => x.Id, map => map.Generator(Generators.Native)); 
     Property(x => x.Name); 
     Set(x => x.Tags, 
      map => map.Access(Accessor.Field), 
      map => map.ManyToMany(m2m => { })); 
    } 
} 

La configurazione di NHibernate/banca dati appare così:

<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2"> 
    <session-factory> 
     <property name="dialect">NHibernate.Dialect.MsSql2008Dialect</property> 
     <property name="connection.connection_string_name">PrimaryDatabase</property> 
     <property name="format_sql">true</property> 
    </session-factory> 
    </hibernate-configuration> 
    <connectionStrings> 
    <add name="PrimaryDatabase" providerName="System.Data.SqlClient" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=PerfTest;Integrated Security=True" /> 
    </connectionStrings> 

voglio raggiungere i seguenti query: dammi tutte le immagini in cui il nome contiene una stringa specifica o in cui qualsiasi tag contiene una stringa specifica. Per trovare quest'ultimo utilizzo una sottoquery che mi restituisce gli ID di tutte le immagini con tag corrispondenti. Quindi alla fine i criteri di ricerca sono: l'immagine ha un nome contenente una stringa specifica o il suo ID è uno di quelli restituiti dalla sottoquery.

Ecco il codice che esegue la query:

var term = "abc"; 
var mode = MatchMode.Anywhere; 

var imagesWithMatchingTag = QueryOver.Of<Image>() 
    .JoinQueryOver<Tag>(x => x.Tags) 
    .WhereRestrictionOn(x => x.Id).IsLike(term, mode) 
    .Select(x => x.Id); 

var qry = session.QueryOver<Image>() 
    .Where(Restrictions.On<Image>(x => x.Name).IsLike(term, mode) || 
      Subqueries.WhereProperty<Image>(x => x.Id).In(imagesWithMatchingTag)) 
    .List(); 

il database di test (DBMS: SQL Server 2008 Express R2) ho eseguito questo query è stato creato appositamente per questo test e non contiene niente altro. L'ho riempito con dati casuali: 10.000 immagini (tabella Image), 4.000 tag (tabella Tag) e circa 200.000 associazioni tra immagini e tag (tabella Tags), es. ogni immagine ha circa 20 tag associati. Il database

Lo SQL NHibernate afferma di utilizzare è:

SELECT 
    this_.Id as Id1_0_, 
    this_.Name as Name1_0_ 
FROM 
    Image this_ 
WHERE 
    (
     this_.Name like @p0 
     or this_.Id in (
      SELECT 
       this_0_.Id as y0_ 
      FROM 
       Image this_0_ 
      inner join 
       Tags tags3_ 
        on this_0_.Id=tags3_.image_key 
      inner join 
       Tag tag1_ 
        on tags3_.elt=tag1_.Id 
      WHERE 
       tag1_.Id like @p1 
     ) 
    ); 
@p0 = '%abc%' [Type: String (4000)], @p1 = '%abc%' [Type: String (4000)] 

Questo sembra ragionevole data la query che sto creando.

Se si esegue questa query utilizzando NHibernate, la query impiega circa 30+ secondi (NHibernate.AdoNet.AbstractBatcher - ExecuteReader took 32964 ms) e restituisce 98 entità.

Tuttavia, se eseguo una query equivalente direttamente all'interno dello studio di SQL Server Management:

DECLARE @p0 nvarchar(4000) 
DECLARE @p1 nvarchar(4000) 

SET @p0 = '%abc%' 
SET @p1 = '%abc%'  

SELECT 
    this_.Id as Id1_0_, 
    this_.Name as Name1_0_ 
FROM 
    Image this_ 
WHERE 
    (
     this_.Name like @p0 
     or this_.Id in (
      SELECT 
       this_0_.Id as y0_ 
      FROM 
       Image this_0_ 
      inner join 
       Tags tags3_ 
        on this_0_.Id=tags3_.image_key 
      inner join 
       Tag tag1_ 
        on tags3_.elt=tag1_.Id 
      WHERE 
       tag1_.Id like @p1 
     ) 
    ); 

La query richiede molto meno di un secondo (e restituisce 98 risultati pure).

Ulteriori esperimenti:

Se ho solo cercare attraverso il nome o solo con i tag, cioè .:

var qry = session.QueryOver<Image>() 
    .Where(Subqueries.WhereProperty<Image>(x => x.Id).In(imagesWithMatchingTag)) 
    .List(); 

o

var qry = session.QueryOver<Image>() 
    .Where(Restrictions.On<Image>(x => x.Name).IsLike(term, mode)) 
    .List(); 

le query sono veloci.

Se io non uso come ma una corrispondenza esatta nel mio subquery:

var imagesWithMatchingTag = QueryOver.Of<Image>() 
    .JoinQueryOver<Tag>(x => x.Tags) 
    .Where(x => x.Id == term) 
    .Select(x => x.Id); 

la query è veloce, troppo.

La modifica della modalità di corrispondenza per il nome su Esatta non modifica nulla.

Quando il debug del programma e mettere in pausa durante l'esecuzione della query nella parte superiore dello stack di chiamate gestite assomiglia:

[Managed to Native Transition] 
System.Data.dll!SNINativeMethodWrapper.SNIReadSync(System.Runtime.InteropServices.SafeHandle pConn, ref System.IntPtr packet, int timeout) + 0x53 bytes 
System.Data.dll!System.Data.SqlClient.TdsParserStateObject.ReadSni(System.Data.Common.DbAsyncResult asyncResult, System.Data.SqlClient.TdsParserStateObject stateObj) + 0xa3 bytes 
System.Data.dll!System.Data.SqlClient.TdsParserStateObject.ReadNetworkPacket() + 0x24 bytes 
System.Data.dll!System.Data.SqlClient.TdsParserStateObject.ReadBuffer() + 0x1f bytes  
System.Data.dll!System.Data.SqlClient.TdsParserStateObject.ReadByte() + 0x46 bytes 
System.Data.dll!System.Data.SqlClient.TdsParser.Run(System.Data.SqlClient.RunBehavior runBehavior, System.Data.SqlClient.SqlCommand cmdHandler, System.Data.SqlClient.SqlDataReader dataStream, System.Data.SqlClient.BulkCopySimpleResultSet bulkCopyHandler, System.Data.SqlClient.TdsParserStateObject stateObj) + 0x67 bytes  
System.Data.dll!System.Data.SqlClient.SqlDataReader.ConsumeMetaData() + 0x22 bytes 
System.Data.dll!System.Data.SqlClient.SqlDataReader.MetaData.get() + 0x57 bytes 
System.Data.dll!System.Data.SqlClient.SqlCommand.FinishExecuteReader(System.Data.SqlClient.SqlDataReader ds, System.Data.SqlClient.RunBehavior runBehavior, string resetOptionsString) + 0xe1 bytes 
... 

Quindi, le mie domande sono:

  • Perché la query in modo molto più tempo quando eseguito da NHibernate anche se l'SQL utilizzato è lo stesso?
  • Come posso eliminare la differenza? C'è un'impostazione che può causare questo comportamento?

So che la query in generale non è la cosa più efficiente del mondo, ma ciò che mi colpisce di più è la differenza tra l'utilizzo di NHibernate e l'interrogazione manuale. C'è definitivamente qualcosa di strano che succede qui.

Ci scusiamo per il post lungo, ma volevo includere il più possibile sul problema. Grazie mille in anticipo per il tuo aiuto!

Update 1: Ho testato l'applicazione con NHProf senza valore molto aggiunto: NHProf mostra che lo SQL eseguito è

SELECT this_.Id as Id1_0_, 
     this_.Name as Name1_0_ 
FROM Image this_ 
WHERE (this_.Name like '%abc%' /* @p0 */ 
     or this_.Id in (SELECT this_0_.Id as y0_ 
         FROM Image this_0_ 
           inner join Tags tags3_ 
            on this_0_.Id = tags3_.image_key 
           inner join Tag tag1_ 
            on tags3_.elt = tag1_.Id 
         WHERE tag1_.Id like '%abc%' /* @p1 */)) 

Il che è esattamente quello che ho postato prima (perché è quello che ha scritto NHibernate al suo registro in primo luogo).

Ecco uno screenshot di NHProf Screenshot of NHProf

Gli avvisi sono comprensibili, ma non spiegano il comportamento.

Update 2 @surfen sugested di tirare i risultati della query sub fuori del DB prima e attaccarli di nuovo nella query principale:

var imagesWithMatchingTag = QueryOver.Of<Image>() 
    .JoinQueryOver<Tag>(x => x.Tags) 
    .WhereRestrictionOn(x => x.Id).IsLike(term, mode) 
    .Select(x => x.Id); 

var ids = imagesWithMatchingTag.GetExecutableQueryOver(session).List<int>().ToArray(); 

var qry = session.QueryOver<Image>() 
    .Where(
      Restrictions.On<Image>(x => x.Name).IsLike(term, mode) || 
      Restrictions.On<Image>(x => x.Id).IsIn(ids)) 
    .List(); 

Anche se questo effettivamente rendere la query principale veloce di nuovo, preferirei non seguire questo approccio in quanto non si adatta bene all'uso previsto nell'applicazione del mondo reale. È interessante che questo sia molto più veloce, però. Mi aspetto che l'approccio delle subquery sia altrettanto veloce, dato che non dipende dalla query esterna.

Aggiornamento 3 Questo non sembra essere correlato a NHibernate. Se corro la query utilizzando normali oggetti ADO.NET ottengo lo stesso comportamento:

var cmdText = @"SELECT this_.Id as Id1_0_, 
         this_.Name as Name1_0_ 
       FROM Image this_ 
       WHERE (this_.Name like @p0 
          or this_.Id in 
         (SELECT this_0_.Id as y0_ 
         FROM Image this_0_ 
          inner join Tags tags3_ 
           on this_0_.Id = tags3_.image_key 
          inner join Tag tag1_ 
           on tags3_.elt = tag1_.Id 
         WHERE tag1_.Id like @p1));"; 

using (var con = new SqlConnection(ConfigurationManager.ConnectionStrings["PrimaryDatabase"].ConnectionString)) 
{ 
    con.Open(); 
    using (var txn = con.BeginTransaction()) 
    { 
     using (var cmd = new SqlCommand(cmdText, con, txn)) 
     { 
      cmd.CommandTimeout = 120; 
      cmd.Parameters.AddWithValue("p0", "%abc%"); 
      cmd.Parameters.AddWithValue("p1", "%abc%"); 

      using (var reader = cmd.ExecuteReader()) 
      { 
       while (reader.Read()) 
       { 
        Console.WriteLine("Match"); 
       } 
      } 

     } 
     txn.Commit(); 
    } 
} 

Update 4

query piani (clicca per ingrandire):

query lente Slow plan

Richiesta veloce Fast plan

C'è definitivamente una differenza nel piano.

Update 5

A quanto pare infatti che SQL Server considera la subquery come essere correlata ho provato qualcosa di diverso: ho spostato il criterio relativo al nome di una sottoquery per sé:

var term = "abc"; 
var mode = MatchMode.Anywhere; 

var imagesWithMatchingTag = QueryOver.Of<Image>() 
    .JoinQueryOver<Tag>(x => x.Tags) 
    .WhereRestrictionOn(x => x.Id).IsLike(term, mode) 
    .Select(x => x.Id); 

var imagesWithMatchingName = QueryOver.Of<Image>() 
    .WhereRestrictionOn(x => x.Name).IsLike(term, mode) 
    .Select(x => x.Id); 

var qry = session.QueryOver<Image>() 
    .Where(
     Subqueries.WhereProperty<Image>(x => x.Id).In(imagesWithMatchingName) ||   
     Subqueries.WhereProperty<Image>(x => x.Id).In(imagesWithMatchingTag) 
    ).List(); 

SQL generato:

SELECT 
    this_.Id as Id1_0_, 
    this_.Name as Name1_0_ 
FROM 
    Image this_ 
WHERE 
    (
     this_.Id in (
      SELECT 
       this_0_.Id as y0_ 
      FROM 
       Image this_0_ 
      inner join 
       Tags tags3_ 
        on this_0_.Id=tags3_.image_key 
      inner join 
       Tag tag1_ 
        on tags3_.elt=tag1_.Id 
      WHERE 
       tag1_.Id like @p0 
     ) 
     or this_.Id in (
      SELECT 
       this_0_.Id as y0_ 
      FROM 
       Image this_0_ 
      WHERE 
       this_0_.Name like @p1 
     ) 
    ); 
@p0 = '%abc%' [Type: String (4000)], @p1 = '%abc%' [Type: String (4000)] 

Questo sembra rompere la correlazione e di conseguenza il beco interrogazione mes "fast" di nuovo ("veloce" come in "accettabile per il momento"). Il tempo di interrogazione è passato da 30+ secondi a ~ 170 ms. Ancora non è una query leggera, ma almeno mi consentirà di continuare da qui. So che uno "like '%foo%'" non sarà mai super veloce. Se si verifica il peggio, posso comunque passare a un server di ricerca specializzato (Lucene, solr) o alla ricerca di testo completo.

Update 6 sono stato in grado di riscrivere la query non utilizzare per sottointerrogazioni affatto:

var qry = session.QueryOver(() => img) 
    .Left.JoinQueryOver(x => x.Tags,() => tag) 
    .Where(
     Restrictions.Like(Projections.Property(() => img.Name), term, mode) || 
     Restrictions.Like(Projections.Property(() => tag.Id), term, mode)) 
    .TransformUsing(Transformers.DistinctRootEntity) 
    .List(); 

SQL:

SELECT 
    this_.Id as Id1_1_, 
    this_.Name as Name1_1_, 
    tags3_.image_key as image1_3_, 
    tag1_.Id as elt3_, 
    tag1_.Id as Id0_0_ 
FROM 
    Image this_ 
left outer join 
    Tags tags3_ 
     on this_.Id=tags3_.image_key 
left outer join 
    Tag tag1_ 
     on tags3_.elt=tag1_.Id 
WHERE 
    (
     this_.Name like @p0 
     or tag1_.Id like @p1 
    ); 
@p0 = '%abc%' [Type: String (4000)], @p1 = '%abc%' [Type: String (4000)] 

Tuttavia, la query esegue ora un po 'peggio rispetto alla versione con sottoquery. Investigherò ulteriormente.

+0

Possibile duplicato: http://dba.stackexchange.com/q/9167/724 –

+0

@RowlandShaw non è un duplicato in quanto il problema potrebbe essere nel lato C# - che lo rende anche fuori portata per dba – Mark

+1

@AndreLoker - hai eseguito un profiler su questo per vedere dove è il tempo impiegato – Mark

risposta

2

La mia scommessa è che è la seconda query che è lento:

var qry = session.QueryOver<Image>() 
.Where(Restrictions.On<Image>(x => x.Name).IsLike(term, mode) || 
     Subqueries.WhereProperty<Image>(x => x.Id).In(imagesWithMatchingTag)) 
.List(); 

hai fornito SQL solo per la prima query. E il secondo? L'hai provato sotto SQL Management Studio? Usa SQL Server Profiler come @JoachimIsaksson suggerisce di scoprire quali query exibly NHibernate esegue sul lato server.

Sembra che tu stia caricando 97 image oggetti nella memoria. Quanto è grande ognuno di loro?

EDIT

Un'altra scommessa è che la vostra prima query viene eseguita query interna annuncio per la seconda query. Prova a fare .List() sulla prima query per caricare i tag in memoria.

EDIT 2

dalla query piani sembra proprio che la query viene chiamato come Correlated subquery. Lei ha detto che queste query sono veloci:

var qry = session.QueryOver<Image>() 
.Where(Subqueries.WhereProperty<Image>(x => x.Id).In(imagesWithMatchingTag)) 
.List(); 

o

var qry = session.QueryOver<Image>() 
.Where(Restrictions.On<Image>(x => x.Name).IsLike(term, mode)) 
.List(); 

Basta UNION e si dovrebbe ottenere lo stesso risultato di esecuzione sia di essi separatamente. Assicurarsi inoltre che tutte le colonne di join abbiano indici.

Questo è il trucco con IS IN (query) - non si può essere sicuri di come il database lo esegue (a meno che non si forza in qualche modo a utilizzare un determinato piano). Forse potresti cambiare .In() in JoinQueryOver() in qualche modo?

+0

C'è una sola query. 'imagesWithMatchingTag' è una sottoquery. L'SQL che ho postato è tutto ciò che viene eseguito. Se guardi la mia domanda, vedrai che gli oggetti 'Image' sono piuttosto piccoli: due campi primitivi e un hash impostato con al massimo 20 voci. –

+0

Vedo. Vedi le mie modifiche. Prova a fare .List() sulla prima query per vedere se aiuta. – surfen

+0

Non estrae la prima query dall'SQL e invece fa passare l'elenco di ID al e dal codice? Non sarà peggio? – Rup