web-dev-qa-db-fra.com

Concaténation de lignes SQL avec XML PATH et STUFF donnant une erreur SQL agrégée

J'essaie d'interroger deux tables et d'obtenir des résultats comme celui-ci:

Section    Names
shoes      AccountName1, AccountName2, AccountName3
books      AccountName1

Les tableaux sont:

CREATE TABLE dbo.TableA(ID INT, Section varchar(64), AccountId varchar(64));

INSERT dbo.TableA(ID, Section, AccountId) VALUES
(1 ,'shoes','A1'),
(2 ,'shoes','A2'),
(3 ,'shoes','A3'),
(4 ,'books','A1');

CREATE TABLE dbo.TableB(AccountId varchar(20), Name varchar(64));

INSERT dbo.TableB(AccountId, Name) VALUES
('A1','AccountName1'),
('A2','AccountName2'),
('A3','AccountNAme3');

J'ai vu une réponse à quelques questions disant d'utiliser "XML PATH" et "STUFF" pour interroger les données pour obtenir les résultats que je recherche, mais je pense qu'il manque quelque chose. J'ai essayé la requête ci-dessous et j'obtiens le message d'erreur:

La colonne 'a.AccountId' n'est pas valide dans la liste de sélection car elle n'est contenue ni dans une fonction d'agrégation ni dans la clause GROUP BY.

Je ne l'ai pas dans la clause SELECT de l'une ou l'autre requête, mais je suppose que l'erreur est due au fait que AccountId n'est pas unique dans TableA.

Voici la requête que j'essaie actuellement de faire fonctionner correctement.

SELECT section, names= STUFF((
    SELECT ', ' + Name FROM TableB as b 
WHERE AccountId = b.AccountId
FOR XML PATH('')), 1, 1, '')
FROM TableA AS a
GROUP BY a.section
7
B.McCarthy

Désolé, j'ai raté une étape de la relation. Essayez cette version (bien que celle de Martin fonctionnera également ):

SELECT DISTINCT o.section, names= STUFF((
    SELECT ', ' + b.Name 
    FROM dbo.TableA AS a
    INNER JOIN dbo.TableB AS b
    ON a.AccountId = b.AccountId
    WHERE a.Section = o.Section
    FOR XML PATH, TYPE).value(N'.[1]', N'varchar(max)'), 1, 2, '')
FROM dbo.TableA AS o;

Une approche qui est au moins aussi bonne, mais parfois mieux, passe de DISTINCT à GROUP BY:

SELECT o.section, names= STUFF((
    SELECT ', ' + b.Name 
    FROM dbo.TableA AS a
    INNER JOIN dbo.TableB AS b
    ON a.AccountId = b.AccountId
    WHERE a.Section = o.Section
    FOR XML PATH, TYPE).value(N'.[1]', N'varchar(max)'), 1, 2, '')
FROM dbo.TableA AS o
GROUP BY o.section;

À un niveau élevé, la raison DISTINCT s'applique à la liste de colonnes entière. Par conséquent, pour tous les doublons, il doit effectuer le travail d'agrégation pour chaque doublon avant d'appliquer DISTINCT. Si tu utilises GROUP BY alors il peut potentiellement supprimer les doublons avant faisant tout le travail d'agrégation. Ce comportement peut varier selon le plan en fonction de divers facteurs, notamment les index, la stratégie du plan, etc. Et un basculement direct vers GROUP BY peut ne pas être possible dans tous les cas.

Dans tous les cas, j'ai exécuté ces deux variantes dans SentryOne Plan Explorer . Les plans sont différents de quelques manières mineures et inintéressantes, mais les E/S impliquées dans la table de travail sous-jacente sont révélatrices. Voici DISTINCT:

enter image description here

Et voici GROUP BY:

enter image description here

Lorsque j'ai agrandi les tableaux (plus de 14 000 lignes correspondant à 24 valeurs potentielles), cette différence est plus prononcée. DISTINCT:

enter image description here

GROUP BY:

enter image description here

Dans SQL Server 2017, vous pouvez utiliser STRING_AGG:

SELECT a.section, STRING_AGG(b.Name, ', ')
    FROM dbo.TableA AS a
    INNER JOIN dbo.TableB AS b
    ON a.AccountId = b.AccountId
    WHERE a.Section = a.Section
    GROUP BY a.section;

L'E/S ici n'est presque rien:

enter image description here


Mais, si vous n'êtes pas sur SQL Server 2017 (ou Azure SQL Database) et ne pouvez pas utiliser STRING_AGG, Je dois donner du crédit là où le crédit est dû ... La réponse de Paul White ci-dessous a très peu d'E/S et lance le pantalon des deux FOR XML PATH solutions ci-dessus.

enter image description here


Autres améliorations de ces messages:

Regarde aussi:

12
Aaron Bertrand

J'ai pensé que j'essaierais une solution utilisant XML.

Démo SEDE

Les tables

DECLARE @TableA AS table
(
    ID integer PRIMARY KEY,
    Section varchar(10) NOT NULL,
    AccountID char(2) NOT NULL
);

DECLARE @TableB AS table
(
    AccountID char(2) PRIMARY KEY,
    Name varchar(20) NOT NULL
);

Les données

INSERT @TableA
    (ID, Section, AccountID)
VALUES
    (1, 'shoes', 'A1'),
    (2, 'shoes', 'A2'),
    (3, 'shoes', 'A3'),
    (4, 'books', 'A1');

INSERT @TableB
    (AccountID, Name)
VALUES
    ('A1', 'AccountName1'),
    ('A2', 'AccountName2'),
    ('A3', 'AccountName3');

Rejoignez et convertissez en XML

DECLARE @x xml =
(
    SELECT
        TA.Section,
        CA.Name
    FROM @TableA AS TA
    JOIN @TableB AS TB
        ON TB.AccountID = TA.AccountID
    CROSS APPLY
    (
        VALUES(',' + TB.Name)
    ) AS CA (Name)
    ORDER BY TA.Section
    FOR XML AUTO, TYPE, ELEMENTS, ROOT ('Root')
);

XML creation query

Le XML de la variable ressemble à ceci:

<Root>
  <TA>
    <Section>shoes</Section>
    <CA>
      <Name>,AccountName1</Name>
    </CA>
    <CA>
      <Name>,AccountName2</Name>
    </CA>
    <CA>
      <Name>,AccountName3</Name>
    </CA>
  </TA>
  <TA>
    <Section>books</Section>
    <CA>
      <Name>,AccountName1</Name>
    </CA>
  </TA>
</Root>

Requete

La requête finale déchiquette le XML en sections et concatène les noms dans chacune:

SELECT
    Section = 
        N.n.value('(./Section/text())[1]', 'varchar(10)'),
    Names = 
        STUFF
        (
            -- Consecutive text nodes collapse
            N.n.query('./CA/Name/text()')
            .value('./text()[1]', 'varchar(8000)'), 
            1, 1, ''
        )
-- Shred per section
FROM @x.nodes('Root/TA') AS N (n);

Résultat

Output

Plan d'exécution

Execution plan

10
Paul White 9