Question

La boîte sur laquelle cette requête est exécutée est un serveur dédié exécuté dans un centre de données.

AMD Opteron 1354 Quad-Core 2.20GHz 2 Go de RAM Windows Server 2008 x64 (Oui, je sais que je ne dispose que de 2 Go de RAM; je passe à 8 Go lorsque le projet est mis en production).

J'ai donc créé 250 000 lignes fictives dans un tableau pour tester certaines requêtes générées par LINQ to SQL et vous assurer qu'elles ne sont pas terribles. J'ai remarqué que l'une d'elles prenait un temps absurde. <

J'ai eu cette requête jusqu'à 17 secondes avec des index, mais je les ai supprimés pour que cette réponse puisse aller du début à la fin. Seuls les index sont des clés primaires.

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,

Actuellement dans la base de données, il y a 1 utilisateur, 1 catégorie et 250 000 histoires et j'ai essayé de lancer cette requête.

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'exécution de la requête prend 52 secondes, l'utilisation du processeur se situe à 2-3%, Membery est de 1,1 Go, 900 Mo d'espace libre, mais l'utilisation du disque semble hors de contrôle. C'est @ 100Mo / sec avec les 2/3 de cela écrit dans tempdb.mdf et le reste lit dans tempdb.mdf.

Maintenant pour la partie intéressante ...

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

Ces trois requêtes sont quasi instantanées.

Plan d'exécution pour la première requête.
http://i43.tinypic.com/xp6gi1.png

Exec prévoit 3 autres requêtes (dans l’ordre).
http://i43.tinypic.com/30124bp.png
http://i44.tinypic.com/13yjml1.png
http://i43.tinypic.com/33ue7fb.png

Toute aide serait très appréciée.

Plan après l’ajout d’index (17 secondes à nouveau).
http://i39.tinypic.com/2008ytx.png

J'ai reçu beaucoup de commentaires utiles de tous et je vous remercie, j'ai essayé un nouvel angle à cela. J'interroge les histoires dont j'ai besoin, puis dans différentes requêtes, j'obtiens les catégories et les utilisateurs et avec 3 requêtes, cela ne prend que 250 ms ... Je ne comprends pas le problème, mais si cela fonctionne et à 250 ms, rien de moins pour le moment, je vais s'en tenir à cela. Voici le code que j'ai utilisé pour tester cela.

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);
Était-ce utile?

La solution

Essayez d’ajouter un index sur Stories.LastActivityAt. Je pense que l'analyse d'index en cluster dans le plan d'exécution peut être due au tri.

Modifier: Depuis que ma requête est revenue en un instant avec des lignes longues de quelques octets seulement, mais elle fonctionne déjà depuis 5 minutes et continue après que j'ai ajouté une variable de 2 Ko, je pense que Mitch a un point. C'est le volume de ces données qui est brassé pour rien, mais cela peut être corrigé dans la requête.

Essayez de placer la jointure, le tri et le haut (10) dans une vue ou dans une requête imbriquée, puis de rejoindre la table d’histoire pour obtenir le reste des données uniquement pour les 10 lignes dont vous avez besoin.

Comme ceci:

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

Si vous avez un index sur LastActivityAt, celui-ci devrait s'exécuter très rapidement.

Autres conseils

Donc, si je lis correctement la première partie, elle répond en 17 secondes avec un index. Ce qui est encore un temps pour chug out 10 disques. Je pense que l'heure est à l'ordre par article. Je voudrais un index sur LastActivityAt, UserID, CategoryID. Juste pour le plaisir, supprimez la commande et voyez si elle renvoie rapidement les 10 enregistrements. Si c'est le cas, alors vous savez que ce n'est pas dans les jointures aux autres tables. En outre, il serait utile de remplacer le * par les colonnes nécessaires car les 3 colonnes du tableau sont dans la base de données tempdb en cours de tri, comme l'a mentionné Neil.

En regardant les plans d'exécution, vous remarquerez le type supplémentaire - je crois que c'est l'ordre qui va prendre du temps. Je suppose que vous aviez un index avec le 3 et que le temps était de 17 secondes ... vous voudrez peut-être un index pour les critères de jointure (userid, categoryID) et un autre pour la dernière activité - voyez si cela donne de meilleurs résultats. En outre, il serait bon d'exécuter la requête via l'assistant de réglage d'index.

Ma première suggestion est de supprimer le * et de le remplacer par le minimum de colonnes dont vous avez besoin.

seconde, y a-t-il un déclencheur impliqué? Quelque chose qui mettrait à jour le champ LastActivityAt?

En fonction de votre requête, essayez d'ajouter un index de combinaison à la table Stories (CategoryID, UserID, LastActivityAt)

.

Vous maximisez le nombre de disques dans la configuration de votre matériel.

Étant donné vos commentaires sur l'emplacement de votre fichier de données / journal / tempDB, je pense que tout réglage sera un bandaid.

250 000 lignes est petite. Imaginez la gravité de vos problèmes avec 10 millions de lignes.

Je vous suggère de déplacer tempDB sur son propre disque physique (de préférence un RAID 0).

Bien, ma machine d’essai n’est pas rapide. En fait, c'est vraiment lent. Il 1,6 GHz, n 1 Go de RAM, pas de disques multiples, juste un seul disque (lecture lente) pour le serveur SQL, os, et extras.

J'ai créé vos tables avec des clés primaires et étrangères définies. Inséré 2 catégories, 500 utilisateurs aléatoires et 250000 histoires aléatoires.

L'exécution de la première requête ci-dessus prend 16 secondes (pas de cache de plan non plus). Si j'indexe la colonne LastActivityAt, les résultats sont affichés en moins d'une seconde (pas de cache de plan ici non plus).

Voici le script que j'ai utilisé pour faire tout cela.

    --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

Le type est certainement le lieu où votre ralentissement se produit. Le tri s'effectue principalement dans la base de données tempdb et une grande table entraînera l'ajout de LOTS. Avoir un index sur cette colonne va certainement améliorer les performances sur une commande de.

De plus, la définition de vos clés primaires et étrangères aide SQL Server à se mettre à l'aise

Votre méthode répertoriée dans votre code est élégante et correspond en gros à la même réponse que cdonner a écrit, sauf en c # et non en sql. Le réglage de la base de données donnera probablement de meilleurs résultats!

- Kris

Avez-vous vidé le cache de SQL Server avant d'exécuter chacune des requêtes?

Dans SQL 2000, cela ressemble à DBCC DROPCLEANBUFFERS. Google la commande pour plus d'informations.

En regardant la requête, j'aurais un index pour

Catégories.ID Histoires.CatégorieID Users.ID Stories.UserID

et éventuellement Stories.LastActivityAt

Mais oui, on dirait que le résultat pourrait être un faux problème de mise en cache.

Après avoir travaillé avec SQL Server pendant un certain temps, vous découvrirez que même les plus petites modifications apportées à une requête peuvent entraîner des temps de réponse extrêmement différents. D'après ce que j'ai lu dans la question initiale et en examinant le plan de requête, je suppose que l'optimiseur a décidé que la meilleure approche consiste à former un résultat partiel, puis à le trier séparément. Le résultat partiel est un composite des tables Users et Stories. Ceci est formé dans tempdb. L’accès excessif au disque est donc dû à la formation puis au tri de cette table temporaire.

Je conviens que la solution devrait consister à créer un index composé sur Stories.LastActivityAt, Stories.UserId, Stories.CategoryId. L'ordre est TRES important, le champ LastActivityAt doit être le premier.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top