web-dev-qa-db-fra.com

Refactoring de performance: essayer d'éviter la numérisation de la table

J'ai un processus contenant une requête qui rejoint quelques tables et j'ai des problèmes de performance.

La table principale (qui est une énorme table) a un PK et quelques index NC.

CREATE TABLE [dbo].[TableA]
(
    [TableAID] [bigint] NOT NULL,
    [UserID] [int] NOT NULL,
    [IP1] [tinyint] NOT NULL,
    [IP2] [tinyint] NOT NULL,
    [IP3] [tinyint] NOT NULL,
    [IP4] [tinyint] NOT NULL
    CONSTRAINT [PK_TableA] 
        PRIMARY KEY CLUSTERED  ([TableAID] ASC)
) ON [PRIMARY]


CREATE NONCLUSTERED INDEX [idx_1] ON [dbo].[TableA] 
(
    [UserID] ASC
)


CREATE NONCLUSTERED INDEX [idx_2] ON [dbo].[TableA] 
(
    [IP1] ASC,
    [IP2] ASC,
    [IP3] ASC,
    [IP4] ASC
)

C'est la requête avec les pauvres perf:

SELECT DISTINCT a.UserID, a.IP1, a.IP2, a.IP3, a.IP4
    FROM [dbo].[TableA] a WITH (NOLOCK) 
    JOIN [dbo].[TableB] b WITH (NOLOCK) ON b.UserID = a.UserID 
    JOIN [dbo].[Tablec] c WITH (NOLOCK) ON b.CountryID = c.CountryID
    JOIN ( 
        SELECT 
            IP1, IP2, IP3, IP4          
            from 
            @IPs 
            ) as ip ON 
                ((ip.IP1 is NULL) OR (ip.IP1=a.IP1)) AND 
                ((ip.IP2 is NULL) OR (ip.IP2=a.IP2)) AND
                ((ip.IP3 is NULL) OR (ip.IP3=a.IP3)) AND
                ((ip.IP4 is NULL) OR (ip.IP4=a.IP4)) 

La définition de @IPs tableau:

DECLARE @IPs TABLE (
    IP1 int,
    IP2 int,
    IP3 int,
    IP4 int
    )


INSERT INTO @IPs(IP1,IP2,IP3,IP4)
SELECT      T.v.value('(IP1/node())[1]', 'int'),
            T.v.value('(IP2/node())[1]', 'int'),
            T.v.value('(IP3/node())[1]', 'int'),
            T.v.value('(IP4/node())[1]', 'int')
FROM @IPAddresses.nodes('//IPAddresses/IPAddress') T(v)

@IPAddresses est xml. Je viens de découvrir que XML pourrait envoyer plus d'IP, donc cela signifie plus d'une ligne de la table IPS.

La question est le nombre de lectures sur le tassa. Même si j'ai un index NC pour les colonnes IP, la condition de jointure forçait une analyse de table ...

Comment puis-je améliorer la performance? Comment puis-je refactoriser cette table/requête?

Je pense toujours s'il y a un moyen plus simple et meilleur de réécrire ce code:

SELECT 
        IP1, IP2, IP3, IP4          
        from 
        @IPs 
        ) as ip ON 
            ((ip.IP1 is NULL) OR (ip.IP1=a.IP1)) AND 
            ((ip.IP2 is NULL) OR (ip.IP2=a.IP2)) AND
            ((ip.IP3 is NULL) OR (ip.IP3=a.IP3)) AND
            ((ip.IP4 is NULL) OR (ip.IP4=a.IP4))

... au cas où nous aurons plus d'IP.

5
Cristina Isăilă

Il y a quelques points qui font de cela difficile. Si vous n'êtes pas prudent, les contrôles NULL peuvent empêcher la recherche d'index. En outre, lorsque les colonnes sont NULL, vous ne pouvez évidemment pas rechercher contre eux. Donc si IP1 est NULL alors l'index de quatre colonnes idx_2 ne sera pas très utile. Il ne semble pas vraiment être possible de définir un indice qui sera sélectif pour une combinaison de NULL variables. En outre, SQL Server ne peut pas poursuivre une demande d'index après qu'il recherche sur un prédicat des inégalités:

De la même manière, si nous avons un index sur deux colonnes, nous ne pouvons utiliser l'index que pour satisfaire un prédicat sur la deuxième colonne si nous avons un prédicat d'égalité sur la première colonne.

Cela signifie que les astuces qui utilisent les limites du type de données TINYINT ne sont pas susceptibles d'être efficaces, telles que la suivante:

a.IP1 >= NULLIF(ip.IP1, 0) AND a.IP1 <= NULLIF(ip.IP1, 255)

En plus de cela, la stratégie que j'utilise semble fonctionner beaucoup mieux avec le nouvel estimateur de cardinalité introduit dans SQL Server 2014 et votre question est étiquetée avec SQL Server 2008.

