ID en premier: c'est le champ le plus sélectif (c'est-à-dire le plus unique). Mais en étant un champ d'incrémentation automatique (ou aléatoire s'il utilise toujours des GUID), les données de chaque client sont réparties sur chaque table. Cela signifie qu'il y a des moments où un client a besoin de 100 lignes, et cela nécessite près de 100 pages de données lues à partir du disque (pas rapide) dans le pool de mémoire tampon (prenant plus d'espace que 10 pages de données). Cela augmente également les conflits sur les pages de données car il sera plus fréquent que plusieurs clients aient besoin de mettre à jour la même page de données.
Cependant, vous ne rencontrez généralement pas autant de problèmes de reniflage de paramètres / de plan mis en cache incorrect, car les statistiques sur les différentes valeurs d'ID sont assez cohérentes. Vous n'obtiendrez peut-être pas les plans les plus optimaux, mais vous aurez moins de chances d'en obtenir des horribles. Cette méthode sacrifie essentiellement (légèrement) les performances de tous les clients pour bénéficier de problèmes moins fréquents.
TenantID d'abord:Ce n'est pas du tout sélectif. Il peut y avoir très peu de variations sur 1 million de lignes si vous ne disposez que de 100 TenantID. Mais les statistiques de ces requêtes sont plus précises car SQL Server sait qu'une requête pour le locataire A retirera 500 000 lignes, mais que la même requête pour le locataire B n'est que de 50 lignes. C'est là que se situe le principal point douloureux. Cette méthode augmente considérablement les risques de problèmes de reniflage de paramètres lorsque la première exécution d'une procédure stockée concerne le locataire A et agit de manière appropriée en fonction de l'optimiseur de requête qui voit ces statistiques et sait qu'il doit être efficace pour obtenir 500 000 lignes. Mais lorsque le locataire B, avec seulement 50 lignes, s'exécute, ce plan d'exécution n'est plus approprié, et en fait, est tout à fait inapproprié. ET, étant donné que les données ne sont pas insérées dans l'ordre du champ de tête,
Cependant, pour que le premier TenantID exécute une procédure stockée, les performances doivent être meilleures que dans l'autre approche, car les données (au moins après la maintenance de l'index) seront organisées physiquement et logiquement de sorte que beaucoup moins de pages de données sont nécessaires pour satisfaire la requêtes. Cela signifie moins d'E / S physiques, moins de lectures logiques, moins de conflits entre les locataires pour les mêmes pages de données, moins d'espace gaspillé occupé dans le pool de tampons (d'où une amélioration de l'espérance de vie des pages), etc.
Il existe deux coûts principaux pour obtenir ces performances améliorées. Le premier n'est pas si difficile: vous devez faire une maintenance régulière de l'index pour contrer l'augmentation de la fragmentation. Le second est un peu moins amusant.
Afin de contrer l'augmentation des problèmes de détection de paramètres, vous devez séparer les plans d'exécution entre les locataires. L'approche simpliste consiste à utiliser WITH RECOMPILE
sur procs ou l' OPTION (RECOMPILE)
indice de requête, mais c'est un coup sur les performances qui pourrait effacer tous les gains réalisés en mettant en TenantID
premier. La méthode que j'ai trouvée la plus efficace consiste à utiliser Dynamic SQL paramétré via sp_executesql
. La raison d'avoir besoin de Dynamic SQL est de permettre la concaténation de TenantID dans le texte de la requête, tandis que tous les autres prédicats qui seraient normalement des paramètres sont toujours des paramètres. Par exemple, si vous recherchiez une commande particulière, vous feriez quelque chose comme:
DECLARE @GetOrderSQL NVARCHAR(MAX);
SET @GetOrderSQL = N'
SELECT ord.field1, ord.field2, etc.
FROM dbo.Orders ord
WHERE ord.TenantID = ' + CONVERT(NVARCHAR(10), @TenantID) + N'
AND ord.OrderID = @OrderID_dyn;
';
EXEC sp_executesql
@GetOrderSQL,
N'@OrderID_dyn INT',
@OrderID_dyn = @OrderID;
Cela a pour effet de créer un plan de requête réutilisable pour cet ID de locataire qui correspondra au volume de données de ce locataire particulier. Si ce même locataire A exécute à nouveau la procédure stockée pour un autre, @OrderID
il réutilisera ce plan de requête mis en cache. Un locataire différent exécutant la même procédure stockée générerait un texte de requête différent uniquement dans la valeur de l'ID de locataire, mais toute différence dans le texte de la requête suffit pour générer un plan différent. Et le plan généré pour le locataire B correspondra non seulement au volume de données pour le locataire B, mais il sera également réutilisable pour le locataire B pour différentes valeurs de @OrderID
(puisque ce prédicat est toujours paramétré).
Les inconvénients de cette approche sont:
- C'est un peu plus de travail que de simplement taper une simple requête (mais toutes les requêtes ne doivent pas nécessairement être Dynamic SQL, juste celles qui finissent par avoir le problème de reniflage des paramètres).
- Selon le nombre de clients hébergés sur un système, cela augmente la taille du cache de plan, car chaque requête nécessite désormais 1 plan par TenantID qui l'appelle. Ce n'est peut-être pas un problème, mais c'est au moins quelque chose à savoir.
Dynamic SQL rompt la chaîne de propriété, ce qui signifie que l'accès en lecture / écriture aux tables ne peut pas être supposé en ayant l' EXECUTE
autorisation sur la procédure stockée. La solution simple mais moins sécurisée consiste simplement à donner à l'utilisateur un accès direct aux tables. Ce n'est certainement pas l'idéal, mais c'est généralement le compromis rapide et facile. L'approche la plus sécurisée consiste à utiliser la sécurité basée sur les certificats. Cela signifie, créez un certificat, puis créez un utilisateur à partir de ce certificat, accordez à cet utilisateur les autorisations souhaitées (un utilisateur ou une connexion basé sur un certificat ne peut pas se connecter à SQL Server seul), puis signez les procédures stockées qui utilisent Dynamic SQL avec ce même certificat via ADD SIGNATURE .
Pour plus d'informations sur la signature de module et les certificats, veuillez consulter: ModuleSigning.Info
(ID, TenantID)
et que vous créez également un index non clusterisé(TenantID, ID)
, ou simplement(TenantID)
pour avoir des statistiques précises pour les requêtes qui traitent la plupart des lignes d'un locataire unique?