sql >> Base de Datos >  >> RDS >> Database

Uso de DBCC CLONEDATABASE y Query Store para pruebas

El verano pasado, después del lanzamiento del SP2 para SQL Server 2014, escribí sobre el uso de DBCC CLONEDATABASE para algo más que simplemente investigar un problema de rendimiento de consultas. Un comentario reciente en la publicación de un lector me hizo pensar que debería ampliar lo que tenía en mente sobre cómo usar la base de datos clonada para realizar pruebas. Pedro escribió:

“Soy principalmente un desarrollador de C# y aunque escribo y trato con T-SQL todo el tiempo cuando se trata de ir más allá de SQL Server (prácticamente todas las cosas de DBA, estadísticas y similares) realmente no sé mucho . Ni siquiera sé realmente cómo usaría una base de datos clonada como esta para ajustar el rendimiento”

Bueno, Pedro, aquí tienes. ¡Espero que esto ayude!

Configuración

DBCC CLONEDATABASE estuvo disponible en SQL Server 2016 SP1, así que eso es lo que usaremos para probar, ya que es la versión actual y porque puedo usar Query Store para capturar mis datos. Para hacer la vida más fácil, estoy creando una base de datos para realizar pruebas, en lugar de restaurar una muestra de Microsoft.

USE [master];GO DROP DATABASE IF EXISTS [CustomerDB], [CustomerDB_CLONE];GO /* Cambie las ubicaciones de los archivos según corresponda */ CREATE DATABASE [CustomerDB] ON PRIMARY ( NAME =N'CustomerDB', FILENAME =N' C:\Bases de datos\CustomerDB.mdf', TAMAÑO =512 MB, TAMAÑO MÁXIMO =ILIMITADO, CRECIMIENTO DEL ARCHIVO =65536 KB) INICIAR SESIÓN (NOMBRE =N'CustomerDB_log', NOMBRE DEL ARCHIVO =N'C:\Bases de datos\CustomerDB_log.ldf', TAMAÑO =512 MB, MAXSIZE =ILIMITADO , FILEGROWTH =65536KB );IR ALTER DATABASE [CustomerDB] ESTABLECER RECUPERACIÓN SIMPLE;

Ahora, crea una tabla y agrega algunos datos:

USE [CustomerDB];GO CREATE TABLE [dbo].[Customers]( [CustomerID] [int] NOT NULL, [FirstName] [nvarchar](64) NOT NULL, [LastName] [nvarchar](64) NOT NULL, [Correo electrónico] [nvarchar](320) NOT NULL, [Active] [bit] NOT NULL DEFAULT 1, [Created] [datetime] NOT NULL DEFAULT SYSDATETIME(), [Updated] [datetime] NULL, CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED ([CustomerID]));GO /* Esto agrega 1,000,000 filas a la tabla; no dude en agregar menos*/INSERTAR dbo. Clientes CON (TABLOCKX) (ID de cliente, Nombre, Apellido, Correo electrónico, [Activo]) SELECCIONAR rn =ROW_NUMBER() SOBRE (ORDENAR POR n), fn, ln, em, a FROM ( SELECCIONE SUPERIOR (1000000) fn, ln, em, a =MAX(a), n =MAX(NEWID()) DESDE ( SELECCIONE fn, ln, em, a, r =ROW_NUMBER() SOBRE (PARTICIÓN POR em ORDEN POR em ) DESDE ( SELECCIONE ARRIBA (20000000) fn =IZQUIERDA(o.nombre, 64), ln =IZQUIERDA(c.nombre, 64), em =IZQUIERDA(o.nombre, LARGO(c.nombre)%5+1) + '.' + IZQUIERDA(c.nombre, LARGO(o.nombre)%5+2) + '@' + DERECHA(c.nombre, LARGO(o.nombre + c.nombre)%12 + 1) + IZQUIERDA( RTRIM(CHECKSUM(NEWID())),3) + '.com', a =CASE CUANDO c.name LIKE '%y%' THEN 0 ELSE 1 END FROM sys.all_objects AS o CROSS JOIN sys.all_columns AS c ORDER BY NEWID() ) AS x ) AS y WHERE r =1 GROUP BY fn, ln, em ORDER BY n ) AS z ORDER BY rn;GO CREATE NONCLUSTERED INDEX [PhoneBook_Customers] ON [dbo].[Customers]([LastName] ,[Nombre])INCLUDE ([Correo electrónico]);

