Este es uno de esos debates religiosos/políticos que ha durado años:¿debo usar procedimientos almacenados o debo incluir consultas ad hoc en mi aplicación? Siempre he sido partidario de los procedimientos almacenados, por varias razones:
También veo que mucha gente está abandonando los procedimientos almacenados en favor de los ORM. Para aplicaciones simples, esto probablemente funcionará bien, pero a medida que su aplicación se vuelve más compleja, es probable que descubra que su ORM de elección es simplemente incapaz de realizar ciertos patrones de consulta, lo que *lo obliga* a usar un procedimiento almacenado. Si admite procedimientos almacenados, eso es.
Aunque todavía encuentro todos estos argumentos bastante convincentes, no es de lo que quiero hablar hoy; Quiero hablar sobre el rendimiento.
Muchos argumentos simplemente dirán, "¡los procedimientos almacenados funcionan mejor!" Eso puede haber sido marginalmente cierto en algún momento, pero desde que SQL Server agregó la capacidad de compilar a nivel de declaración en lugar de a nivel de objeto, y ha adquirido una funcionalidad poderosa como optimize for ad hoc workloads
, esto ya no es un argumento muy fuerte. El ajuste de índices y los patrones de consulta sensibles tienen un impacto mucho mayor en el rendimiento que el hecho de elegir utilizar un procedimiento almacenado; en las versiones modernas, dudo que encuentre muchos casos en los que exactamente la misma consulta muestre diferencias de rendimiento notables, a menos que también esté introduciendo otras variables (como ejecutar un procedimiento localmente frente a una aplicación en un centro de datos diferente en un continente diferente).
Dicho esto, hay un aspecto de rendimiento que a menudo se pasa por alto cuando se trata de consultas ad hoc:el caché del plan. Podemos usar optimize for ad hoc workloads
para evitar que los planes de un solo uso llenen nuestro caché (Kimberly Tripp (@KimberlyLTripp) de SQLskills.com tiene información excelente sobre esto aquí), y eso afecta los planes de un solo uso independientemente de si las consultas se ejecutan desde dentro de un procedimiento almacenado o se ejecutan ad hoc. Un impacto diferente que quizás no note, independientemente de esta configuración, es cuando idénticas los planes ocupan múltiples ranuras en el caché debido a las diferencias en SET
opciones o deltas menores en el texto de consulta real. Todo el fenómeno "lento en la aplicación, rápido en SSMS" ha ayudado a muchas personas a resolver problemas relacionados con configuraciones como SET ARITHABORT
. Hoy quería hablar sobre las diferencias del texto de consulta y demostrar algo que sorprende a la gente cada vez que lo menciono.
Caché para grabar
Digamos que tenemos un sistema muy simple que ejecuta AdventureWorks2012. Y solo para demostrar que no ayuda, hemos habilitado optimize for ad hoc workloads
:
EXEC sp_configure 'show advanced options', 1; GO RECONFIGURE WITH OVERRIDE; GO EXEC sp_configure 'optimize for ad hoc workloads', 1; GO RECONFIGURE WITH OVERRIDE;
Y luego libera el caché del plan:
DBCC FREEPROCCACHE;
Ahora generamos algunas variaciones simples a una consulta que, por lo demás, es idéntica. Estas variaciones pueden representar potencialmente estilos de codificación para dos desarrolladores diferentes:ligeras diferencias en espacios en blanco, mayúsculas/minúsculas, etc.
SELECT TOP (1) SalesOrderID, OrderDate, SubTotal FROM Sales.SalesOrderHeader WHERE SalesOrderID >= 75120 ORDER BY OrderDate DESC; GO -- change >= 75120 to > 75119 (same logic since it's an INT) GO SELECT TOP (1) SalesOrderID, OrderDate, SubTotal FROM Sales.SalesOrderHeader WHERE SalesOrderID > 75119 ORDER BY OrderDate DESC; GO -- change the query to all lower case GO select top (1) salesorderid, orderdate, subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO -- remove the parentheses around the argument for top GO select top 1 salesorderid, orderdate, subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO -- add a space after top 1 GO select top 1 salesorderid, orderdate, subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO -- remove the spaces between the commas GO select top 1 salesorderid,orderdate,subtotal from sales.salesorderheader where salesorderid > 75119 order by orderdate desc; GO
Si ejecutamos ese lote una vez y luego verificamos el caché del plan, vemos que tenemos 6 copias, esencialmente, exactamente del mismo plan de ejecución. Esto se debe a que el texto de la consulta tiene un hash binario, lo que significa que las mayúsculas y minúsculas y los espacios en blanco marcan la diferencia y pueden hacer que consultas idénticas parezcan exclusivas de SQL Server.
SELECT [text], size_in_bytes, usecounts, cacheobjtype FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t WHERE LOWER(t.[text]) LIKE '%ales.sales'+'orderheader%';
Resultados:
texto | tamaño_en_bytes | conteos de uso | tipo de objeto de caché |
---|---|---|---|
seleccione 1 ID de pedido de ventas superior, o… | 272 | 1 | Resumen del plan compilado |
seleccione 1 ID de pedido de ventas principal, … | 272 | 1 | Resumen del plan compilado |
seleccione el número 1 de ID de pedido de ventas, o… | 272 | 1 | Resumen del plan compilado |
seleccione el (1) ID de pedido de ventas superior,… | 272 | 1 | Resumen del plan compilado |
SELECCIONE TOP (1) Id. de pedido de venta,… | 272 | 1 | Resumen del plan compilado |
SELECCIONE TOP (1) Id. de pedido de venta,… | 272 | 1 | Resumen del plan compilado |
Resultados después de la primera ejecución de consultas "idénticas"
Por lo tanto, esto no es del todo un desperdicio, ya que la configuración ad hoc ha permitido que SQL Server solo almacene pequeños fragmentos en la primera ejecución. Sin embargo, si volvemos a ejecutar el lote (sin liberar el caché de procedimientos), vemos un resultado un poco más alarmante:
texto | tamaño_en_bytes | conteos de uso | tipo de objeto de caché |
---|---|---|---|
seleccione 1 ID de pedido de ventas superior, o… | 49,152 | 1 | Plan Compilado |
seleccione 1 ID de pedido de ventas principal, … | 49,152 | 1 | Plan Compilado |
seleccione el número 1 de ID de pedido de ventas, o… | 49,152 | 1 | Plan Compilado |
seleccione el (1) ID de pedido de ventas superior,… | 49,152 | 1 | Plan Compilado |
SELECCIONE TOP (1) Id. de pedido de venta,… | 49,152 | 1 | Plan Compilado |
SELECCIONE TOP (1) Id. de pedido de venta,… | 49,152 | 1 | Plan Compilado |
Resultados después de la segunda ejecución de consultas "idénticas"
Lo mismo ocurre con las consultas parametrizadas, independientemente de que la parametrización sea simple o forzada. Y lo mismo sucede cuando la configuración ad hoc no está habilitada, excepto que sucede antes.
El resultado neto es que esto puede producir una gran cantidad de caché de planes, incluso para consultas que parecen idénticas, hasta dos consultas en las que un desarrollador sangra con una tabulación y el otro sangra con 4 espacios. No tengo que decirte que tratar de hacer cumplir este tipo de consistencia en un equipo puede ser desde tedioso hasta imposible. Entonces, en mi opinión, esto da un fuerte guiño a modularizar, ceder a DRY y centralizar este tipo de consulta en un único procedimiento almacenado.
Una advertencia
Por supuesto, si coloca esta consulta en un procedimiento almacenado, solo tendrá una copia, por lo que evita por completo la posibilidad de tener varias versiones de la consulta con un texto de consulta ligeramente diferente. Ahora, también podría argumentar que diferentes usuarios pueden crear el mismo procedimiento almacenado con diferentes nombres, y en cada procedimiento almacenado hay una ligera variación del texto de consulta. Si bien es posible, creo que representa un problema completamente diferente. :-)