La requête contre sys.schemas et sys.synonymes s'exécute très lentement pour un utilisateur

8

Scénario: SQL Server 2014 (v12.0.4100.1)

Le service .NET exécute cette requête:

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

... qui renvoie environ 6500 lignes, mais il arrive souvent à expiration après 3 minutes ou plus. Ce qui XXXXprécède n'est pas «dbo».

Si j'exécute cette requête dans SSMS en tant qu'utilisateurA, la requête retourne en moins d'une seconde.

Lorsqu'elle est exécutée en tant qu'utilisateurB (c'est ainsi que le service .NET se connecte), la requête prend 3 à 6 minutes et le CPU% à 25% (sur 4 cœurs) tout le temps.

UserA est une connexion de domaine dans le rôle sysadmin.

UserB est une connexion SQL avec:

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]

Je peux dupliquer cela dans SSMS en enveloppant le SQL ci-dessus dans un Execute as...Revertbloc, de sorte que le code .NET est hors de l'image.

Le plan d'exécution est identique. J'ai diff'ed le XML et il n'y a que des différences mineures (CompileTime, CompileCPU, CompileMemory).

Les statistiques d'E / S n'indiquent aucune lecture physique:

Table 'sysobjvalues'. Nombre de balayages 0, lectures logiques 19970, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Tableau «Fichier de travail». Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Tableau «Table de travail». Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Tableau «sysschobjs». Nombre de balayages 1, lectures logiques 9122, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Tableau 'sysclsobjs'. Nombre de balayages 0, lectures logiques 2, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.

L'état d'attente de XEvent (pour une requête d'environ 3 minutes) est le suivant:

+ --------------------- + ------------ + -------------- -------- + ------------------------------ + ---------- ------------------- +
| Type d'attente | Nombre d'attente | Temps d'attente total (ms) | Temps d'attente total des ressources (ms) | Temps d'attente total du signal (ms) |
+ --------------------- + ------------ + -------------- -------- + ------------------------------- + --------- -------------------- +
| SOS_SCHEDULER_YIELD | 37300 | 427 | 20 | 407 |
| NETWORK_IO | 5 | 26 | 26 | 0 |
| IO_COMPLETION | 3 | 1 | 1 | 0 |
+ --------------------- + ------------ + -------------- -------- + ------------------------------- + --------- -------------------- +

Si je réécris la requête (dans SSMS, je n'ai pas accès au code d'application) pour

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

puis UserB s'exécute à la même vitesse (rapide) que UserA.

Si j'ajoute db_ownerà UserB, alors, encore une fois, la requête s'exécute <1 sec.

Schéma créé via ce modèle:

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

Et {2} est, je crois, une liste de synonymes créés dans ce schéma.

Profil de requête à deux points de la requête:

entrez la description de l'image ici

entrez la description de l'image ici

J'ai ouvert un ticket avec Microsoft.

De plus, nous avons essayé d'ajouter UserB à db_owner, puis d' ingérer DENYtous les privilèges que nous connaissons et qui sont associés db_owner. Le résultat est une requête rapide. Soit nous avons raté quelque chose (tout à fait possible), soit il y a une vérification spéciale pour le db_ownerrôle.

James
la source

Réponses:

5

Vous voudrez peut-être réécrire votre requête comme suit (j'utilise dboplutôt que XXXXpour trouver des synonymes dans ma base de données de test). Ceci est similaire à la réécriture que vous avez trouvée plus efficace, mais évite d'avoir à déclarer une variable et à utiliser deux requêtes.

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

Cela donne un plan comme le suivant:

entrez la description de l'image ici

Une chose très intéressante à propos de l' Filteropérateur dans ce plan est qu'il a un prédicat qui effectue une has_access()vérification interne . Ce filtre supprime tous les objets que le compte actuel ne dispose pas des autorisations suffisantes pour voir. Cependant, cette vérification est court-circuitée (c'est-à-dire qu'elle se termine beaucoup plus rapidement) si vous êtes membre du db_ownerrôle, ce qui peut expliquer les différences de performances que vous voyez.

entrez la description de l'image ici

Voici le plan de requête pour votre requête d'origine. Notez que tous les synonymes de la base de données ( 1,126dans mon cas, mais probablement beaucoup d'autres dans votre cas) passent par le has_access()filtre très coûteux , même si seuls les 2synonymes correspondent au schéma. En utilisant la requête simplifiée ci-dessus, nous pouvons nous assurer qu'elle has_access()n'est invoquée que pour les synonymes qui correspondent à votre requête plutôt que pour tous les synonymes de la base de données.

entrez la description de l'image ici


Utilisation de sys.dm_exec_query_profiles pour explorer davantage

Comme Martin le suggère, nous pouvons confirmer que la vérification has_access () est un goulot d'étranglement important en utilisant sys.dm_exec_query_profilessur SQL Server 2014+. Si j'exécute la requête suivante en utilisant un db_ownercompte sur une base de données avec ~ 700K objets, la requête prend ~500ms:

SELECT COUNT(*)
FROM sys.objects

Lorsqu'elle est exécutée avec un compte qui n'est pas un db_owner, cette même requête prend environ huit minutes! En exécutant le plan réel et en utilisant une procédure p_queryProgress que j'ai écrite pour faciliter l'analyse de la sys.dm_exec_query_profilessortie, nous pouvons voir que presque tout le temps de traitement est consacré à l' Filteropérateur qui effectue la has_access()vérification:

entrez la description de l'image ici

Geoff Patterson
la source
Problème avec TokenAndPermUserStore? Dans ce cas, cet article de la base de connaissances pourrait aider support.microsoft.com/en-gb/kb/955644
Martin Smith
@MartinSmith Très intéressant, je ne connaissais pas les options de configuration access check cache bucket countet access check cache quotaauparavant. Faudra jouer un peu avec ceux-ci.
Geoff Patterson
Je ne suis pas certain que ce cache soit pertinent pour le cas ici. Je me souviens juste que cela a causé des problèmes dans le passé.
Martin Smith
1
@MartinSmith Bien que ces paramètres n'aient pas eu d'impact, il se passe quelque chose d'intéressant avec le cache. Il semble que l'existence du cache soit préjudiciable. Par exemple, si je tourne WHILE(1=1) BEGIN DBCC FREESYSTEMCACHE ('TokenAndPermUserStore') WAITFOR DELAY '00:00:05' ENDen boucle pour toujours, la requête se termine en moins de 2 minutes contre 8 minutes en général.
Geoff Patterson
1
Merci à tous - la réponse de Microsoft fait écho aux commentaires ci-dessus, et une réécriture de la requête est la meilleure solution. Il s'avère que has_access () a un court-circuit au début pour tester db_owner ou sysadmin et cela a entraîné une grande différence de temps.
James
0

Si cela est toujours en ligne - nous avons eu le même problème - il semble que si vous êtes le dbo ou un administrateur système, alors tout accès à sys.objects (ou quelque chose comme ça) - alors c'est instantané sans vérification contre les objets individuels.

si c'est un db_datareader modeste, il doit vérifier chaque objet à son tour ... il est caché dans le plan de requête car ceux-ci se comportent plus comme des fonctions que des vues / tables

le plan est le même, mais il fait des choses différentes derrière le capot

Mike
la source