Ahora, habilitaremos Query Store:

USE [maestro];GO ALTER DATABASE [CustomerDB] SET QUERY_STORE =ON; Alter database [customerDB] set query_store (operation_mode =read_write, limpiP_policy =(stale_query_threshold_days =30), data_flush_interval_seconds =60, interval_length_minutes =5, max_storaz_size_mb =256, Query_capture_mode =All, All, All, All, size_bask. 

Una vez que hayamos creado y completado la base de datos, y hayamos configurado Query Store, crearemos un procedimiento almacenado para probar:

USAR [CustomerDB];VAYA A DEJAR EL PROCEDIMIENTO SI EXISTE [dbo].[usp_GetCustomerInfo];VAYA A CREAR O ALTERAR EL PROCEDIMIENTO [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECCIONE [CustomerID], [ FirstName], [LastName], [Email], CASE WHEN [Active] =1 THEN 'Active' ELSE 'Inactive' END [Status] FROM [dbo].[Customers] WHERE [LastName] =@LastName;

Tome nota:utilicé la nueva y genial sintaxis CREAR O ALTERAR PROCEDIMIENTO que está disponible en SP1.

Ejecutaremos nuestro procedimiento almacenado un par de veces para obtener algunos datos en Query Store. Agregué WITH RECOMPILE porque sé que estos dos valores de entrada generarán planes diferentes y quiero asegurarme de capturarlos a ambos.

EXEC [dbo].[usp_GetCustomerInfo] 'name' CON RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' CON RECOMPILE;

Si miramos en Query Store, vemos la única consulta de nuestro procedimiento almacenado y dos planes diferentes (cada uno con su propio plan_id). Si este fuera un entorno de producción, tendríamos muchos más datos en términos de estadísticas de tiempo de ejecución (duración, IO, información de CPU) y más ejecuciones. Aunque nuestra demostración tiene menos datos, la teoría es la misma.

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[last_execution_time]) AS [LocalLastExecutionTime], [qst].[query_sql_text], ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])FROM [sys].[query_store_query] [ qsq] ÚNETE [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst].[query_text_id]JOIN [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[ qsp].[query_id]JOIN [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id]WHERE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo'); 

Query Almacenar datos de consulta de procedimiento almacenado Query Almacenar datos después de la ejecución del procedimiento almacenado (query_id =1) con dos planes diferentes (plan_id =1, plan_id =2)

Plan de consulta para plan_id =1 (valor de entrada ='nombre') Plan de consulta para plan_id =2 (valor de entrada ='query_cost')

Una vez que tengamos la información que necesitamos en Query Store, podemos clonar la base de datos (los datos de Query Store se incluirán en el clon de forma predeterminada):

DBCC CLONEDATABASE (N'CustomerDB', N'CustomerDB_CLONE');

Como mencioné en mi publicación anterior de CLONEDATABASE, la base de datos clonada está diseñada para usarse como soporte de productos para probar problemas de rendimiento de consultas. Como tal, es de solo lectura después de clonarlo. Vamos a ir más allá de lo que DBCC CLONEDATABASE está diseñado actualmente para hacer, así que nuevamente, solo quiero recordarles esta nota de la documentación de Microsoft:

La base de datos recién generada generada a partir de DBCC CLONEDATABASE no se admite para usarse como una base de datos de producción y está diseñada principalmente para fines de diagnóstico y solución de problemas.

Para realizar cambios para la prueba, necesito sacar la base de datos del modo de solo lectura. Y estoy de acuerdo con eso porque no planeo usar esto con fines de producción. Si esta base de datos clonada se encuentra en un entorno de producción, le recomiendo que realice una copia de seguridad y la restaure en un servidor de desarrollo o de prueba y realice las pruebas allí. No recomiendo probar en producción, ni recomiendo probar contra la instancia de producción (incluso con una base de datos diferente).

/* Haz que sea de lectura y escritura (haz una copia de seguridad y restáuralo en otro lugar para que no estés trabajando en producción)*/ALTER DATABASE [CustomerDB_CLONE] SET READ_WRITE WITH NO_WAIT;

Ahora que estoy en un estado de lectura y escritura, puedo hacer cambios, hacer algunas pruebas y capturar métricas. Comenzaré verificando que tengo el mismo plan que tenía antes (recordatorio, no verá ningún resultado aquí porque no hay datos en la base de datos clonada):

/* verificar que tengamos el mismo plan */USE [CustomerDB_CLONE];GOEXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;

Al comprobar Query Store, verá el mismo valor de plan_id que antes. Hay varias filas para la combinación query_id/plan_id debido a los diferentes intervalos de tiempo en los que se capturaron los datos (determinados por la configuración INTERVAL_LENGTH_MINUTES, que establecemos en 5).

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[last_execution_time]) AS [LocalLastExecutionTime], [rsi].[runtime_stats_interval_id], [rsi].[start_time], [rsi].[end_time], [qst].[query_sql_text] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])FROM [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst]. [query_text_id]ÚNETE [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id]ÚNETE [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id]ÚNETE [sys].[query_store_runtime_stats_interval] [rsi] ON [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]WHERE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo');IR

