web-dev-qa-db-ja.com

XML PATHとSTUFFを使用したSQL行の連結により、集計SQLエラーが発生する

2つのテーブルをクエリして、次のような結果を取得しようとしています。

Section    Names
shoes      AccountName1, AccountName2, AccountName3
books      AccountName1

テーブルは次のとおりです。

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

「XML PATH」と「STUFF」を使用してデータをクエリし、探している結果を取得するようにといういくつかの質問に答えましたが、不足しているものがあると思います。以下のクエリを試したところ、エラーメッセージが表示されました。

列 'a.AccountId'は、集約関数にもGROUP BY句にも含まれていないため、選択リストでは無効です。

どちらのクエリのSELECT句にもありませんが、エラーはTableIdでAccountIdが一意でないためと考えられます。

これは、私が現在正しく動作するようにしようとしているクエリです。

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

申し訳ありませんが、関係の一歩を逃しました。このバージョンを試してください(ただし、 Martinも同様に機能します )。

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;

少なくとも同じように、時にはより良いアプローチは、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;

高レベルでは、理由DISTINCTは列リスト全体に適用されます。したがって、重複の場合は、DISTINCTを適用する前に、重複ごとに集計作業を実行する必要があります。 GROUP BYを使用すると、重複を削除する可能性がありますbefore集計作業のいずれかを実行します。この動作は、インデックス、プラン戦略などのさまざまな要因に応じて、プランによって異なる場合があります。また、すべての場合において、GROUP BYへの直接切り替えができない場合があります。

いずれにせよ、私はこれら両方のバリエーションを SentryOne Plan Explorer で実行しました。計画はいくつかのマイナーで興味のない方法で異なりますが、基礎となる作業テーブルに関連するI/Oが示しています。これがDISTINCTです:

enter image description here

そして、これがGROUP BYです。

enter image description here

テーブルを大きくした場合(14,000以上の行が24の潜在的な値にマッピングされる)、この違いはより顕著になります。 DISTINCT

enter image description here

GROUP BY

enter image description here

SQL Server 2017では、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;

ここのI/Oはほとんど何もありません。

enter image description here


ただし、SQL Server 2017(またはAzure SQLデータベース)を使用しておらず、STRING_AGGを使用できない場合は、 クレジットを提供する必要がありますdue...以下のPaul Whiteの回答 はI/Oがほとんどなく、上記のFOR XML PATHソリューションの両方からズボンを蹴飛ばします。

enter image description here


これらの投稿からの他の機能強化:

こちらもご覧ください:

12
Aaron Bertrand

XMLを使用した解決策を試してみようと思いました。

SEDEデモ

テーブル

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

データ

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

結合して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

変数のXMLは次のようになります。

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

クエリ

最後のクエリは、XMLをセクションに細断し、それぞれの名前を連結します。

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

結果

Output

実行計画

Execution plan

10
Paul White 9