A consulta contra sys.schemas e sys.synônimos é executada muito lentamente para um usuário

8

Cenário: SQL Server 2014 (v12.0.4100.1)

O serviço .NET executa esta consulta:

SELECT name, base_object_name 
FROM sys.synonyms 
WHERE schema_id IN (SELECT schema_id 
                    FROM sys.schemas 
                    WHERE name = N'XXXX')
ORDER BY name

... que retorna cerca de 6500 linhas, mas geralmente atinge o tempo limite após mais de 3 minutos. O XXXXacima não é 'dbo'.

Se eu executar esta consulta no SSMS como UsuárioA, a consulta retornará em menos de um segundo.

Quando executada como UsuárioB (que é como o serviço .NET se conecta), a consulta leva de 3 a 6 minutos e tem a% da CPU em 25% (de 4 núcleos) o tempo todo.

UserA é um logon de domínio na função sysadmin.

UserB é um logon SQL com:

EXEC sp_addrolemember N'db_datareader', N'UserB'
EXEC sp_addrolemember N'db_datawriter', N'UserB'
EXEC sp_addrolemember N'db_ddladmin', N'UserB'
GRANT EXECUTE TO [UserB]
GRANT CREATE SCHEMA TO [UserB]
GRANT VIEW DEFINITION TO [UserB]

Eu posso duplicar isso no SSMS envolvendo o SQL acima em um Execute as...Revertbloco, para que o código .NET fique fora de cena.

O plano de execução parece o mesmo. Eu diferenciei o XML e existem apenas pequenas diferenças (CompileTime, CompileCPU, CompileMemory).

Todas as estatísticas de E / S não mostram leituras físicas:

Tabela 'sysobjvalues'. Contagem de varredura 0, leituras lógicas 19970, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de read-ahead de lob 0.
Tabela 'Arquivo de Trabalho'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de lob 0.
Tabela 'Mesa de trabalho'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de lob 0.
Tabela 'sysschobjs'. Contagem de varredura 1, leituras lógicas 9122, leituras físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob 0.
Tabela 'sysclsobjs'. Contagem de varreduras 0, leituras lógicas 2, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de lob 0.

O status XEvent aguarda (para uma consulta de ~ 3 minutos) é:

+ --------------------- + ------------ + -------------- -------- + ------------------------------ + ---------- ------------------- +
| Tipo de espera | Contagem de espera | Tempo total de espera (ms) | Tempo total de espera do recurso (ms) | Tempo total de espera do sinal (ms) |
+ --------------------- + ------------ + -------------- -------- + ------------------------------- + --------- -------------------- +
| SOS_SCHEDULER_YIELD 37300 427 20 407
| NETWORK_IO | 5 26 26 0
| IO_COMPLETION | 3 1 | 1 | 0
+ --------------------- + ------------ + -------------- -------- + ------------------------------- + --------- -------------------- +

Se eu reescrever a consulta (no SSMS, não tenho acesso ao código do aplicativo) para

declare @id int 
SELECT @id=schema_id FROM sys.schemas WHERE name = N'XXXX'
SELECT a.name, base_object_name FROM sys.synonyms a
WHERE schema_id = @id
ORDER BY name

então, o UsuárioB executa na mesma velocidade (rápida) que o UsuárioA.

Se eu adicionar db_ownerao UsuárioB, novamente, a consulta será executada <1 s.

Esquema criado através deste modelo:

DECLARE @TranName VARCHAR(20)
SELECT @TranName = 'MyTransaction'

BEGIN TRANSACTION @TranName
GO

IF NOT EXISTS (SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA
        WHERE SCHEMA_NAME = '{1}')
BEGIN
    EXEC('CREATE SCHEMA [{1}]')
    EXEC sp_addextendedproperty @name='User', @value='{0}', @level0type=N'Schema', @level0name=N'{1}'
END
GO

{2}

COMMIT TRANSACTION MyTransaction;
GO

E {2} é, acredito, uma lista de sinônimos criados nesse esquema.

Perfil de consulta em dois pontos da consulta:

insira a descrição da imagem aqui

insira a descrição da imagem aqui

Abri um ticket com a Microsoft.