Query Store data después de ejecutar el procedimiento almacenado contra la base de datos clonada

Prueba de cambios de código

Para nuestra primera prueba, veamos cómo podríamos probar un cambio en nuestro código; específicamente, modificaremos nuestro procedimiento almacenado para eliminar la columna [Activo] de la lista SELECCIONAR.

/* Cambiar procedimiento usando CREAR O ALTERAR (eliminar [Activo] de la consulta)*/CREAR O ALTERAR PROCEDIMIENTO [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName ], [Apellido], [Correo electrónico] DE [dbo].[Clientes] DONDE [Apellido] =@Apellido;

Vuelva a ejecutar el procedimiento almacenado:

EXEC [dbo].[usp_GetCustomerInfo] 'name' CON RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' CON RECOMPILE;

Si mostró el plan de ejecución real, notará que ambas consultas ahora usan el mismo plan, ya que la consulta está cubierta por el índice no agrupado que creamos originalmente.

Plan de ejecución después de cambiar el procedimiento almacenado para eliminar [Activo]

Podemos verificar con Query Store, nuestro nuevo plan tiene un plan_id de 41:

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[last_execution_time]) AS [LocalLastExecutionTime], [rsi].[runtime_stats_interval_id], [rsi].[start_time], [rsi].[end_time], [qst].[query_sql_text] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])FROM [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst]. [query_text_id]ÚNETE [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id]ÚNETE [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id]ÚNETE [sys].[query_store_runtime_stats_interval] [rsi] ON [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]WHERE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo');

Consultar datos del almacén después de cambiar el procedimiento almacenado

También notará aquí que hay un nuevo query_id (40). Query Store realiza una coincidencia textual y cambiamos el texto de la consulta, por lo que se genera un nuevo query_id. También tenga en cuenta que el object_id permaneció igual, porque el uso usó la sintaxis CREAR O ALTERAR. Hagamos otro cambio, pero use DROP y luego CREATE OR ALTER.

/* Cambiar el procedimiento usando DROP y luego CREAR O ALTERAR (concatenar [FirstName] y [LastName])*/DROP PROCEDURE IF EXISTE [dbo].[usp_GetCustomerInfo];GO CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], RTRIM([FirstName]) + ' ' + RTRIM([LastName]), [Email] FROM [dbo].[Customers] WHERE [LastName] =@ Apellido;

Ahora, volvemos a ejecutar el procedimiento:

EXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' CON RECOMPILE;

Ahora, la salida de Query Store se vuelve más interesante y tenga en cuenta que mi predicado de Query Store ha cambiado a WHERE [qsq].[object_id] <> 0.

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[last_execution_time]) AS [LocalLastExecutionTime], [rsi].[runtime_stats_interval_id], [rsi].[start_time], [rsi].[end_time], [qst].[query_sql_text] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])FROM [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst]. [query_text_id]ÚNETE [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id]ÚNETE [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id]ÚNETE [sys].[query_store_runtime_stats_interval] [rsi] ON [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]WHERE [qsq].[object_id] <> 0;

Query Store data después de cambiar el procedimiento almacenado usando DROP y luego CREATE OR ALTER

El object_id ha cambiado a 661577395, y tengo un nuevo query_id (42) porque el texto de la consulta cambió y un nuevo plan_id (43). Si bien este plan sigue siendo una búsqueda de índice de mi índice no agrupado, sigue siendo un plan diferente en Query Store. Comprenda que el método recomendado para cambiar objetos cuando usa Query Store es usar ALTER en lugar de un patrón DROP y CREATE. Esto es cierto en producción, y para pruebas como esta, ya que desea mantener el object_id igual para facilitar la búsqueda de cambios.

