En general casi todos los DBAs de SQL Server tienen grabadas a fuego las fases por las que pasamos desde que lanzamos una consulta hasta que comienza su ejecución. El proceso básicamente arranca con un paseo inicial, donde detectaremos por ejemplo los errores de sintaxis. A continuación, se validará que los objetos referenciados existen, que los tipos de datos encajan, etc. (metadata) y comenzaremos la fase de optimización. Esta fase de optimización puede ser eludida si previamente tenemos un plan ya cacheado, pero en todo caso tras la optimización obtendremos un plan de ejecución válido que podremos ejecutar:

Desde el Management Studio podemos obtener tanto el plan estimado como el plan actual/real de la ejecución de una consulta. Entre ellos pueden existir algunas diferencias siendo el segundo además mucho más completo al incluir información real sobre el número de filas real procesadas, el número de ejecuciones reales, etc. por lo que suele ser mucho más útil desde el punto de vista de realizar troubleshooting de rendimiento.

Sin embargo, algo que, en general consideramos como cierto, es que este plan va a ser inmutable una vez la consulta comience a ejecutar. Dicho de otra forma, consideramos que cuando arranca un plan de ejecución ya no se modifica hasta finalizar más allá de para añadir los datos sobre las filas reales, los waitstats, etc.

En versiones modernas de SQL Server (2017+) podemos tener cierto “dinamismo” en el plan de ejecución, gracias a las nuevas funcionalidades englobadas dentro del Intelligent Query Processing. Por ejemplo, diferencias causadas por compilaciones entrelazadas (interleaved execution) o a los cruces ajustables (adaptative joins):

Sin embargo, hemos observado un caso curioso en el cual, sin que entren en efecto ninguna de las funcionalidades anteriores, y estando en marcha un plan de ejecución y con bloqueos ya adquiridos, se producen, aparentemente, cambios sobre el plan de ejecución ya en marcha. Veamos paso a paso esta situación con un ejemplo sencillo.

Comenzaremos creando un par de tablas:

use tempdb

go

create table test (a uniqueidentifier)

go

create table test2 (a int)

Una vez creadas, lanzaremos una transacción que implique sufrir un bloqueo para otras operaciones de inserción sobre la tabla test:

begin tran
go
select * from test with (TABLOCK,HOLDLOCK) where 0=1
go

Desde otra sesión habilitaremos la opción “live query” y lanzaremos el siguiente insert el cual quedará bloqueado por la anterior sesión:

El bloqueo, como era previsible lo tenemos sobre el objeto test:

Si nos fijamos en el plan de ejecución de la inserción (que sigue en marcha), veremos que tenemos una expresión que nos convierte el valor de tipo cadena al tipo base de la tabla, uniqueidentifier. Esta conversión, si liberamos el bloqueo de la otra sesión, nos debería dar un error al no poderse convertir el valor ‘12’ en un uniqueidentifier:

¿Pero qué ocurre si, mientras tenemos este plan de ejecución en marcha y con un bloqueo pendiente, intercambiamos las tablas test y test2 desde la transacción que tiene el bloqueo concedido (con lo que puede ejecutar sin problemas la operación)?

Vamos a ello, renombraremos test a test_old y test2 a test:

exec sp_rename 'test','test_old'
go
exec sp_rename 'test2','test'
go

Una vez realizado el cambio podemos ver que el nombre del objeto se modifica pero el object_id del bloqueo no se modifica por lo que apunta a test_old:

Llegados a este punto confirmaremos la transacción, para permitir a la transacción bloqueada continuar y, en teoría, fallar el insert que teníamos bloqueado al intentar insertar en el objeto al que apunta el plan de ejecución que ahora es llamado test_old:

commit
go

Sin embargo, si volvemos a la consulta bloqueada nos encontramos que no ha fallado, ha insertado correctamente el valor:

Si volvemos a observar el plan de ejecución, veremos que la conversión implícita a UNIQUEIDENTIFIER que teníamos en el plan ha sido modificada a una conversión a entero pese a que el plan se encontraba ya ejecutando:

De la misma forma, el insert que inicialmente tenía un bloqueo sobre la tabla que se renombró a test_old se concedió pero se ignoró ese plan, se creó un plan nuevo, apuntando a la nueva tabla test (la anterior test2).

Esto podemos comprobarlo viendo los eventos de compilación iniciales donde se veía claramente que se hacía un cast a uniqueidentifier y luego la recopilación tras el commit de la transacción que lo bloqueaba donde se genera un nuevo plan con el convert a tipo de datos int:

Esta situación ocurriría igualmente si en vez de un renombrado/intercambio de tablas se realizara por ejemplo un cambio de esquema. En este caso si añadimos una columna nueva, tendríamos un error si dicha columna no incluye un valor por defecto y no admite nulos. Sin embargo, si realizamos el añadido de esa columna sin admitir nulos pero con un valor por defecto sí funcionaría.

Los pasos serían similares, crearíamos una tabla y generaríamos un bloqueo:

create table test2 (a int)
go
begin tran
go
select * from test2 with (TABLOCK,HOLDLOCK) where 0=1

A continuación, lanzaríamos el insert en otra sesión que quedaría bloqueado:

insert into test2 (a) values ('12')

Añadiríamos la nueva columna con el valor por defecto y confirmaríamos la transacción:

alter table test2 add v varchar(10) not null default 'a'
commit

Y al desbloquearse la sesión previamente bloqueada, se generará un nuevo plan, añadiendo el valor por defecto y se insertará sin problemas el registro:

En conclusión, existen algunos casos específicos durante los cuales, aunque el plan de ejecución ya esté en marcha, SQL Server fuerza una recompilación, rearranca el plan, vuelve a readquirir nuevos bloqueos y ejecuta un plan adaptado a los nuevos metadatos. Este plan podrá funcionar o nos generará un error en función de si los valores originales, sean procedentes de una select en un caso de insert – select o de un insert adhoc, es  posible convertirlos a los nuevos tipos de datos.

Rubén Garrigós

Rubén Garrigós is an expert in high-availability enterprise solutions based on SQL Server design, tuning, and troubleshooting. Over the past fifteen years, he has worked with Microsoft data access technologies in leading companies around the world. He currently is a Microsoft SQL Server and .NET applications architect with SolidQ. Ruben is certified by Microsoft as a Solution Expert on the Microsoft Data Platform (MSCE: Data Platform) and as a Solution Expert on the Microsoft Private Cloud (MSCE: Private Cloud). As a Microsoft Certified Trainer (MCT), Ruben has taught multiple official Microsoft courses as well as other courses specializing in SQL Server. He has also presented sessions at official events for various Microsoft technologies user groups.

Latest posts by Rubén Garrigós (see all)