Pregunta

El cuadro en el que se ejecuta esta consulta es un servidor dedicado que se ejecuta en un centro de datos.

AMD Opteron 1354 Quad-Core 2.20GHz 2 GB de RAM Windows Server 2008 x64 (Sí, sé que solo tengo 2 GB de RAM, estoy actualizando a 8 GB cuando el proyecto se pone en marcha).

Así que revisé y creé 250,000 filas ficticias en una tabla para probar realmente algunas consultas que LINQ to SQL genera y me aseguré de que no fueran tan terribles y noté que una de ellas estaba tomando una cantidad absurda de tiempo.

Tuve esta consulta hasta 17 segundos con índices, pero los eliminé en aras de esta respuesta para ir de principio a fin. Solo los índices son claves principales.

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,

Actualmente en la base de datos hay 1 usuario, 1 categoría y 250,000 historias e intenté ejecutar esta consulta.

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

La consulta tarda 52 segundos en ejecutarse, el uso de la CPU oscila entre el 2-3%, Membery tiene 1.1GB, 900MB gratis, pero el uso del disco parece estar fuera de control. Es @ 100MB / seg con 2/3 de eso escrito en tempdb.mdf y el resto es lectura de tempdb.mdf.

Ahora para la parte interesante ...

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

Las 3 consultas son casi instantáneas.

Plan de ejecución para la primera consulta.
http://i43.tinypic.com/xp6gi1.png

Exec planea otras 3 consultas (en orden).
http://i43.tinypic.com/30124bp.png
http://i44.tinypic.com/13yjml1.png
http://i43.tinypic.com/33ue7fb.png

Cualquier ayuda sería muy apreciada.

Plan de ejecución después de agregar índices (hasta 17 segundos nuevamente).
http://i39.tinypic.com/2008ytx.png

He recibido muchos comentarios útiles de todos y les agradezco, he intentado un nuevo ángulo en esto. Consulto las historias que necesito, luego en consultas separadas obtengo las Categorías y Usuarios y con 3 consultas solo tomó 250 ms ... No entiendo el problema, pero si funciona y a 250 ms no menos por el momento Quédate con eso. Aquí está el código que usé para probar esto.

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);
¿Fue útil?

Solución

Intente agregar un índice en Stories.LastActivityAt. Creo que la exploración de índice agrupado en el plan de ejecución puede deberse a la clasificación.

Editar: Como mi consulta regresó en un instante con filas de solo unos pocos bytes de longitud, pero ya se ha estado ejecutando durante 5 minutos y aún continúa después de que agregué un varchar 2K, creo que Mitch tiene un punto. Es el volumen de esos datos que se barajan para nada, pero esto se puede arreglar en la consulta.

Intente colocar la unión, la ordenación y la parte superior (10) en una vista o en una consulta anidada, y luego vuelva a unirse contra la tabla de la historia para obtener el resto de los datos solo para las 10 filas que necesita.

Me gusta esto:

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 tiene un índice en LastActivityAt, esto debería ejecutarse muy rápido.

Otros consejos

Entonces, si leo la primera parte correctamente, responde en 17 segundos con un índice. Que todavía es un tiempo para producir 10 discos. Estoy pensando que el tiempo está en el orden por cláusula. Quisiera un índice en LastActivityAt, UserID, CategoryID. Solo por diversión, elimine el pedido y vea si devuelve los 10 registros rápidamente. Si lo hace, entonces sabe que no está en las uniones a las otras tablas. También sería útil reemplazar el * con las columnas necesarias ya que las 3 columnas de la tabla están en el tempdb mientras está ordenando, como mencionó Neil.

Al observar los planes de ejecución, notará el tipo adicional: creo que ese es el orden por el cual tomará algún tiempo. Supongo que tenía un índice con el 3 y fueron 17 segundos ... por lo que es posible que desee un índice para los criterios de unión (userid, categoryID) y otro para lastactivityat - vea si eso funciona mejor. También sería bueno ejecutar la consulta a través del asistente de ajuste de índice.

Mi primera sugerencia es eliminar el * y reemplazarlo con las columnas mínimas que necesita.

segundo, ¿hay algún desencadenante involucrado? ¿Algo que actualizaría el campo LastActivityAt?

Según su consulta de problema, intente agregar un índice de combinación en la tabla Stories (CategoryID, UserID, LastActivityAt)

Está maximizando los discos en su configuración de hardware.

Dados sus comentarios sobre la ubicación de su archivo de datos / registro / tempDB, creo que cualquier cantidad de ajuste será una bandaid.

250,000 Filas es pequeño. Imagine lo graves que serán sus problemas con 10 millones de filas.

Le sugiero que mueva tempDB a su propia unidad física (preferiblemente un RAID 0).

Ok, entonces mi máquina de prueba no es rápida. En realidad es muy lento. Tiene 1.6 ghz, n 1 gb de ram, No hay discos múltiples, solo un disco (lectura lenta) para servidor sql, sistema operativo y extras.

Creé sus tablas con claves primarias y externas definidas. Se insertaron 2 categorías, 500 usuarios aleatorios y 250000 historias aleatorias.

Ejecutar la primera consulta anterior lleva 16 segundos (tampoco hay caché del plan). Si indexo la columna LastActivityAt obtengo resultados en menos de un segundo (tampoco hay caché del plan aquí).

Aquí está la secuencia de comandos que solía hacer todo esto.

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

El tipo es definitivamente donde está ocurriendo su desaceleración. La clasificación se realiza principalmente en tempdb y una tabla grande hará que se agregue MUCHO. Tener un índice en esta columna definitivamente mejorará el rendimiento en un pedido de.

Además, la definición de las claves principal y externa ayuda enormemente a SQL Server

Su método que figura en su código es elegante, y básicamente la misma respuesta que escribió cdonner, excepto en c # y no en sql. ¡Ajustar la base de datos probablemente dará mejores resultados!

--Kris

¿Ha borrado el caché de SQL Server antes de ejecutar cada una de las consultas?

En SQL 2000, es algo así como DBCC DROPCLEANBUFFERS. Google el comando para más información.

Mirando la consulta, tendría un índice para

Categorías.ID Stories.CategoryID Users.ID Stories.UserID

y posiblemente Stories.LastActivityAt

Pero sí, parece que el resultado podría ser falso porque el almacenamiento en caché.

Cuando haya trabajado con SQL Server durante algún tiempo, descubrirá que incluso los cambios más pequeños en una consulta pueden causar tiempos de respuesta muy diferentes. Por lo que leí en la pregunta inicial, y mirando el plan de consulta, sospecho que el optimizador ha decidido que el mejor enfoque es formar un resultado parcial y luego ordenarlo como un paso separado. El resultado parcial es un compuesto de las tablas Usuarios e Historias. Esto se forma en tempdb. Por lo tanto, el acceso excesivo al disco se debe a la formación y clasificación de esta tabla temporal.

Estoy de acuerdo en que la solución debería ser crear un índice compuesto en Stories.LastActivityAt, Stories.UserId, Stories.CategoryId. El orden es MUY importante, el campo LastActivityAt debe ser el primero.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top