Domanda

La casella su cui è in esecuzione questa query è un server dedicato in esecuzione in un datacenter.

AMD Opteron 1354 Quad-Core 2.20GHz 2 GB di RAM Windows Server 2008 x64 (Sì, lo so, ho solo 2 GB di RAM, sto eseguendo l'aggiornamento a 8 GB quando il progetto sarà attivo).

Quindi ho esaminato e creato 250.000 righe fittizie in una tabella per stressare davvero alcune query generate da LINQ to SQL e accertarmi che non siano terribili e ho notato che una di esse impiegava un tempo assurdo.

Ho avuto questa query fino a 17 secondi con gli indici ma li ho rimossi per far sì che questa risposta andasse dall'inizio alla fine. Solo gli indici sono chiavi primarie.

Stories table --
[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NOT NULL,
[CategoryID] [int] NOT NULL,
[VoteCount] [int] NOT NULL,
[CommentCount] [int] NOT NULL,
[Title] [nvarchar](96) NOT NULL,
[Description] [nvarchar](1024) NOT NULL,
[CreatedAt] [datetime] NOT NULL,
[UniqueName] [nvarchar](96) NOT NULL,
[Url] [nvarchar](512) NOT NULL,
[LastActivityAt] [datetime] NOT NULL,

Categories table --
[ID] [int] IDENTITY(1,1) NOT NULL,
[ShortName] [nvarchar](8) NOT NULL,
[Name] [nvarchar](64) NOT NULL,

Users table --
[ID] [int] IDENTITY(1,1) NOT NULL,
[Username] [nvarchar](32) NOT NULL,
[Password] [nvarchar](64) NOT NULL,
[Email] [nvarchar](320) NOT NULL,
[CreatedAt] [datetime] NOT NULL,
[LastActivityAt] [datetime] NOT NULL,

Attualmente nel database ci sono 1 utente, 1 categoria e 250.000 storie e ho provato a eseguire questa query.

SELECT TOP(10) *
FROM Stories
INNER JOIN Categories ON Categories.ID = Stories.CategoryID
INNER JOIN Users ON Users.ID = Stories.UserID
ORDER BY Stories.LastActivityAt

L'esecuzione della query richiede 52 secondi, l'utilizzo della CPU è del 2-3%, Membery è 1,1 GB, 900 MB gratuiti ma l'utilizzo del disco sembra fuori controllo. Sono @ 100 MB / sec con 2/3 di quelli scritti su tempdb.mdf e il resto sta leggendo da tempdb.mdf.

Ora per la parte interessante ...

SELECT TOP(10) *
FROM Stories
INNER JOIN Categories ON Categories.ID = Stories.CategoryID
INNER JOIN Users ON Users.ID = Stories.UserID

SELECT TOP(10) *
FROM Stories
INNER JOIN Users ON Users.ID = Stories.UserID
ORDER BY Stories.LastActivityAt

SELECT TOP(10) *
FROM Stories
INNER JOIN Categories ON Categories.ID = Stories.CategoryID
ORDER BY Stories.LastActivityAt

Tutte e 3 queste domande sono praticamente istantanee.

Piano Exec per la prima query.
http://i43.tinypic.com/xp6gi1.png

Piani Exec per altre 3 query (in ordine).
http://i43.tinypic.com/30124bp.png
http://i44.tinypic.com/13yjml1.png
http://i43.tinypic.com/33ue7fb.png

Qualsiasi aiuto sarebbe molto apprezzato.

Piano Exec dopo l'aggiunta di indici (di nuovo fino a 17 secondi).
http://i39.tinypic.com/2008ytx.png

Ho ricevuto molti feedback utili da tutti e ti ringrazio, ho provato un nuovo punto di vista. Interrogo le storie di cui ho bisogno, quindi in query separate ottengo le categorie e gli utenti e con 3 query ci sono voluti solo 250ms ... Non capisco il problema ma se funziona e a 250ms non meno per il momento attenersi a quello. Ecco il codice che ho usato per testarlo.

DBDataContext db = new DBDataContext();
Console.ReadLine();

Stopwatch sw = Stopwatch.StartNew();

var stories = db.Stories.OrderBy(s => s.LastActivityAt).Take(10).ToList();
var storyIDs = stories.Select(c => c.ID);
var categories = db.Categories.Where(c => storyIDs.Contains(c.ID)).ToList();
var users = db.Users.Where(u => storyIDs.Contains(u.ID)).ToList();

sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
È stato utile?

Soluzione

Prova ad aggiungere un indice su Stories.LastActivityAt. Penso che la scansione dell'indice cluster nel piano di esecuzione potrebbe essere dovuta all'ordinamento.

Modifica: Poiché la mia query è tornata in un istante con righe lunghe solo pochi byte, ma è già in esecuzione da 5 minuti e continua ancora dopo aver aggiunto un varchar 2K, penso che Mitch abbia ragione. È il volume di quei dati che viene mischiato per nulla, ma questo può essere risolto nella query.

Prova a mettere il join, l'ordinamento e la parte superiore (10) in una vista o in una query nidificata, quindi ricollegala alla tabella delle storie per ottenere il resto dei dati solo per le 10 righe necessarie.

In questo modo:

select * from 
(
    SELECT TOP(10) id, categoryID, userID
    FROM Stories
    ORDER BY Stories.LastActivityAt
) s
INNER JOIN Stories ON Stories.ID = s.id
INNER JOIN Categories ON Categories.ID = s.CategoryID
INNER JOIN Users ON Users.ID = s.UserID

Se hai un indice su LastActivityAt, questo dovrebbe funzionare molto velocemente.

Altri suggerimenti

Quindi, se leggo correttamente la prima parte, risponde in 17 secondi con un indice. Che è ancora un po 'di tempo per eliminare 10 record. Sto pensando che il tempo sia nell'ordine per clausola. Vorrei un indice su LastActivityAt, UserID, CategoryID. Solo per divertimento, rimuovi l'ordine e vedi se restituisce rapidamente i 10 record. Se lo fa, allora sai che non è nei join delle altre tabelle. Inoltre sarebbe utile sostituire * con le colonne necessarie poiché tutte e 3 le colonne della tabella si trovano nel tempdb durante l'ordinamento, come menzionato da Neil.

Osservando i piani di esecuzione noterai l'ordinamento aggiuntivo: credo che sia l'ordine con cui ci vorrà del tempo. Suppongo che tu avessi un indice con 3 ed era 17 secondi ... quindi potresti volere un indice per i criteri di join (userid, ID categoria) e un altro per lastactivityat - vedi se funziona meglio. Inoltre sarebbe bene eseguire la query tramite la procedura guidata di ottimizzazione dell'indice.

Il mio primo suggerimento è di rimuovere * e sostituirlo con le colonne minime necessarie.

secondo, c'è un trigger coinvolto? Qualcosa che aggiornerebbe il campo LastActivityAt?

In base alla query del problema, prova ad aggiungere un indice di combinazione nella tabella Stories (IDCategoria, IDutente, LastActivityAt)

Stai raggiungendo il massimo dei dischi nella tua configurazione hardware.

Dati i tuoi commenti sul posizionamento dei tuoi file Data / Log / tempDB, penso che qualsiasi quantità di ottimizzazione sarà un cerotto.

250.000 righe è piccola. Immagina quanto saranno gravi i tuoi problemi con 10 milioni di righe.

Suggerisco di spostare tempDB sulla propria unità fisica (preferibilmente un RAID 0).

Ok, quindi la mia macchina di prova non è veloce. In realtà è molto lento. 1.6 ghz, n 1 gb di ram, nessun disco multiplo, solo un singolo disco (lento) per server sql, sistema operativo ed extra.

Ho creato le tue tabelle con le chiavi primarie ed esterne definite. Inserite 2 categorie, 500 utenti casuali e 250000 storie casuali.

L'esecuzione della prima query sopra richiede 16 secondi (nessuna cache del piano). Se indicizzo la colonna LastActivityAt ottengo risultati in meno di un secondo (nessuna cache del piano neanche qui).

Ecco lo script che ho usato per fare tutto questo.

    --Categories table --
Create table Categories (
[ID] [int] IDENTITY(1,1) primary key NOT NULL,
[ShortName] [nvarchar](8) NOT NULL,
[Name] [nvarchar](64) NOT NULL)

--Users table --
Create table Users(
[ID] [int] IDENTITY(1,1) primary key NOT NULL,
[Username] [nvarchar](32) NOT NULL,
[Password] [nvarchar](64) NOT NULL,
[Email] [nvarchar](320) NOT NULL,
[CreatedAt] [datetime] NOT NULL,
[LastActivityAt] [datetime] NOT NULL
)
go

-- Stories table --
Create table Stories(
[ID] [int] IDENTITY(1,1) primary key NOT NULL,
[UserID] [int] NOT NULL references Users ,
[CategoryID] [int] NOT NULL references Categories,
[VoteCount] [int] NOT NULL,
[CommentCount] [int] NOT NULL,
[Title] [nvarchar](96) NOT NULL,
[Description] [nvarchar](1024) NOT NULL,
[CreatedAt] [datetime] NOT NULL,
[UniqueName] [nvarchar](96) NOT NULL,
[Url] [nvarchar](512) NOT NULL,
[LastActivityAt] [datetime] NOT NULL)

Insert into Categories (ShortName, Name) 
Values ('cat1', 'Test Category One')

Insert into Categories (ShortName, Name) 
Values ('cat2', 'Test Category Two')

--Dummy Users
Insert into Users
Select top 500
UserName=left(SO.name+SC.name, 32)
, Password=left(reverse(SC.name+SO.name), 64)
, Email=Left(SO.name, 128)+'@'+left(SC.name, 123)+'.com'
, CreatedAt='1899-12-31'
, LastActivityAt=GETDATE()
from sysobjects SO 
Inner Join syscolumns SC on SO.id=SC.id
go

--dummy stories!
-- A Count is given every 10000 record inserts (could be faster)
-- RBAR method!
set nocount on
Declare @count as bigint
Set @count = 0
begin transaction
while @count<=250000
begin
Insert into Stories
Select
  USERID=floor(((500 + 1) - 1) * RAND() + 1)
, CategoryID=floor(((2 + 1) - 1) * RAND() + 1)
, votecount=floor(((10 + 1) - 1) * RAND() + 1)
, commentcount=floor(((8 + 1) - 1) * RAND() + 1)
, Title=Cast(NEWID() as VARCHAR(36))+Cast(NEWID() as VARCHAR(36))
, Description=Cast(NEWID() as VARCHAR(36))+Cast(NEWID() as VARCHAR(36))+Cast(NEWID() as VARCHAR(36))
, CreatedAt='1899-12-31'
, UniqueName=Cast(NEWID() as VARCHAR(36))+Cast(NEWID() as VARCHAR(36)) 
, Url=Cast(NEWID() as VARCHAR(36))+Cast(NEWID() as VARCHAR(36))
, LastActivityAt=Dateadd(day, -floor(((600 + 1) - 1) * RAND() + 1), GETDATE())
If @count % 10000=0
Begin
Print @count
Commit
begin transaction
End
Set @count=@count+1
end 
set nocount off
go

--returns in 16 seconds
DBCC DROPCLEANBUFFERS
SELECT TOP(10) *
FROM Stories
INNER JOIN Categories ON Categories.ID = Stories.CategoryID
INNER JOIN Users ON Users.ID = Stories.UserID
ORDER BY Stories.LastActivityAt
go

--Now create an index
Create index IX_LastADate on Stories (LastActivityAt asc)
go
--With an index returns in less than a second
DBCC DROPCLEANBUFFERS
SELECT TOP(10) *
FROM Stories
INNER JOIN Categories ON Categories.ID = Stories.CategoryID
INNER JOIN Users ON Users.ID = Stories.UserID
ORDER BY Stories.LastActivityAt
go

L'ordinamento è sicuramente il luogo in cui si sta verificando il tuo rallentamento. L'ordinamento viene eseguito principalmente nel tempdb e una tabella di grandi dimensioni comporta l'aggiunta di LOTTI. Avere un indice su questa colonna migliorerà sicuramente le prestazioni di un ordine di.

Inoltre, la definizione di chiavi primarie ed esterne aiuta immensamente SQL Server

Il tuo metodo elencato nel tuo codice è elegante, e sostanzialmente la stessa risposta che cdonner ha scritto, tranne in c # e non in sql. Ottimizzare il db probabilmente darà risultati ancora migliori!

- Kris

Hai cancellato la cache di SQL Server prima di eseguire ciascuna query?

In SQL 2000, è qualcosa come DBCC DROPCLEANBUFFERS. Google il comando per ulteriori informazioni.

Guardando la query, avrei un indice per

Categories.ID Stories.CategoryID Users.ID Stories.UserID

e possibilmente Stories.LastActivityAt

Ma sì, sembra che il risultato potrebbe essere falso per la memorizzazione nella cache.

Dopo aver lavorato con SQL Server per un po 'di tempo, scoprirai che anche le più piccole modifiche a una query possono causare tempi di risposta molto diversi. Da quello che ho letto nella domanda iniziale e guardando il piano di query, sospetto che l'ottimizzatore abbia deciso che l'approccio migliore è quello di formare un risultato parziale e quindi ordinarlo come un passaggio separato. Il risultato parziale è un composto delle tabelle Utenti e Storie. Questo è formato in tempdb. Pertanto, l'eccessivo accesso al disco è dovuto alla formazione e all'ordinamento di questa tabella temporanea.

Concordo sul fatto che la soluzione dovrebbe essere quella di creare un indice composto su Stories.LastActivityAt, Stories.UserId, Stories.CategoryId. L'ordine è MOLTO importante, il campo LastActivityAt deve essere il primo.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top