Prueba de cambios en el índice

Para la Parte II de nuestras pruebas, en lugar de cambiar la consulta, queremos ver si podemos mejorar el rendimiento cambiando el índice. Así que cambiaremos el procedimiento almacenado de vuelta a la consulta original, luego modificaremos el índice.

CREAR O ALTERAR PROCEDIMIENTO [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName], [LastName], [Email], CASE WHEN [Active] =1 THEN 'Activo' ELSE 'Inactivo' END [Estado] FROM [dbo].[Clientes] WHERE [Apellido] =@Apellido;IR /* Modificar el índice existente para agregar [Activo] para cubrir la consulta*/CREATE NONCLUSTERED INDEX [PhoneBook_Customers] ON [dbo].[Clientes]([Apellido],[Nombre])INCLUDE ([Correo electrónico], [Activo])CON (DROP_EXISTING=ON);

Debido a que eliminé el procedimiento almacenado original, el plan original ya no está en caché. Si hubiera hecho este cambio de índice primero, como parte de la prueba, recuerde que la consulta no usaría automáticamente el nuevo índice a menos que forzara una recompilación. Podría usar sp_recompile en el objeto, o podría continuar usando la opción CON RECOMPILAR en el procedimiento para ver si obtuve el mismo plan con los dos valores diferentes (recuerde que inicialmente tenía dos planes diferentes). No necesito WITH RECOMPILE ya que el plan no está en caché, pero lo dejo activado por motivos de coherencia.

EXEC [dbo].[usp_GetCustomerInfo] 'name' CON RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' CON RECOMPILE;

Dentro del Almacén de consultas, veo otro nuevo query_id (¡porque el object_id es diferente de lo que era originalmente!) y un nuevo plan_id:

Consultar datos del almacén después de agregar un nuevo índice

Si reviso el plan, puedo ver que se está utilizando el índice modificado.

Plan de consulta después de [Active] añadido al índice (plan_id =50)

Y ahora que tengo un plan diferente, podría dar un paso más e intentar simular una carga de trabajo de producción para verificar que con diferentes parámetros de entrada, este procedimiento almacenado genera el mismo plan y usa el nuevo índice. Sin embargo, hay una advertencia aquí. Es posible que haya notado la advertencia en el operador Búsqueda de índice; esto ocurre porque no hay estadísticas en la columna [Apellido]. Cuando creamos el índice con [Activo] como columna incluida, la tabla se leyó para actualizar las estadísticas. No hay datos en la tabla, de ahí la falta de estadísticas. Esto definitivamente es algo a tener en cuenta con las pruebas de índice. Cuando faltan estadísticas, el optimizador utilizará heurísticas que pueden o no convencer al optimizador de usar el plan que espera.

Resumen

Soy un gran admirador de DBCC CLONEDATABASE. Soy un fan aún más grande de Query Store. Cuando los pone a los dos juntos, tiene una gran capacidad para probar rápidamente los cambios de índice y código. Con este método, busca principalmente planes de ejecución para validar las mejoras. Debido a que no hay datos en una base de datos clonada, no puede capturar el uso de recursos y las estadísticas de tiempo de ejecución para probar o refutar un beneficio percibido en un plan de ejecución. Todavía necesita restaurar la base de datos y probar con un conjunto completo de datos, y Query Store aún puede ser de gran ayuda para capturar datos cuantitativos. Sin embargo, para aquellos casos en los que la validación del plan es suficiente, o para aquellos de ustedes que actualmente no realizan ninguna prueba, DBCC CLONEDATABASE proporciona ese botón fácil que ha estado buscando. Query Store facilita aún más el proceso.

Algunos elementos a destacar:

No recomiendo usar WITH RECOMPILE al llamar a procedimientos almacenados (o declararlos de esa manera; consulte la publicación de Paul White). Usé esta opción para esta demostración porque creé un procedimiento almacenado sensible a los parámetros y quería asegurarme de que los diferentes valores generaran diferentes planes y no usaran un plan de la memoria caché.

Es bastante posible ejecutar estas pruebas en SQL Server 2014 SP2 con DBCC CLONEDATABASE, pero obviamente hay un enfoque diferente para capturar consultas y métricas, así como para observar el rendimiento. Si desea ver esta misma metodología de prueba, sin Query Store, ¡deje un comentario y avíseme!