it-swarm.com.de

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

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

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

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 mindestens genauso gut, aber manchmal besser ist, ist der Wechsel von DISTINCT zu 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 DISTINCT für die gesamte Spaltenliste. Daher muss für alle Duplikate die Gesamtarbeit für jedes Duplikat ausgeführt werden, bevor DISTINCT angewendet wird. Wenn Sie GROUP BY Verwenden, können möglicherweise Duplikate entfernt werden vor, die eine der Aggregationsarbeiten ausführen. Dieses Verhalten kann je nach Plan variieren, abhängig von einer Vielzahl von Faktoren wie Indizes, Planstrategie usw. Ein direkter Wechsel zu GROUP BY Ist möglicherweise nicht in allen Fällen möglich.

Auf jeden Fall habe ich beide Variationen 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:

(enter image description here

Und hier ist GROUP BY:

(enter image description here

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:

(enter image description here

GROUP BY:

(enter image description here

In SQL Server 2017 können Sie STRING_AGG Verwenden:

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:

(enter image description here


Wenn Sie sich jedoch nicht in SQL Server 2017 (oder in der Azure SQL-Datenbank) befinden und STRING_AGG Nicht verwenden können, muss ich Kredit geben, wo Kredit ist fällig ... Paul Whites Antwort unten hat sehr wenig E/A und macht beide oben genannten FOR XML PATH - Lösungen fertig.

(enter image description here


Weitere Verbesserungen aus diesen Beiträgen:

Siehe auch:

12
Aaron Bertrand

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

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 den einzelnen Abschnitten:

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

(Output

Ausführungsplan

(Execution plan

10
Paul White 9