行方向データから1カラムへの変換をcase文の中で実現するSQL Server用クエリー

業務系システムのデータベースで、行方向に並んでいるデータを1カラムの文字列として取得したい場合がたまにある。しかも、それをcase文の中で実行できれば嬉しいことがある。case文の中にfetch nextなどは書けないので、あらかじめ本文実行前に別のクエリーでfetch nextで回して取得しておく方法もあるが、どうもスマートではない。

いろいろ探してみたらfor xml句とstuff関数を活用する方法が書かれていた。

[SQL Server Developer Center SQL Server Forums] T-SQL Equivalent For Group_Concat() Function

例題として以下にテーブルを挙げ、実際にクエリーを書いてみる。

GroupID | GroupName
--------|----------
  1     |  GroupA
  2     |  GroupB
  3     |  GroupC
  4     |  GroupD
  5     |  GroupE
GroupID | IndividualID | IndividualName
--------|--------------|---------------
  1     |      1       |     DataA
  2     |      2       |     DataB
  3     |      3       |     DataC
  0     |      4       |     DataD
  5     |      5       |     DataE
  0     |      6       |     DataF
  4     |      7       |     DataG
  1     |      8       |     DataH
  2     |      9       |     DataI
  0     |      10      |     DataJ
  0     |      11      |     DataK

Groupテーブルに、とあるグループのデータがあり、Individualテーブルにグループに所属する個別データがあるとする。IndividualテーブルのGroupIDは、グループに所属していれば所属するグループのIDが格納されており、所属していなければ0が格納されているとする。

クエリーの結果としてGroupNameとIndividualNameの複合データが欲しいが、グループに所属する個別データはGroupNameの後ろに括弧で入れて表示したいとする。

select
case (select COUNT(*) from Individual i where i.GroupID = g.GroupID)
when 0 then g.GroupName
else
	g.GroupName + '(' +
	STUFF((
		select ',' + i.IndividualName
		from Individual i
		where i.GroupID = g.GroupID
		order by i.IndividualName
		for xml path('')), 1, 1, '')
	+ ')'
end DataName
from [Group] g
union
select IndividualName DataName from Individual where GroupID = 0

このクエリーを実行すると、グループに所属していない個別データはそのまま表示され、グループに所属しているデータはグループ名の後ろに括弧付きで出力される。グループの一覧と個別の一覧はunion句を使って統合している。

DataName
-------------------
DataD
DataF
DataJ
DataK
GroupA(DataA,DataH)
GroupB(DataB,DataI)
GroupC(DataC)
GroupD(DataG)
GroupE(DataE)

括弧内の個別データはfor xml句を利用している。for xml句は結果をXML形式にして返してくれる。例えば、次のクエリーを実行すると、IndividualNameタグが付与された結果が返ってくる。

select IndividualName from Individual for xml path('')
<IndividualName>DataA</IndividualName><IndividualName>DataB</IndividualName><IndividualName>DataC</IndividualName><IndividualName>DataD</IndividualName><IndividualName>DataE</IndividualName><IndividualName>DataF</IndividualName><IndividualName>DataG</IndividualName><IndividualName>DataH</IndividualName><IndividualName>DataI</IndividualName><IndividualName>DataJ</IndividualName><IndividualName>DataK</IndividualName>

このクエリーのselect文のIndividualNameカラムの前に「’,’」を入れると名前のついていないカラムになりタグが消える。

select ',' + IndividualName from Individual for xml path('')
,DataA,DataB,DataC,DataD,DataE,DataF,DataG,DataH,DataI,DataJ,DataK

このデータの最初の「,」は不要なのでSTUFF関数で除去してあげるという仕組みだ。

少々ややこしいが、このテクニックを覚えておけば、例えばグループ化されたデータの一覧取得などでグループ内のデータがどうなっているか見たいという要望があった場合に、一覧を表示するプログラム側で変更を行わなくてもグループ名のカラムに中身のデータを表示することができる。

ちなみにグループ内のデータのいくつかを見たいという場合には、STUFF関数内のselect文にtop句をつけて件数を指定してあげればよい。

select
case (select COUNT(*) from Individual i where i.GroupID = g.GroupID)
when 0 then g.GroupName
else
	g.GroupName + '(' +
	STUFF((
		select top 1 ',' + i.IndividualName
		from Individual i
		where i.GroupID = g.GroupID
		order by i.IndividualName
		for xml path('')), 1, 1, '')
	+ '...' + CONVERT(nvarchar(max), (select COUNT(*) from Individual i where i.GroupID = g.GroupID)) + '件)'
end DataName
from [Group] g
union
select IndividualName DataName from Individual where GroupID = 0
DataName
-------------------
DataD
DataF
DataJ
DataK
GroupA(DataA...2件)
GroupB(DataB...2件)
GroupC(DataC...1件)
GroupD(DataG...1件)
GroupE(DataE...1件)

グループ内の取得件数は3件ぐらいが妥当だと思うが、例題なのでここでは1件としている。ちなみに1件の場合はfor xml句で一覧を1カラムで取得して、STUFF関数で先頭の「,」を除去する必要はなく、そのままselect top 1でOKである。

select
case (select COUNT(*) from Individual i where i.GroupID = g.GroupID)
when 0 then g.GroupName
else
	g.GroupName + '(' +
	(select top 1 i.IndividualName
	from Individual i
	where i.GroupID = g.GroupID
	order by i.IndividualName)
	+ '...' + CONVERT(nvarchar(max), (select COUNT(*) from Individual i where i.GroupID = g.GroupID)) + '件)'
end DataName
from [Group] g
union
select IndividualName DataName from Individual where GroupID = 0

Follow me!

Feedlyで新着記事をチェックしよう!

Feedlyでフォローしておけば、新着記事をチェックすることができます。ぜひ、この機会にFeedlyに追加しておきましょう。