SQL Row-Verkettung mit XML PATH und STUFF führt zu einem aggregierten SQL-Fehler

9

Ich versuche, zwei Tabellen abzufragen und Ergebnisse wie die folgenden zu erhalten:

Section    Names
shoes      AccountName1, AccountName2, AccountName3
books      AccountName1

Die Tabellen sind:

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');

Ich habe einige Fragen beantwortet, die besagten, dass "XML PATH" und "STUFF" verwendet werden sollen, um die Daten abzufragen und die gewünschten Ergebnisse zu erhalten, aber ich denke, es fehlt etwas. Ich habe die folgende Abfrage versucht und erhalte die Fehlermeldung:

Die Spalte 'a.AccountId' ist in der Auswahlliste ungültig, da sie weder in einer Aggregatfunktion noch in der GROUP BY-Klausel enthalten ist.

Ich habe es nicht in der SELECT-Klausel einer der Abfragen, aber ich gehe davon aus, dass der Fehler darin besteht, dass AccountId in Tabelle A nicht eindeutig ist.

Hier ist die Abfrage, die ich gerade versuche, um richtig zu arbeiten.

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
B. McCarthy
quelle

Antworten:

14

Entschuldigung, ich habe einen Schritt in der Beziehung verpasst. Versuchen Sie diese Version (obwohl Martins auch funktionieren wird ):

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;

Ein Ansatz, der ist mindestens so gut , aber manchmal besser, wird das Umschalten von DISTINCTzu 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;

Auf hoher Ebene gilt der Grund DISTINCTfür die gesamte Spaltenliste. Daher muss für alle Duplikate die Gesamtarbeit für jedes Duplikat ausgeführt werden, bevor es angewendet wird DISTINCT. Wenn Sie verwenden GROUP BY, können möglicherweise Duplikate entfernt werden, bevor Aggregationsarbeiten ausgeführt werden. Dieses Verhalten kann je nach Plan variieren, abhängig von einer Vielzahl von Faktoren, einschließlich Indizes, Planstrategie usw. Ein direkter Wechsel zu ist GROUP BYmöglicherweise nicht in allen Fällen möglich.

Auf jeden Fall habe ich beide Varianten in SentryOne Plan Explorer ausgeführt . Die Pläne unterscheiden sich in einigen geringfügigen, uninteressanten Punkten, aber die mit dem zugrunde liegenden Arbeitstisch verbundenen E / A sind aussagekräftig. Hier ist DISTINCT:

Geben Sie hier die Bildbeschreibung ein

Und hier ist GROUP BY:

Geben Sie hier die Bildbeschreibung ein

Wenn ich die Tabellen vergrößert habe (mehr als 14.000 Zeilen, die 24 potenziellen Werten zugeordnet sind), ist dieser Unterschied stärker ausgeprägt. DISTINCT::

Geben Sie hier die Bildbeschreibung ein

GROUP BY::

Geben Sie hier die Bildbeschreibung ein

In SQL Server 2017 können Sie Folgendes verwenden 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;

Die E / A hier ist fast nichts:

Geben Sie hier die Bildbeschreibung ein


Wenn Sie jedoch nicht mit SQL Server 2017 (oder der Azure SQL-Datenbank) arbeiten und diese nicht verwenden können STRING_AGG, muss ich eine Gutschrift geben, wenn eine Gutschrift fällig ist ... Die Antwort von Paul White unten enthält nur sehr wenige E / A-Vorgänge aus beiden FOR XML PATHoben genannten Lösungen.

Geben Sie hier die Bildbeschreibung ein


Weitere Verbesserungen aus diesen Beiträgen:

Siehe auch:

Aaron Bertrand
quelle
11

Ich dachte, ich würde eine Lösung mit XML versuchen.

SEDE Demo

Tabellen

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
);

Daten

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');

Beitreten und in XML konvertieren

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-Erstellungsabfrage

Das XML in der Variablen sieht folgendermaßen aus:

<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>

Abfrage

Die letzte Abfrage zerlegt das XML in Abschnitte und verkettet die Namen in jedem:

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);

Ergebnis

Ausgabe

Ausführungsplan

Ausführungsplan

Paul White 9
quelle
-3

Geben Sie hier die Bildbeschreibung ein

Demo-Tabelle

Geben Sie hier die Bildbeschreibung ein

Abteilungstabelle

Geben Sie hier die Bildbeschreibung ein

select 
    g.Department,
    t.numberofemp,
    t.totalslary,
    t.totalage,
    t.name 
from (
    select  
        count(*) numberofemp,
        tl.dtid,
        sum(salary) as totalslary,
        sum(age) as totalage,
        name=
            (stuff((
                select  DISTINCT  
                    ', '+ [name] 
                from  demo as d 
                inner join demodept as dt 
                    on d.dtid=dt.dtid 
                where dt.dtid=tl.dtid 
            for xml path(''),type).value('(./text())[1]','varchar(max)'),1,2,''))  
 -- from demo as d inner join demodept as dt on d.dtid=dt.dtid
    from Demo as tl
    group by tl.dtid) as t 
inner join  demodept as g 
    on  t.dtid=g.dtid

Ausgabe

Avadhesh Kumar
quelle
Willkommen bei DBA.SE! Sie haben Ihre Ausgabe vergessen, wie es scheint ...
Glorfindel