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
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
Richiesta veloce
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.
Possibile duplicato: http://dba.stackexchange.com/q/9167/724 –
@RowlandShaw non è un duplicato in quanto il problema potrebbe essere nel lato C# - che lo rende anche fuori portata per dba – Mark
@AndreLoker - hai eseguito un profiler su questo per vedere dove è il tempo impiegato – Mark