Je vous recommande vivement de séparer la variable de table en rangées et de traiter une ligne à la fois. Ceci est principalement pour gérer les valeurs NULL et parce que vos commentaires ont laissé entendre que la plupart du temps, vous obtenez seulement une ligne. Tant que la performance est assez bonne pour une rangée et que vous n'avez pas trop de rangées, cela devrait aller. Si cela n'est pas acceptable, vous pouvez peut-être vérifier si l'une des lignes de la table TEMP (n'utilisez pas une variable de table) est NULL et branche votre code dans ce cas.

Avec tout cela, il semble que cela semble être possible d'obtenir de très bonnes performances sans balayage d'index en cluster aussi longtemps que au plus deux des pièces d'adresse IP ne sont pas NULL. Lorsque trois d'entre eux sont NULL vous récupérez la majeure partie de la table et à ce stade, il est probablement logique de faire un scan à index en cluster.

Vous trouverez ci-dessous les données que je me suis moquée de tester diverses solutions. J'ai généré 100 millions d'adresses IP en cueillant au hasard un entier compris entre 0 et 255 pour chaque pièce. Je sais que la distribution d'adresse IP dans la vie réelle n'est pas tout à fait aléatoire mais n'a pas eu le moyen de générer de meilleures données.

CREATE TABLE [dbo].[TableA](
    [TableAID] [bigint] NOT NULL,
    [UserID] [int] NOT NULL,
    [IP1] [tinyint] NOT NULL,
    [IP2] [tinyint] NOT NULL,
    [IP3] [tinyint] NOT NULL,
    [IP4] [tinyint] NOT NULL
CONSTRAINT [PK_TableA] PRIMARY KEY CLUSTERED 
( [TableAID] ASC )
);

-- insert 100 million random IP addresses
INSERT INTO [dbo].[TableA] WITH (TABLOCK)
SELECT TOP (100000000)
      ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    , ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 10000
    , ABS(BINARY_CHECKSUM(NEWID()) % 256)
    , ABS(BINARY_CHECKSUM(NEWID()) % 256)
    , ABS(BINARY_CHECKSUM(NEWID()) % 256)
    , ABS(BINARY_CHECKSUM(NEWID()) % 256)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3;

CREATE TABLE [dbo].[TableB] (
    [UserID] [int] NOT NULL,
    FILLER VARCHAR(100),
    PRIMARY KEY (UserId)
);

-- insert 10k users
INSERT INTO [dbo].[TableB] WITH (TABLOCK)
SELECT TOP (10000) -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

Remarquez comment je n'ai pas créé l'un des index non clusters que vous avez déjà. Au lieu de cela, je créerai un index pour chaque pièce IP:

CREATE NONCLUSTERED INDEX [idx_IP1] ON [dbo].[TableA] ([IP1] ASC);
CREATE NONCLUSTERED INDEX [idx_IP2] ON [dbo].[TableA] ([IP2] ASC);
CREATE NONCLUSTERED INDEX [idx_IP3] ON [dbo].[TableA] ([IP3] ASC);
CREATE NONCLUSTERED INDEX [idx_IP4] ON [dbo].[TableA] ([IP4] ASC);

L'espace requis pour les index n'est pas insignifiant. Voici la comparaison:

╔═══════════╦═════════════╗
║ IndexName ║ IndexSizeKB ║
╠═══════════╬═════════════╣
║ idx_1     ║     1786488 ║
║ idx_2     ║     1786480 ║
║ idx_IP1   ║     1487616 ║
║ idx_IP2   ║     1487616 ║
║ idx_IP3   ║     1487632 ║
║ idx_IP4   ║     1487608 ║
║ PK_TableA ║     2482056 ║
╚═══════════╩═════════════╝

Si nécessaire, vous pouvez peser les avantages et les inconvénients d'utiliser la ligne ou la compression de page pour réduire la taille des index. Cependant, si vous ne savez pas quelles pièces IP seront NULL et vous devez éviter une analyse d'index en cluster, je ne vois pas une meilleure alternative aux quatre index. La stratégie que je vais utiliser s'appelle une jointure d'index. L'index non clustered comprend la clé en cluster TableAID qui permet de rejoindre les index ensemble. Chaque indice devrait avoir une sélectivité d'environ 0,4% et il devrait être relativement peu coûteux de trouver ces lignes avec une recherche indicielle non clustée. Ensemble ensemble tous les index devraient considérablement réduire le jeu de résultats et à ce stade que vous pouvez effectuer en clustered Index recherche contre la table pour obtenir les autres valeurs de colonne que vous avez besoin telles que UserID.

Voici la requête:

DECLARE @ip1 TINYINT = ?;
DECLARE @ip2 TINYINT = ?;
DECLARE @ip3 TINYINT = ?;
DECLARE @ip4 TINYINT = ?;

