En la mayor parte de los proyectos de consultoría en los que he trabajado, me he encontrado que los predicados de las sentencias están mal escritos, o dicho de otra forma no son óptimos.
¿Cuál es el predicado de una sentencia?, es aquel que limita las filas que va a devolver la sentencia. Para entender mejor este concepto vamos a analizar unos queries sobre la base de datos AdventureWorks.
SELECT ProductId, Name, ListPrice
FROM Production.Product
WHERE Color = 'White'
En este query vamos a obtener los productos que son de color blanco. Si revisamos un poco su plan de ejecución (Ctrl + L) y colocamos el mouse encima de la operación "Clustered Index Scan" vamos a ver algo como esto:
Si te fijas, el predicado es la parte del WHERE.
Ahora vamos a analizar otro query, uno que nos devuelva todos los productos que pertenezcan a la categoría 2 (Components).
SELECT P.ProductID, P.Name, P.ListPrice
FROM Production.Product P
INNER JOIN Production.ProductSubcategory PS
ON P.ProductSubcategoryID = PS.ProductSubcategoryID
WHERE PS.ProductCategoryID = 2
Vamos a obtener su plan de ejecución y si colocamos el mouse encima de la operación "Clustered Index Scan" sobre el objeto ProductSubcategory.PK_ProductSubcategoryID veremos algo como esto:
El predicado de nueva cuenta es la parte del WHERE.
Una vez que hemos entendido este punto, es importante notar que un predicado mal escrito puede llevarnos a no utilizar los índices que se hayan creado para la optimización de las consultas, es decir que nuestros predicados no sean SARG (Search Argument).
¿Cómo reconocer cuando un predicado contiene non-SARG?, es sencillo, cuando una operación se lleva a cabo sobre las columnas de nuestras tablas, ese predicado no será óptimo.
Por ejemplo, supongamos que queremos obtener todos los productos a los que realizando un 10% de descuento vayan a tener un precio menor a 100 y mayor a 0. El query normal que escribiríamos sería el siguiente:
SELECT ProductId, Name, ListPrice
FROM Production.Product
WHERE ListPrice * 0.9 < 100
AND ListPrice > 0
Si revisamos su plan de ejecución (costo 0.0127757) y colocamos el mouse encima de la operación "Clustered Index Scan" veremos que el siguiente predicado:
Notemos que el predicado está llevando a cabo una conversión de la columna Production.Product.ListPrice a un tipo de dato NUMERIC(19, 4).
Si es un query que se va a estar utilizando mucho entonces lo lógico sería crear un índice que nos permita obtener la información de forma mucho más rápida:
CREATE NONCLUSTERED INDEX ixPProduct_ListPrice
ON Production.Product (ListPrice)
INCLUDE (ProductId, Name)
En este query sencillo SQL Server podrá utilizar el índice pero nuestra sentencia no está totalmente optimizada. Veamos de nuevo el plan de ejecución (costo 0.0053521) y observemos el predicado:
Ahora la operación que se está realizando es un "Index Seek" que es mucho mejor que un "Clustered Index Scan", pero la conversión que se está realizando sobre la columna Production.Product.ListPrice le está afectando a nuestro query. Si recordamos nuestras clases de matemáticas veremos que es lo mismo:
ListPrice * 0.9 < 100
que
ListPrice < (100 / 0.9)
La diferencia en SQL radicará que la operación ya no se va a realizar sobre la columna, sino le vamos a dar un valor fijo (111.11).
Modifiquemos nuestro query para que quede de la siguiente manera:
SELECT ProductId, Name, ListPrice
FROM Production.Product
WHERE ListPrice < 111.11
AND ListPrice > 0
Si vemos el plan de ejecución (costo 0.0051023) y revisamos el predicado, veremos que la operación ya no se está realizando sobre cada fila de nuestro índice, mejora que se ve reflejada en el costo del plan de ejecución.
La mejora no es brutal, pero si estuviéramos hablando de grandes cantidades de información, seguro sería significativa dado que dejaríamos de utilizar recursos del servidor para atender esta sentencia.
Ahora veamos un ejemplo más interesante, supongamos que nos están pidiendo los productos que hayan sido vendidos en la primera semana del año 2003. Con la función DATEPART podemos obtener el número de semana de una fecha pasándole como primer parámetro WEEK, y con la función YEAR podemos obtener el año de una fecha. Nuestro query pudiera ser escrito de la siguiente manera:
SELECT P.ProductID, P.Name, SOD.UnitPrice
FROM Production.Product P
INNER JOIN Sales.SalesOrderDetail SOD
ON P.ProductID = SOD.ProductID
INNER JOIN Sales.SalesOrderHeader SOH
ON SOD.SalesOrderID = SOH.SalesOrderID
WHERE DATEPART(WEEK, SOH.OrderDate) = 1
AND YEAR(SOH.OrderDate) = 2003
Si vemos su plan de ejecución podremos saber que el costo es de 1.38725 y que se está haciendo un barrido de la tabla Sales.SalesOrderHeader.
Vamos a crear un índice en la columna OrderDate de la tabla Sales.SalesOrderHeader para ayudar a que nuestra consulta sea más rápida:
CREATE NONCLUSTERED INDEX ixSSalesOrderHeader_OrderDate
ON Sales.SalesOrderHeader(OrderDate)
Si volvemos a obtener el plan de ejecución de nuestro query (costo 0.91095) veremos que la operación del barrido de la tabla Sales.SalesOrderHeader ha cambiado por un barrido del índice que acabamos de crear.
Una operación Scan sobre un índice no es algo que queramos para nuestros planes de ejecución dado que no se estaría explotando al máximo la funcionalidad de los índices.
No se está realizando un uso óptimo del índice debido a que nuestro criterio de búsqueda no es SARG, estamos llevando a cabo una operación fila a fila sobre la columna OrderDate de nuestra tabla. ¿Cómo podemos corregirlo?, usando las fechas específicas que cubran el rango de la siguiente manera:
SELECT P.ProductID, P.Name, SOD.UnitPrice
FROM Production.Product P
INNER JOIN Sales.SalesOrderDetail SOD
ON P.ProductID = SOD.ProductID
INNER JOIN Sales.SalesOrderHeader SOH
ON SOD.SalesOrderID = SOH.SalesOrderID
WHERE OrderDate >= '20030101'
AND OrderDate <= '20030104'
Si volvemos a revisar el plan de ejecución (costo 0.362978) veremos que el barrido del índice ha cambiado por una búsqueda, es decir un Index Seek, mejora que se ha visto reflejada en el costo de nuestra sentencia:
Como conclusión, cuando no utilizamos SARG no estaremos utilizando las estructuras optimizadas para nuestras consultas o bien le estaremos dando trabajo de más a SQL para poder despachar el resultado. Hay que evitar en lo posible las operaciones sobre las columnas de la base de datos durante las búsquedas.
Espero esta entrada te sea de mucha utilidad y ayude a que mejores tus consultas.