Estou tentando ver se há uma maneira de enganar o SQL Server para usar um determinado plano para a consulta.
1. Ambiente
Imagine que você tem alguns dados que são compartilhados entre diferentes processos. Então, suponha que tenhamos alguns resultados de experimentos que ocupem muito espaço. Então, para cada processo, sabemos qual ano / mês de resultado do 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
Agora, para cada processo, temos parâmetros salvos na tabela
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. Dados de teste
Vamos adicionar alguns dados de teste:
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. Buscando resultados
Agora, é muito fácil obter resultados da experiência @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
O plano é bom e paralelo:
select
calculated_number,
count(*)
from dbo.f_GetSharedData(2014, 4)
group by
calculated_number
consulta 0 plano
4. Problema
Mas, para tornar o uso dos dados um pouco mais genérico, quero ter outra função - dbo.f_GetSharedDataBySession(@session_id int)
. Portanto, a maneira direta seria criar funções escalares, traduzindo @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
E agora podemos criar nossa função:
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
plano de consulta 1
O plano é o mesmo, exceto que, é claro, não é paralelo, porque funções escalares que executam acesso a dados tornam todo o plano serial .
Então, eu tentei várias abordagens diferentes, como usar subconsultas em vez de funções 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
plano de consulta 2
Ou 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
plano de consulta 3
Mas não consigo encontrar uma maneira de escrever essa consulta tão boa quanto a que utiliza funções escalares.
Par de pensamentos:
- Basicamente, o que eu gostaria é poder, de alguma forma, dizer ao SQL Server para pré-calcular certos valores e depois passá-los como constantes.
- O que poderia ser útil é se tivéssemos alguma dica de materialização intermediária . Eu verifiquei algumas variantes (TVF de múltiplas instruções ou cte com top), mas nenhum plano é tão bom quanto o que tem funções escalares até agora
- Eu sei sobre as próximas melhorias do SQL Server 2017 - Froid: Otimização de programas imperativos em um banco de dados relacional . Não tenho certeza se isso ajudará. Seria bom se provar errado aqui, no entanto.
Informação adicional
Estou usando uma função (em vez de selecionar dados diretamente das tabelas) porque é muito mais fácil usar em muitas consultas diferentes, que geralmente têm @session_id
como parâmetro.
Me pediram para comparar os tempos de execução reais. Nesse caso em particular
- a consulta 0 é executada por ~ 500ms
- a consulta 1 é executada por ~ 1500ms
- a consulta 2 é executada por ~ 1500ms
- a consulta 3 é executada por ~ 2000ms.
O plano 2 tem uma varredura de índice em vez de uma busca, que é filtrada por predicados em loops aninhados. O plano 3 não é tão ruim assim, mas ainda faz mais trabalho e funciona mais devagar que o plano 0.
Vamos supor que isso dbo.Params
seja alterado raramente e geralmente tenha de 1 a 200 linhas, não mais do que, digamos, 2000. Agora são 10 colunas e não espero adicionar colunas com muita frequência.
O número de linhas no Params não é fixo, portanto, para cada @session_id
uma delas, haverá uma linha. O número de colunas não é fixo, é um dos motivos pelos quais não desejo ligar dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
de todos os lugares, para que eu possa adicionar nova coluna a essa consulta internamente. Eu ficaria feliz em ouvir quaisquer opiniões / sugestões sobre isso, mesmo que haja algumas restrições.
fonte
Respostas:
Você não pode realmente conseguir exatamente o que deseja no SQL Server hoje, ou seja, em uma única declaração e com execução paralela, dentro das restrições estabelecidas na pergunta (como eu as percebo).
Então, minha resposta simples é não . O restante desta resposta é principalmente uma discussão sobre o motivo disso, caso seja de interesse.
É possível obter um plano paralelo, conforme observado na pergunta, mas existem duas variedades principais, nenhuma das quais é adequada às suas necessidades:
Um loop aninhado correlacionado se junta, com um round-robin distribui fluxos no nível superior. Dado que é garantida a origem
Params
de uma única linha para umsession_id
valor específico , o lado interno será executado em um único encadeamento, mesmo que esteja marcado com o ícone de paralelismo. É por isso que o plano aparentemente paralelo 3 não funciona tão bem; é de fato serial.A outra alternativa é para o paralelismo independente no lado interno da junção de loops aninhados. Independente aqui significa que os threads são iniciados no lado interno, e não apenas os mesmos threads que estão executando o lado externo da junção de loops aninhados. O SQL Server oferece suporte apenas ao paralelismo de loops aninhados do lado interno independente quando é garantido que há uma linha do lado externo e não há parâmetros de junção correlacionados ( plano 2 ).
Portanto, podemos escolher um plano paralelo que seja serial (devido a um encadeamento) com os valores correlatos desejados; ou um plano paralelo do lado interno que precisa ser varrido porque não possui parâmetros para buscar. (Além disso: realmente deve ser permitido conduzir o paralelismo interno usando exatamente um conjunto de parâmetros correlatos, mas nunca foi implementado, provavelmente por um bom motivo).
Uma pergunta natural é: por que precisamos de parâmetros correlatos? Por que o SQL Server não pode simplesmente buscar diretamente os valores escalares fornecidos por, por exemplo, uma subconsulta?
Bem, o SQL Server pode apenas 'indexar busca' usando referências escalares simples, por exemplo, uma referência constante, variável, coluna ou expressão (para que um resultado de função escalar também possa ser qualificado). Uma subconsulta (ou outra construção semelhante) é simplesmente muito complexa (e potencialmente insegura) para ser inserida no mecanismo de armazenamento inteiro. Portanto, são necessários operadores de plano de consulta separados. Por sua vez, isso requer correlação, o que significa que não há paralelismo do tipo que você deseja.
No geral, atualmente não há solução melhor do que métodos como atribuir os valores de pesquisa a variáveis e depois usá-los nos parâmetros de função em uma instrução separada.
Agora você pode ter considerações locais específicas que significam que
SESSION_CONTEXT
vale a pena armazenar em cache os valores atuais do ano e do mês, ou seja:Mas isso se enquadra na categoria de solução alternativa.
Por outro lado, se o desempenho da agregação for de importância primordial, considere manter as funções em linha e criar um índice columnstore (primário ou secundário) na tabela. Você pode encontrar os benefícios do armazenamento columnstore, processamento no modo em lote e empilhamento agregado, oferecendo maiores benefícios do que uma busca paralela no modo de linha de qualquer maneira.
Mas tome cuidado com as funções escalares do T-SQL, especialmente com o armazenamento columnstore, pois é fácil terminar com a função sendo avaliada por linha em um filtro separado no modo de linha. Geralmente, é bastante complicado garantir o número de vezes que o SQL Server escolhe avaliar escalares e é melhor não tentar.
fonte
session_context
mas decido que é uma idéia um pouco louca demais para mim e não tenho certeza de como isso se encaixará na minha arquitetura atual. O que seria útil, porém, é, pode ser, alguma dica que eu possa usar para informar ao otimizador que ele deve tratar o resultado da subconsulta como uma simples referência escalar.Tanto quanto sei, a forma do plano que você deseja não é possível apenas com o T-SQL. Parece que você deseja que a forma do plano original (consulta 0 plano) com as subconsultas de suas funções sejam aplicadas como filtros diretamente na verificação de índice em cluster. Você nunca obterá um plano de consulta como esse se não usar variáveis locais para armazenar os valores de retorno das funções escalares. A filtragem será implementada como uma junção de loop aninhada. Existem três maneiras diferentes (do ponto de vista do paralelismo) em que a junção de loop pode ser implementada:
Essas são as únicas formas possíveis de plano que eu conheço. Você pode obter outros se usar uma tabela temporária, mas nenhum deles resolverá seu problema fundamental se desejar que o desempenho da consulta seja tão bom quanto o da consulta 0.
Você pode obter desempenho equivalente à consulta usando as UDFs escalares para atribuir valores de retorno às variáveis locais e usando essas variáveis locais na sua consulta. Você pode agrupar esse código em um procedimento armazenado ou em um UDF com várias instruções para evitar problemas de manutenção. Por exemplo:
Os UDFs escalares foram movidos para fora da consulta que você deseja ser elegível para paralelismo. O plano de consulta que recebo parece ser o que você deseja:
Ambas as abordagens têm desvantagens se você precisar usar esse conjunto de resultados em outras consultas. Você não pode ingressar diretamente em um procedimento armazenado. Você precisaria salvar os resultados em uma tabela temporária com seu próprio conjunto de problemas. Você pode ingressar em um MS-TVF, mas no SQL Server 2016 você pode ver problemas de estimativa de cardinalidade. O SQL Server 2017 oferece execução intercalada para o MS-TVF, o que poderia resolver o problema completamente.
Apenas para esclarecer algumas coisas: UDFs escalares T-SQL sempre proíbem o paralelismo e a Microsoft não disse que o FROID estará disponível no SQL Server 2017.
fonte
Provavelmente, isso pode ser feito usando o SQLCLR. Um dos benefícios de SQLCLR escalares UDFs é que eles não impedem o paralelismo se eles não fazem qualquer acesso de dados (e às vezes precisa também ser marcado como "determinista"). Então, como você usa algo que não requer acesso a dados quando a própria operação exige acesso a dados?
Bem, porque a
dbo.Params
tabela deve:INT
colunasé possível armazenar em cache as três colunas -
session_id, experiment_year int, experiment_month
- em uma coleção estática (por exemplo, um dicionário, talvez) que seja preenchida fora de processo e lida pelas UDFs escalares que obtêm os valoresexperiment_year int
eexperiment_month
. O que quero dizer com "fora de processo" é: você pode ter um UDF escalar SQLCLR ou procedimento armazenado completamente separado que possa acessar e ler dados dadbo.Params
tabela para preencher a coleção estática. Esse UDF ou Stored Procedure seria executado antes do uso das UDFs que obtêm os valores "year" e "month", assim as UDFs que obtêm os valores "year" e "month" não estão acessando os dados do banco de dados.O UDF ou Stored Procedure que lê os dados pode verificar primeiro se a coleção possui 0 entradas e, se houver, preencher, em seguida, ignorar. Você pode até controlar o tempo em que foi preenchido e se já passou mais de X minutos (ou algo parecido), limpe e preencha novamente mesmo que haja entradas na coleção. Mas pular a população ajudará, pois precisará ser executado com freqüência para garantir que seja sempre preenchido pelas duas UDFs principais para obter os valores.
A principal preocupação é quando o SQL Server decide descarregar o domínio do aplicativo por qualquer motivo (ou é acionado por algo usando
DBCC FREESYSTEMCACHE('ALL');
). Você não deseja arriscar que a coleta seja limpa entre a execução da UDF ou do procedimento armazenado "preencher" e as UDFs para obter os valores "ano" e "mês". Nesse caso, você pode ter uma verificação no início dessas duas UDFs para lançar uma exceção se a coleção estiver vazia, pois é melhor erro do que fornecer resultados falsos com êxito.Obviamente, a preocupação mencionada acima pressupõe que o desejo é ter a Assembléia marcada como
SAFE
. Se o Assembly puder ser marcado comoEXTERNAL_ACCESS
, é possível que um construtor estático execute o método que lê os dados e preencha a coleção, para que você só precise executá-lo manualmente para atualizar as linhas, mas elas sempre serão preenchidas (porque o construtor de classe estática sempre é executado quando a classe é carregada, o que acontece sempre que um método nessa classe é executado após uma reinicialização ou o domínio do aplicativo é descarregado). Isso requer o uso de uma conexão regular e não a Conexão de Contexto em processo (que não está disponível para construtores estáticos, daí a necessidadeEXTERNAL_ACCESS
).Observe: para não ser necessário marcar a montagem como
UNSAFE
, você precisa marcar as variáveis de classe estática comoreadonly
. Isso significa, no mínimo, a coleção. Isso não é um problema, pois as coleções somente leitura podem ter itens adicionados ou removidos, apenas não podem ser inicializadas fora do construtor ou do carregamento inicial. Controlar o tempo em que a coleção foi carregada com o objetivo de expirar após X minutos é mais complicado, pois umastatic readonly DateTime
variável de classe não pode ser alterada fora do construtor ou do carregamento inicial. Para contornar essa restrição, você precisa usar uma coleção estática, somente leitura, que contenha um único item com oDateTime
valor para que possa ser removida e adicionada novamente após uma atualização.fonte
readonly statics
seja seguro ou sábio no SQLCLR. Muito menos estou convencido de que, depois disso, vou enganar o sistema, tornando essereadonly
um tipo de referência, que você muda e muda . Dá-me a vontade absoluta.static
objetos) no SQL Server, sim, há risco de condições de corrida. Foi por isso que primeiro determinei a partir do OP que esses dados são mínimos e estáveis, e porque qualifiquei essa abordagem como exigindo "raramente mudando" e forneci um meio de atualização quando necessário. Em este caso de uso não vejo muito se qualquer risco. Encontrei um post anos atrás sobre a capacidade de atualizar coleções somente leitura por design (em C #, nenhuma discussão sobre: SQLCLR). Vai tentar encontrá-lo.