SELECT DISTINCT a.UserID, a.IP1, a.IP2, a.IP3, a.IP4
FROM [dbo].[TableA] a 
JOIN [dbo].[TableB] b ON b.UserID = a.UserID 
WHERE
((@ip1 is NULL) OR (@ip1=a.IP1)) AND 
((@ip2 is NULL) OR (@ip2=a.IP2)) AND
((@ip3 is NULL) OR (@ip3=a.IP3)) AND
((@ip4 is NULL) OR (@ip4=a.IP4)) 
OPTION (RECOMPILE, QUERYTRACEON 9481);

Avec le RECOMPILE indice que je tire parti de l'optimisation optimisation de l'intégration des paramètres . Cette optimisation est uniquement disponible avec un certain service pack (SP4?) Alors assurez-vous que vous êtes corrigé. L'optimiseur de requête est capable de diviser l'accès à la table unique sur TableA dans une jointure d'index si elle voit l'ajustement. Notez que le plan estimé est très probablement trompeur ici. Vous voulez faire attention au plan réel.

Le QUERYTRACEON 9481 L'indice ne doit pas être inclus dans votre version de la requête. J'utilise cela pour forcer SQL Server à utiliser le The Legacy CE qui n'est nécessaire que parce que je teste contre SQL Server 2016.

Exécutons quelques tests. Avec les paramètres suivants:

DECLARE @ip1 TINYINT = 1;
DECLARE @ip2 TINYINT = 102;
DECLARE @ip3 TINYINT = 234;
DECLARE @ip4 TINYINT = 172;

Je reçois la fusion des jointures et une boucle se joint.

no null query

Avec les paramètres suivants:

DECLARE @ip1 TINYINT = NULL;
DECLARE @ip2 TINYINT = 102;
DECLARE @ip3 TINYINT = 234;
DECLARE @ip4 TINYINT = 172;

Je reçois un plan très similaire, sauf que l'index sur IP1 n'est pas utilisé dans la requête. La requête termine toujours dans environ 125 ms.

Avec les paramètres suivants:

DECLARE @ip1 TINYINT = 88;
DECLARE @ip2 TINYINT = NULL;
DECLARE @ip3 TINYINT = NULL;
DECLARE @ip4 TINYINT = NULL;

La requête se termine dans environ 5 secondes. Je reçois une jointure de hasch avec une analyse d'index en cluster. Ce n'est pas nécessairement une mauvaise chose mais peut être évité avec certains efforts (voir le paragraphe suivant):

3 NULL plan

Au cas où il est nécessaire de forcer efficacement des jointures d'index (SQL Server 2008 a peut-être disparu une optimisation que je profite de), cela peut être fait, mais c'est beaucoup plus compliqué .

9
Joe Obbish

En ce qui concerne votre problème de performance - en fonction de la taille de la variable de tableau à l'aide de #TReCtable, devrait plutôt être un meilleur choix. L'optimiseur de requête attend les variables de table pour toujours renvoyer une ligne (100 dans SQL Server 2016), elle crée donc éventuellement une jointure de boucle imbriquée, ce qui ajoute des frais généraux supplémentaires. Avec la table Temp, vous avez des statistiques valides et des estimations de cardinalité = meilleur plan.

De plus, vous ne retournez aucune ligne de la variable de table, que je suppose que c'est uniquement à des fins de validation, dans ce cas, je changerais la requête comme suit:

CREATE table #IPs  (IP1 tinyint,IP2 tinyint, IP3 tinyint, IP4 tinyint)
insert into #IPs values (1,2,3,4),(null,2,3,null)
SELECT  a.UserID, a.IP1, a.IP2, a.IP3, a.IP4
    FROM [dbo].[TableA] a WITH (NOLOCK) 
   WHERE EXISTS (Select 1 from #IPs as b where a.IP1 = b.IP1 or a.IP2 = b.IP2 OR a.IP3 = b.IP3 OR a.IP4 = b.IP4) 

J'utiliserais EXISTS au lieu de rejoindre s'il est à des fins de validation. Il vous évitera également de la sélection de valeurs distinctes, ce qui pourrait ajouter une surcharge sur TEMPDB (table de travail dans vos statistiques), les trier et en sélectionner une de chacun.

Index

Celui que vous avez créé, dont 4 colonnes ne fonctionnera pas car votre requête utilise ces colonnes comme optionnel, IP1 ou IP2 ou IP3, puis il faut une recherche pour obtenir d'autres colonnes restantes que vous avez demandées. Par conséquent, les estimations de la requête Optimizer. qu'il est plus facile de rechercher toute la table à la place.

Ce que vous pouvez faire, c'est modifier l'index non clusterisé et inclure la colonne ID utilisateur comme suit:

CREATE NONCLUSTERED INDEX [idx_2] ON [dbo].[TableA] 
(
    [IP1] ASC,
    [IP2] ASC,
    [IP3] ASC,
    [IP4] ASC
) INCLUDE ([UserID])
3
S4V1N