Além disso, tentamos adicionar o UserB e db_owner, em seguida, DENYtodos os privilégios que sabemos que estão associados db_owner. O resultado é uma consulta rápida. Perdemos algo (inteiramente possível) ou há uma verificação especial para o db_ownerpapel.

James
fonte

Respostas:

5

Você pode reescrever sua consulta da seguinte maneira (estou usando, dboem vez disso, XXXXpara encontrar alguns sinônimos no meu banco de dados de teste). Isso é semelhante à reescrita que você achou mais eficiente, mas evita a necessidade de declarar uma variável e usar duas consultas.

SELECT name, base_object_name 
FROM sys.synonyms 
WHERE schema_id = SCHEMA_ID(N'dbo')
ORDER BY name

Isso gera um plano como o seguinte:

insira a descrição da imagem aqui

Uma coisa muito interessante sobre o Filteroperador neste plano é que ele tem um predicado que executa uma has_access()verificação interna . Este filtro remove todos os objetos que a conta atual não tem permissões suficientes para ver. No entanto, essa verificação é interrompida (ou seja, é concluída muito mais rapidamente) se você for um membro da db_ownerfunção, o que pode explicar as diferenças de desempenho que você está vendo.

insira a descrição da imagem aqui

Aqui está o plano de consulta para sua consulta original. Observe que todos os sinônimos no banco de dados ( 1,126no meu caso, mas provavelmente muitos mais no seu caso) passam pelo has_access()filtro muito caro , mesmo que apenas 2sinônimos correspondam ao esquema. Usando a consulta simplificada acima, podemos garantir que isso has_access()seja invocado apenas para os sinônimos que correspondem à sua consulta, e não para todos os sinônimos no banco de dados.

insira a descrição da imagem aqui


Usando sys.dm_exec_query_profiles para explorar mais

Como Martin sugere, podemos confirmar que a verificação has_access () é um gargalo significativo usando o sys.dm_exec_query_profilesSQL Server 2014+. Se eu executar a seguinte consulta usando uma db_ownerconta em um banco de dados com ~ 700K objetos, a consulta será realizada ~500ms:

SELECT COUNT(*)
FROM sys.objects

Quando executada com uma conta que não é db_owner, essa mesma consulta leva cerca de oito minutos! Executando com o plano real e usando um procedimento p_queryProgress que escrevi para ajudar a analisar a sys.dm_exec_query_profilessaída com mais facilidade, podemos ver que quase todo o tempo de processamento é gasto no Filteroperador que está executando a has_access()verificação:

insira a descrição da imagem aqui

Geoff Patterson
fonte
Problema com TokenAndPermUserStore? Nesse caso, este artigo
Martin Smith
@ MartinSmith Muito interessante, eu não estava ciente das opções de configuração access check cache bucket counte access check cache quotaanteriormente. Vai ter que brincar um pouco com eles.
Geoff Patterson
Não tenho certeza de que esse cache seja relevante para o caso aqui. Só me lembro que estava causando problemas no passado.
Martin Smith
11
@MartinSmith Embora essas configurações não tenham impactado, há algo interessante acontecendo com o cache. Parece que a existência do cache é prejudicial. Por exemplo, se eu executar WHILE(1=1) BEGIN DBCC FREESYSTEMCACHE ('TokenAndPermUserStore') WAITFOR DELAY '00:00:05' ENDem um loop para sempre, a consulta é concluída em menos de 2 minutos versus 8 minutos normalmente.
Geoff Patterson
11
Obrigado a todos - a resposta da Microsoft faz eco dos comentários acima e uma reescrita de consulta é a melhor solução. Acontece que has_access () tem um curto-circuito no início para testar db_owner ou sysadmin e isso resultou na grande diferença de horário.
James
0

Se isso ainda estiver ativo - tivemos o mesmo problema -, parece que, se você é o dbo ou um administrador de sistema, qualquer acesso ao sys.objects (ou algo assim) - é instantâneo, sem verificação de objetos individuais.

se é um db_datareader humilde, ele deve verificar cada objeto por vez ... está oculto no plano de consulta, pois eles se comportam mais como funções do que visualizações / tabelas

o plano parece o mesmo, mas está fazendo coisas diferentes por trás do capô

Mike
fonte