Estoy tratando de ver si hay una manera de engañar a SQL Server para que use un cierto plan para la consulta.
1. Medio ambiente
Imagine que tiene algunos datos que se comparten entre diferentes procesos. Entonces, supongamos que tenemos algunos resultados de experimentos que ocupan mucho espacio. Luego, para cada proceso, sabemos qué año / mes de resultado del experimento queremos usar.
if object_id('dbo.SharedData') is not null
drop table SharedData
create table dbo.SharedData (
experiment_year int,
experiment_month int,
rn int,
calculated_number int,
primary key (experiment_year, experiment_month, rn)
)
go
Ahora, para cada proceso tenemos parámetros guardados en la tabla
if object_id('dbo.Params') is not null
drop table dbo.Params
create table dbo.Params (
session_id int,
experiment_year int,
experiment_month int,
primary key (session_id)
)
go
2. Datos de prueba
Agreguemos algunos datos de prueba:
insert into dbo.Params (session_id, experiment_year, experiment_month)
select 1, 2014, 3 union all
select 2, 2014, 4
go
insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
2014, 3, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
cross join master.dbo.spt_values as v2
go
insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
2014, 4, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
cross join master.dbo.spt_values as v2
go
3. Obteniendo resultados
Ahora, es muy fácil obtener resultados de experimentos al @experiment_year/@experiment_month
:
create or alter function dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.SharedData as d
where
d.experiment_year = @experiment_year and
d.experiment_month = @experiment_month
)
go
El plan es agradable y paralelo:
select
calculated_number,
count(*)
from dbo.f_GetSharedData(2014, 4)
group by
calculated_number
consulta 0 plan
4. problema
Pero, para hacer uso de los datos un poco más genérico, quiero tener otra función - dbo.f_GetSharedDataBySession(@session_id int)
. Entonces, la forma más sencilla sería crear funciones escalares, traduciendo @session_id
-> @experiment_year/@experiment_month
:
create or alter function dbo.fn_GetExperimentYear(@session_id int)
returns int
as
begin
return (
select
p.experiment_year
from dbo.Params as p
where
p.session_id = @session_id
)
end
go
create or alter function dbo.fn_GetExperimentMonth(@session_id int)
returns int
as
begin
return (
select
p.experiment_month
from dbo.Params as p
where
p.session_id = @session_id
)
end
go
Y ahora podemos crear nuestra función:
create or alter function dbo.f_GetSharedDataBySession1(@session_id int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.f_GetSharedData(
dbo.fn_GetExperimentYear(@session_id),
dbo.fn_GetExperimentMonth(@session_id)
) as d
)
go
consulta 1 plan
El plan es el mismo, excepto que, por supuesto, no es paralelo, porque las funciones escalares que realizan el acceso a datos hacen que todo el plan sea serial .
Así que probé varios enfoques diferentes, como usar subconsultas en lugar de funciones escalares:
create or alter function dbo.f_GetSharedDataBySession2(@session_id int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.f_GetSharedData(
(select p.experiment_year from dbo.Params as p where p.session_id = @session_id),
(select p.experiment_month from dbo.Params as p where p.session_id = @session_id)
) as d
)
go
consulta 2 plan
O usando cross apply
create or alter function dbo.f_GetSharedDataBySession3(@session_id int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.Params as p
cross apply dbo.f_GetSharedData(
p.experiment_year,
p.experiment_month
) as d
where
p.session_id = @session_id
)
go
consulta 3 plan
Pero no puedo encontrar una manera de escribir esta consulta para que sea tan buena como la que usa funciones escalares.
Par de pensamientos:
- Básicamente, lo que quiero es poder decirle de alguna manera a SQL Server que precalcule ciertos valores y luego los pase más como constantes.
- Lo que podría ser útil es si tuviéramos alguna pista de materialización intermedia . He comprobado un par de variantes (TVF multi-declaración o cte con top), pero hasta ahora ningún plan es tan bueno como el que tiene funciones escalares
- Sé acerca de la próxima mejora de SQL Server 2017 - Froid: optimización de programas imperativos en una base de datos relacional. Sin embargo, no estoy seguro de que ayude. Sin embargo, hubiera sido bueno que se demuestre lo contrario aquí.
Información Adicional
Estoy usando una función (en lugar de seleccionar datos directamente de las tablas) porque es mucho más fácil de usar en muchas consultas diferentes, que generalmente tienen @session_id
como parámetro.
Me pidieron que comparara los tiempos de ejecución reales. En este caso particular
- la consulta 0 se ejecuta durante ~ 500 ms
- la consulta 1 se ejecuta durante ~ 1500 ms
- la consulta 2 se ejecuta durante ~ 1500 ms
- la consulta 3 se ejecuta durante ~ 2000 ms.
El plan n. ° 2 tiene una exploración de índice en lugar de una búsqueda, que luego se filtra por predicados en bucles anidados. El plan n. ° 3 no es tan malo, pero aún así trabaja más y funciona más lentamente que el plan n. ° 0.
Supongamos que dbo.Params
se cambia raramente, y generalmente tiene alrededor de 1-200 filas, no más de, digamos, que se espera 2000. Ahora son alrededor de 10 columnas y no espero agregar columnas con demasiada frecuencia.
El número de filas en los parámetros no es fijo, por lo que por cada @session_id
habrá una fila. El número de columnas no está fijo, es una de las razones por las que no quiero llamar dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
desde todas partes, por lo que puedo agregar una nueva columna a esta consulta internamente. Me alegraría escuchar cualquier opinión / sugerencia sobre esto, incluso si tiene algunas restricciones.
fuente
Respuestas:
Realmente no puede lograr con seguridad exactamente lo que quiere en SQL Server hoy, es decir, en una sola declaración y con ejecución paralela, dentro de las restricciones establecidas en la pregunta (como las percibo).
Entonces mi respuesta simple es no . El resto de esta respuesta es principalmente una discusión de por qué es así, en caso de que sea de interés.
Es posible obtener un plan paralelo, como se señaló en la pregunta, pero hay dos variedades principales, ninguna de las cuales es adecuada para sus necesidades:
Se unen los bucles anidados correlacionados, con un flujo de distribución de turnos en el nivel superior. Dado que se garantiza que una única fila proviene de
Params
unsession_id
valor específico , el lado interno se ejecutará en un solo hilo, aunque esté marcado con el icono de paralelismo. Es por eso que el plan aparentemente paralelo 3 no funciona tan bien; De hecho es serial.La otra alternativa es el paralelismo independiente en el lado interno de la unión de bucles anidados. Independiente aquí significa que los subprocesos se inician en el lado interno, y no simplemente los mismos subprocesos que se ejecutan en el lado externo de la unión de bucles anidados. SQL Server solo admite el paralelismo de bucles anidados del lado interno independiente cuando se garantiza que haya una fila del lado externo y no hay parámetros de unión correlacionados ( plan 2 ).
Entonces, tenemos la opción de un plan paralelo que es serial (debido a un hilo) con los valores correlacionados deseados; o un plan paralelo del lado interno que tiene que escanear porque no tiene parámetros para buscar. (Aparte: realmente debería permitirse conducir el paralelismo del lado interno utilizando exactamente un conjunto de parámetros correlacionados, pero nunca se ha implementado, probablemente por una buena razón).
Una pregunta natural es: ¿por qué necesitamos parámetros correlacionados? ¿Por qué SQL Server no puede buscar directamente los valores escalares proporcionados por, por ejemplo, una subconsulta?
Bueno, SQL Server solo puede 'indexar la búsqueda' usando referencias escalares simples, por ejemplo, una referencia constante, variable, de columna o de expresión (por lo que un resultado de función escalar también puede calificar). Una subconsulta (u otra construcción similar) es simplemente demasiado compleja (y potencialmente insegura) para introducirla en el motor de almacenamiento completo. Por lo tanto, se requieren operadores de planes de consulta separados. Este turno requiere correlación, lo que significa que no hay paralelismo del tipo que desea.
En general, actualmente no hay una solución mejor que métodos como asignar los valores de búsqueda a las variables y luego usarlos en los parámetros de la función en una declaración separada.
Ahora puede tener consideraciones locales específicas que significan que
SESSION_CONTEXT
vale la pena almacenar en caché los valores actuales del año y mes, es decir:Pero esto cae en la categoría de solución alternativa.
Por otro lado, si el rendimiento de la agregación es de importancia primordial, podría considerar quedarse con las funciones en línea y crear un índice de almacén de columnas (primario o secundario) en la tabla. Es posible que los beneficios del almacenamiento en almacén de columnas, el procesamiento en modo por lotes y el pushdown agregado brinden mayores beneficios que una búsqueda paralela en modo fila de todos modos.
Pero tenga cuidado con las funciones escalares de T-SQL, especialmente con el almacenamiento del almacén de columnas, ya que es fácil terminar con la función que se evalúa por fila en un filtro de modo de fila separado. Por lo general, es bastante complicado garantizar la cantidad de veces que SQL Server elegirá evaluar escalares, y mejor no intentarlo.
fuente
session_context
pero decido que es una idea demasiado loca para mí y no estoy seguro de cómo se adaptará a mi arquitectura actual. Sin embargo, lo que sería útil es, puede ser, alguna pista que podría usar para informarle al optimizador que debería tratar el resultado de la subconsulta como una simple referencia escalar.Hasta donde sé, la forma del plan que desea no es posible con solo T-SQL. Parece que desea que la forma del plan original (consulta 0 plan) con las subconsultas de sus funciones se apliquen como filtros directamente contra el escaneo de índice agrupado. Nunca obtendrá un plan de consulta como ese si no utiliza variables locales para contener los valores de retorno de las funciones escalares. En su lugar, el filtrado se implementará como una unión de bucle anidado. Hay tres formas diferentes (desde un punto de vista de paralelismo) de implementar la unión de bucle:
Esas son las únicas formas de plan posibles que conozco. Puede obtener otros si usa una tabla temporal, pero ninguno de ellos resuelve su problema fundamental si desea que el rendimiento de la consulta sea tan bueno como lo fue para la consulta 0.
Puede lograr un rendimiento de consulta equivalente utilizando las UDF escalares para asignar valores de retorno a variables locales y utilizando esas variables locales en su consulta. Puede ajustar ese código en un procedimiento almacenado o en un UDF de varias instrucciones para evitar problemas de mantenimiento. Por ejemplo:
Los UDF escalares se han movido fuera de la consulta en la que desea ser elegible para el paralelismo. El plan de consulta que obtengo parece ser el que desea:
Ambos enfoques tienen desventajas si necesita utilizar este conjunto de resultados en otras consultas. No puede unirse directamente a un procedimiento almacenado. Tendría que guardar los resultados en una tabla temporal que tiene su propio conjunto de problemas. Puede unirse a un MS-TVF, pero en SQL Server 2016 puede ver problemas de estimación de cardinalidad. SQL Server 2017 ofrece ejecución intercalada para MS-TVF que podría resolver el problema por completo.
Solo para aclarar algunas cosas: las UDF escalares de T-SQL siempre prohíben el paralelismo y Microsoft no ha dicho que FROID estará disponible en SQL Server 2017.
fuente
Lo más probable es que esto se pueda hacer usando SQLCLR. Uno de los beneficios de SQLCLR escalares UDF es que no impiden el paralelismo si ellos no hacen ningún acceso a los datos (y, a veces necesitan también ser marcado como "determinista"). Entonces, ¿cómo hace uso de algo que no requiere acceso a datos cuando la operación en sí misma requiere acceso a datos?
Bueno, porque
dbo.Params
se espera que la tabla:INT
columnases factible almacenar en caché las tres columnas -
session_id, experiment_year int, experiment_month
en una colección estática (por ejemplo, un diccionario, tal vez) que se completa fuera de proceso y es leída por los UDF escalares que obtienen los valoresexperiment_year int
yexperiment_month
. Lo que quiero decir con "fuera de proceso" es: puede tener un UDF escalar SQLCLR completamente separado o procedimiento almacenado que puede hacer acceso a datos y lecturas de ladbo.Params
tabla para completar la colección estática. Ese UDF o procedimiento almacenado se ejecutará antes de usar los UDF que obtienen los valores de "año" y "mes", de esa manera los UDF que obtienen los valores de "año" y "mes" no están haciendo ningún acceso a los datos de la base de datos.El UDF o procedimiento almacenado que lee los datos puede verificar primero para ver si la colección tiene 0 entradas y, en caso afirmativo, completar, de lo contrario omitir. Incluso puede realizar un seguimiento de la hora en que se rellenó y si han pasado más de X minutos (o algo así), luego borre y vuelva a llenar incluso si hay entradas en la colección. Pero omitir a la población ayudará, ya que deberá ejecutarse con frecuencia para garantizar que siempre se complete para que los dos UDF principales obtengan los valores.
La principal preocupación es cuando SQL Server decide descargar el dominio de la aplicación por cualquier motivo (o se desencadena por algo que lo usa
DBCC FREESYSTEMCACHE('ALL');
). No desea arriesgarse a que se borre la colección entre la ejecución del UDF "poblado" o Procedimiento almacenado y los UDF para obtener los valores "año" y "mes". En ese caso, puede hacer una comprobación al comienzo de esos dos UDF para lanzar una excepción si la colección está vacía, ya que es mejor cometer un error que proporcionar resultados falsos con éxito.Por supuesto, la preocupación mencionada anteriormente supone que el deseo es que la Asamblea se marque como
SAFE
. Si el ensamblaje se puede marcar comoEXTERNAL_ACCESS
, entonces es posible que un constructor estático ejecute el método que lee los datos y llena la colección, de modo que solo necesite ejecutarlos manualmente para actualizar las filas, pero siempre se completarán (porque el constructor de la clase estática siempre se ejecuta cuando se carga la clase, lo que sucede cada vez que se ejecuta un método en esta clase después de un reinicio o se descarga el dominio de la aplicación). Esto requiere el uso de una conexión regular y no la conexión de contexto en proceso (que no está disponible para los constructores estáticos, de ahí la necesidad de hacerloEXTERNAL_ACCESS
).Tenga en cuenta: para no tener que marcar el ensamblado como
UNSAFE
, debe marcar cualquier variable de clase estática comoreadonly
. Esto significa, al menos, la colección. Esto no es un problema ya que las colecciones de solo lectura pueden tener elementos agregados o eliminados, simplemente no se pueden inicializar fuera del constructor o la carga inicial. El seguimiento del tiempo en que se cargó la colección con el fin de expirarla después de X minutos es más complicado ya que unastatic readonly DateTime
variable de clase no se puede cambiar fuera del constructor o la carga inicial. Para evitar esta restricción, debe usar una colección estática de solo lectura que contenga un único elemento que sea elDateTime
valor para que pueda eliminarse y volver a agregarse después de una actualización.fuente
readonly statics
SQLCLR sea seguro o inteligente. Mucho menos, estoy convencido de que luego pasaré a engañar al sistema convirtiéndolo enreadonly
un tipo de referencia, que luego irás y cambiarás . Me da la voluntad absoluta tbh.static
objetos) en SQL Server, sí, existe el riesgo de condiciones de carrera. Es por eso que primero determiné a través del OP que estos datos son mínimos y estables, y por qué califiqué este enfoque como "rara vez cambia", y proporcioné un medio de actualización cuando era necesario. En este caso de uso, no veo mucho o ningún riesgo. Encontré una publicación hace años sobre la capacidad de actualizar colecciones de solo lectura por diseño (en C #, sin discusión sobre: SQLCLR). Intentaremos encontrarlo.