次のようなテーブルがあります。
RECIPE VERSION_ID INGREDIENT PERCENTAGE
4000 100 Ing_1 23,0
4000 100 Ing_100 0,1
4000 200 Ing_1 20,0
4000 200 Ing_100 0,7
4000 300 Ing_1 22,3
4000 300 Ing_100 0,9
4001 900 Ing_1 8,3
4001 900 Ing_100 72,4
4001 901 Ing_1 9,3
4001 901 Ing_100 70,5
5012 871 Ing_1 45,1
5012 871 Ing_100 0,9
5012 877 Ing_1 47,2
5012 877 Ing_100 0,8
5012 879 Ing_1 46,6
5012 879 Ing_100 0,9
5012 880 Ing_1 43,6
5012 880 Ing_100 1,2
レシピ/バージョンごとに100の成分があります。この表のデータを次のように表示したいと思います。
RECIPE INGREDIENT_Vxxx PERCENTAGE_Vxxx INGREDIENT_Vyyy INGREDIENT_Vyyy (ETC)
4000 Ing_1 23,0 Ing_1 20,0
4000 Ing_100 0,1 Ing_100 0,7
異なるバージョンのレシピでは、食材を削除または追加できるため、レシピごとにバージョンごとに食材とパーセンテージの両方を表示したいと思います。また、レシピによってバージョン数が異なるという難しさもあります。
これがまったく可能か、それともどこから始めればよいのかさえわかりません。たぶんPIVOT
関数と一緒に?
誰か私を正しい方向に向けてもらえますか?
ここでの問題は、主にスコーピングの問題であると思われます。要件が十分に定義されていないため、この問題を解決するのが難しい可能性があります。説明とサンプルデータが提供されているため、少なくとも3つの部分的なソリューションがあり、特定のユースケースには適用できない場合があります。テストデータを次のように設定して、
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'Recipe'
AND type = 'U' )
BEGIN
--DROP TABLE dbo.Recipe;
CREATE TABLE dbo.Recipe
(
Recipe INTEGER NOT NULL,
VersionID INTEGER NOT NULL,
Ingredient VARCHAR( 8 ) NOT NULL,
Percentage DECIMAL( 5, 2 )
);
INSERT INTO dbo.Recipe ( Recipe, VersionID, Ingredient, Percentage )
SELECT 4000, 100, 'Ing_1', 23.0
UNION ALL SELECT 4000, 100, 'Ing_100', 0.1
UNION ALL SELECT 4000, 200, 'Ing_1', 20.0
UNION ALL SELECT 4000, 200, 'Ing_100', 0.7
UNION ALL SELECT 4000, 300, 'Ing_1', 22.3
UNION ALL SELECT 4000, 300, 'Ing_100', 0.9
UNION ALL SELECT 4001, 900, 'Ing_1', 8.3
UNION ALL SELECT 4001, 900, 'Ing_100', 72.4
UNION ALL SELECT 4001, 901, 'Ing_1', 9.3
UNION ALL SELECT 4001, 901, 'Ing_100', 70.5
UNION ALL SELECT 5012, 871, 'Ing_1', 45.1
UNION ALL SELECT 5012, 871, 'Ing_100', 0.9
UNION ALL SELECT 5012, 877, 'Ing_1', 47.2
UNION ALL SELECT 5012, 877, 'Ing_100', 0.8
UNION ALL SELECT 5012, 879, 'Ing_1', 46.6
UNION ALL SELECT 5012, 879, 'Ing_100', 0.9
UNION ALL SELECT 5012, 880, 'Ing_1', 43.6
UNION ALL SELECT 5012, 880, 'Ing_100', 1.2;
ALTER TABLE dbo.Recipe
ADD CONSTRAINT PK__Recipe
PRIMARY KEY CLUSTERED ( Recipe, VersionID, Ingredient );
CREATE NONCLUSTERED INDEX IX__Recipe__Recipe__VersionID
ON dbo.Recipe ( Recipe, VersionID )
INCLUDE ( Percentage );
END;
GO
新しいテーブルを使用して、いくつかの可能なソリューションを探索できます。サンプル出力を拡張して、次のレシピを結果セットに追加して、質問の難しさを示します。
RECIPE --- INGREDIENT_V100 --- PERCENTAGE_V100 --- INGREDIENT_V200 --- INGREDIENT_V200
4000 Ing_1 23,0 Ing_1 20,0
4000 Ing_100 0,1 Ing_100 0,7
4001 Ing_1 8,3 Ing_1 9,3
4001 Ing_100 72,4 Ing_100 70,5
列%_V100
および%_V200
4000
レシピ。ただし、レシピが追加されるとすぐに意味が失われます。 4001
レシピでは、データをバージョンごとに適切にラベル付けするために、新しい個別の列が必要ですが、バージョン番号はレシピごとに異なるため、そのパスを使用すると非常にまばらな結果セットになり、使用するのが非常に面倒です。列にエイリアスを設定すると、バージョン番号データが失われます。
私がこれについて取り組む絶対最悪の方法であると私が感じていることから始めて、まばらな結果セットを見てみましょう。サンプルデータの場合、次の行に沿ったクエリの生成を試みます。
SELECT p.Recipe,
[Ingredient_v100] = CASE WHEN p.[100] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v100] = p.[100],
[Ingredient_v200] = CASE WHEN p.[200] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v200] = p.[200],
[Ingredient_v300] = CASE WHEN p.[300] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v300] = p.[300],
[Ingredient_v871] = CASE WHEN p.[871] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v871] = p.[871],
[Ingredient_v877] = CASE WHEN p.[877] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v877] = p.[877],
[Ingredient_v879] = CASE WHEN p.[879] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v879] = p.[879],
[Ingredient_v880] = CASE WHEN p.[880] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v880] = p.[880],
[Ingredient_v900] = CASE WHEN p.[900] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v900] = p.[900],
[Ingredient_v901] = CASE WHEN p.[901] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v901] = p.[901]
FROM ( SELECT r.Recipe,
r.VersionID,
r.Ingredient,
r.Percentage
FROM dbo.Recipe r ) s
PIVOT ( MAX( s.Percentage )
FOR s.VersionID IN ( [100], [200], [300], [871], [877], [879], [880], [900], [901] ) ) p
ORDER BY p.Recipe;
バージョンの数が可変であるため、いくつかの動的SQLを使用してクエリを生成および実行できます。
DECLARE @Piv NVARCHAR( MAX ),
@Col NVARCHAR( MAX ),
@SQL NVARCHAR( MAX );
SELECT @Piv = LEFT( b.Piv, LEN( b.Piv ) - 1 )
FROM ( SELECT N'[' + CONVERT( VARCHAR( 8 ), a.VersionID ) + '], '
FROM ( SELECT DISTINCT r.VersionID
FROM dbo.Recipe r ) a
ORDER BY a.VersionID
FOR XML PATH ( '' ) ) b ( Piv );
SELECT @Col = LEFT( b.Piv, LEN( b.Piv ) - 1 )
FROM ( SELECT N'[Ingredient_v' + CONVERT( VARCHAR( 8 ), a.VersionID ) + '] = CASE'
+ ' WHEN p.[' + CONVERT( VARCHAR( 8 ), a.VersionID ) + '] IS NULL THEN NULL'
+ ' ELSE p.[Ingredient] END, '
+ '[Percentage_v' + CONVERT( VARCHAR( 8 ), a.VersionID ) + '] = p.['
+ CONVERT( VARCHAR( 8 ), a.VersionID ) + '], '
FROM ( SELECT DISTINCT r.VersionID
FROM dbo.Recipe r ) a
ORDER BY a.VersionID
FOR XML PATH ( '' ) ) b ( Piv );
SET @SQL = N'
SELECT p.Recipe, ' + @Col + '
FROM ( SELECT r.Recipe,
r.VersionID,
r.Ingredient,
r.Percentage
FROM dbo.Recipe r ) s
PIVOT ( MAX( s.Percentage )
FOR s.VersionID IN ( ' + @Piv + ' ) ) p
ORDER BY p.Recipe;';
EXECUTE dbo.sp_executesql @statement = @SQL;
GO
この結果セットは明らかに悪いものです。以下は SQL Fiddle です。これは結果を表示し、のぞいて、次に進みましょう。
疎な結果セットが役に立たないことがわかったので、レシピのバージョン番号を失うことを受け入れ、単純にバージョン番号の昇順でそれらを並べ替えることができます。例のために、アルファベット順にエイリアスを付け、バージョン100
、200
および300
/レシピ4000
はA
、B
、C
の指定を受け取りますが、バージョン900
および901
はA
とB
のみを受け取ります。このために生成するクエリは、次のようになります。
SELECT p.Recipe,
[Ingredient_vA] = p.[Ingredient], [Percentage_vA] = ISNULL( p.[Percentage_vA], 0 ),
[Ingredient_vB] = p.[Ingredient], [Percentage_vB] = ISNULL( p.[Percentage_vB], 0 ),
[Ingredient_vC] = p.[Ingredient], [Percentage_vC] = ISNULL( p.[Percentage_vC], 0 ),
[Ingredient_vD] = p.[Ingredient], [Percentage_vD] = ISNULL( p.[Percentage_vD], 0 )
FROM ( SELECT Lvl = 'Percentage_v' + CHAR( 64 +
DENSE_RANK() OVER (
PARTITION BY r.Recipe
ORDER BY r.VersionID ) ),
r.Recipe,
r.Ingredient,
r.Percentage
FROM dbo.Recipe r ) s
PIVOT ( MAX( s.Percentage )
FOR s.Lvl IN ( [Percentage_vA], [Percentage_vB], [Percentage_vC], [Percentage_vD] ) ) p
ORDER BY p.Recipe;
ソリューション1と同様に、動的SQLを利用してこれを実現できます。
DECLARE @Piv NVARCHAR( MAX ),
@Col NVARCHAR( MAX ),
@SQL NVARCHAR( MAX );
SELECT @Piv = LEFT( b.Piv, LEN( b.Piv ) - 1 )
FROM ( SELECT N'[Percentage_v' + CHAR( 64 + a.Lvl ) + '], '
FROM ( SELECT DISTINCT Lvl = DENSE_RANK()
OVER ( PARTITION BY r.Recipe
ORDER BY r.VersionID )
FROM dbo.Recipe r ) a
ORDER BY a.Lvl
FOR XML PATH ( '' ) ) b ( Piv );
SELECT @Col = LEFT( b.Col, LEN( b.Col ) - 1 )
FROM ( SELECT N'[Ingredient_v' + CHAR( 64 + a.Lvl ) + '] = p.[Ingredient], '
+ '[Percentage_v' + CHAR( 64 + a.Lvl ) + '] = ISNULL( p.[Percentage_v'
+ CHAR( 64 + a.Lvl ) + '], 0 ),'
FROM ( SELECT DISTINCT Lvl = DENSE_RANK()
OVER ( PARTITION BY r.Recipe
ORDER BY r.VersionID )
FROM dbo.Recipe r ) a
ORDER BY a.Lvl
FOR XML PATH ( '' ) ) b ( Col );
SET @SQL = N'
SELECT p.Recipe, ' + @Col + '
FROM ( SELECT Lvl = ''Percentage_v'' + CHAR( 64 +
DENSE_RANK() OVER (
PARTITION BY r.Recipe
ORDER BY r.VersionID ) ),
r.Recipe,
r.Ingredient,
r.Percentage
FROM dbo.Recipe r ) s
PIVOT ( MAX( s.Percentage )
FOR s.Lvl IN ( ' + @Piv + ' ) ) p
ORDER BY p.Recipe;';
EXECUTE dbo.sp_executesql @statement = @SQL;
GO
this SQL Fiddle に示されているように、各レシピの特定のバージョン番号が失われているにもかかわらず、これはかなりきれいな結果セットになります。
バージョン番号の喪失が許容できない場合は、ハイブリッドアプローチを実装できますが、各呼び出しの結果は単一のレシピのみに制限されます。実際、私たちの目標SQLは最初のソリューションと似ていますが、Recipe
番号が明示的に定義されています。
SELECT p.Recipe,
[Ingredient_v100] = CASE WHEN p.[100] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v100] = p.[100],
[Ingredient_v200] = CASE WHEN p.[200] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v200] = p.[200],
[Ingredient_v300] = CASE WHEN p.[300] IS NULL THEN NULL ELSE p.[Ingredient] END, [Percentage_v300] = p.[300]
FROM ( SELECT r.Recipe,
r.VersionID,
r.Ingredient,
r.Percentage
FROM dbo.Recipe r
WHERE r.Recipe = @Recipe ) s
PIVOT ( MAX( s.Percentage )
FOR s.VersionID IN ( [100], [200], [300] ) ) p
ORDER BY p.Recipe;
生成は次のように処理できます。
DECLARE @Piv NVARCHAR( MAX ),
@Col NVARCHAR( MAX ),
@Param NVARCHAR( MAX ),
@SQL NVARCHAR( MAX ),
@Recipe INTEGER = 4000;
SELECT @Piv = LEFT( b.Piv, LEN( b.Piv ) - 1 )
FROM ( SELECT N'[' + CONVERT( VARCHAR( 8 ), a.VersionID ) + '], '
FROM ( SELECT DISTINCT r.VersionID
FROM dbo.Recipe r
WHERE Recipe = @Recipe ) a
ORDER BY a.VersionID
FOR XML PATH ( '' ) ) b ( Piv );
SELECT @Col = LEFT( b.Piv, LEN( b.Piv ) - 1 )
FROM ( SELECT N'[Ingredient_v' + CONVERT( VARCHAR( 8 ), a.VersionID ) + '] = CASE'
+ ' WHEN p.[' + CONVERT( VARCHAR( 8 ), a.VersionID ) + '] IS NULL THEN NULL'
+ ' ELSE p.[Ingredient] END, '
+ '[Percentage_v' + CONVERT( VARCHAR( 8 ), a.VersionID ) + '] = p.['
+ CONVERT( VARCHAR( 8 ), a.VersionID ) + '], '
FROM ( SELECT DISTINCT r.VersionID
FROM dbo.Recipe r
WHERE Recipe = @Recipe ) a
ORDER BY a.VersionID
FOR XML PATH ( '' ) ) b ( Piv );
SET @Param = N'@Recipe INTEGER';
SET @SQL = N'
SELECT p.Recipe, ' + @Col + '
FROM ( SELECT r.Recipe,
r.VersionID,
r.Ingredient,
r.Percentage
FROM dbo.Recipe r
WHERE r.Recipe = @Recipe ) s
PIVOT ( MAX( s.Percentage )
FOR s.VersionID IN ( ' + @Piv + ' ) ) p
ORDER BY p.Recipe;';
EXECUTE dbo.sp_executesql @statement = @SQL, @param = @Param, @Recipe = @Recipe;
GO
この SQL Fiddle または this one に示すように、結果はレシピごとにアクセスできます。