web-dev-qa-db-fra.com

Un moyen efficace de gérer plusieurs instructions CASE dans SELECT

Je lance un rapport dans lequel j'ai une situation où, sur la base d'une valeur de colonne qui est essentiellement une clé ou un identifiant, j'ai besoin de récupérer la valeur correspondante dans la table des identifiants de mappage. Quelque chose comme ci-dessous:

    SELECT
     (case when [column1='A'] then (select value from Table1) 
           when [column1='B'] then (select value from Table2) 
           when [column1='C'] then (select value from Table3)

           and so on uptil 35 more 'when' conditions ...

  ELSE column1 end) Value 
from Table1

Plus précisément:

SELECT 
(case when [A.column1='1'] 
then (select value from B where B.clientId=100 and A.column1=B.Id) 
when [A.column1='2'] 
then (select value from C where C.clientId=100 and A.column1=C.Id) 
when [A.column1='3'] 
then (select value from D where D.clientId=100 and A.column1=D.Id) 
...
and so on uptil 30 more 'when' conditions 
...
ELSE column1 end) 
FROM A

Dans les tableaux B, C, D..so on, nous conservons des données pour tous les clients. Chaque client a un ClientId spécifique et ces tables B, C, D etc. ont un index en place sur les colonnes Id et ClientId.

Existe-t-il un moyen efficace de gérer cela dans SQL Server?

3
Abhi

Je suppose que vous disposez des index appropriés sur les tables des sous-requêtes. J'ai simulé des données de test rapide et mis 10 millions de lignes dans la table A. Je n'étais pas un jeu pour créer 30 tables, donc je viens d'en créer 3 pour l'expression CASE. Je pense que 3 suffisent pour montrer les principes généraux.

DROP TABLE IF EXISTS dbo.B;
CREATE TABLE dbo.B (
    ClientID INT NOT NULL,
    Id VARCHAR(20) NOT NULL,
    [Value] VARCHAR(100),
    PRIMARY KEY (ClientID, Id)
);

INSERT INTO B VALUES (100, '1', 'TABLE B'); 

DROP TABLE IF EXISTS dbo.C;
CREATE TABLE dbo.C (
    ClientID INT NOT NULL,
    Id VARCHAR(20) NOT NULL,
    [Value] VARCHAR(100),
    PRIMARY KEY (ClientID, Id)
);

INSERT INTO C VALUES (100, '2', 'TABLE C'); 

DROP TABLE IF EXISTS dbo.D;
CREATE TABLE dbo.D (
    ClientID INT NOT NULL,
    Id VARCHAR(20) NOT NULL,
    [Value] VARCHAR(100),
    PRIMARY KEY (ClientID, Id)
);

INSERT INTO D VALUES (100, '3', 'TABLE D'); 


DROP TABLE IF EXISTS dbo.A;
CREATE TABLE dbo.A (
    column1 VARCHAR(20) NOT NULL
);

INSERT INTO dbo.A WITH (TABLOCK)
SELECT CAST(1 + t.RN % 3 AS VARCHAR(20))
FROM
(
    SELECT TOP (5000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t;

J'ai désactivé les jeux de résultats et exécuté la requête suivante dans SSMS:

SELECT A.column1
FROM A;

Cela a pris environ 0,723 seconde. Je fais des tests assez non scientifiques parce que je ne sais rien de vos données. Dans tous les cas, avec des requêtes en série, nous ne pouvons pas espérer un meilleur résultat que 0,7 seconde. C'est notre référence.

Le moyen le plus efficace d'écrire cette requête est sans jointure. La clé est que l'expression CASE ne retournera jamais 3 (ou 30) valeurs uniques que si elle trouve une correspondance. Vous pouvez enregistrer les résultats dans des variables locales et simplement les utiliser dans la requête. La requête ci-dessous se termine en environ 1,044 secondes:

DECLARE @B_VALUE VARCHAR(100) = (select value from B where B.clientId=100 and B.Id = '1');
DECLARE @C_VALUE VARCHAR(100) = (select value from C where C.clientId=100 and C.Id = '2');
DECLARE @D_VALUE VARCHAR(100) = (select value from D where D.clientId=100 and D.Id = '3');

SELECT 
(case when A.column1='1' then @B_VALUE
      when A.column1='2' then @C_VALUE
      when A.column1='3' then @D_VALUE
      -- omitted other columns
else column1 end) 
FROM A;

Le plan est très simple:

enter image description here

Pour une autre option, vous pouvez écrire la requête avec des jointures (où nous pouvons réécrire l'expression CASE sous une forme plus compacte, en utilisant COALESCE(). Cela s'est terminé en environ 2,314 secondes:

SELECT 
    COALESCE(B.column1, C.column1, D.column1, -- omitted other columns  
             A.column1)
    -- (case A.column1
    --    when '1' then B.value
    --    when '2' then C.value
    --    when '3' then D.value
    --    -- omitted other columns
    -- else A.column1 end) 
FROM A
LEFT JOIN B ON B.clientId=100 and B.Id = '1'
LEFT JOIN C ON C.clientId=100 and C.Id = '2'
LEFT JOIN D ON D.clientId=100 and D.Id = '3';

Voici le plan:

enter image description here

Vous pouvez obtenir un plan d'exécution et un plan de requête presque identiques en écrivant la requête comme ceci:

SELECT 
(case A.column1
   when '1' then (select value from B where B.clientId=100 and '1'=B.Id) 
   when '2' then (select value from C where C.clientId=100 and '2'=C.Id) 
   when '3' then (select value from D where D.clientId=100 and '3'=D.Id) 
-- omitted other columns
else column1 end) 
FROM A;

La requête d'origine dans la question a un problème: SQL Server effectue un tri inutile avant la jointure de la boucle imbriquée. Cette requête se termine en environ 5,838 secondes sur ma machine.

enter image description here

L'indicateur de trace 8690 élimine le tri ainsi que les bobines de table. La requête s'exécute en environ 7.479 secondes avec l'indicateur de trace 8690, donc je suppose que les spools sont utiles pour cette requête.

6
Joe Obbish

S'ils sont tous différents, ce peut être votre meilleur scénario. SAUF les valeurs de Table1.column1 peuvent être répétées (plusieurs lignes = '1', etc.). Dans ce cas, vous souhaiterez peut-être passer des sous-requêtes aux jointures.

SELECT CASE 
    WHEN A.column1='1' THEN B.value
    WHEN A.column1='2' THEN C.value
    WHEN A.column1='3' THEN D.value
    and so on uptil 30 more 'when' conditions 
    ELSE A.column1
    END 
FROM A
    LEFT JOIN B ON A.column1='1' AND A.column1=B.Id AND B.clientId=100
    LEFT JOIN C ON A.column1='2' AND A.column1=C.Id AND C.clientId=100
    LEFT JOIN D ON A.column1='2' AND A.column1=D.Id AND D.clientId=100
4
indiri