/**************************************************** AboutSQLServer.com blog Written by Dmitri Korotkevitch Store Custom Fields/Attributes in Microsoft SQL Server Database (Part 2 - Name/Value pairs) 2012-02-16 *****************************************************/ set nocount on go create table dbo.Articles ( ArticleId int not null, Name nvarchar(255) not null, Description nvarchar(max) not null, constraint PK_Articles primary key clustered(ArticleId) ) go create table dbo.ArticleAttributes ( ArticleId int not null, AttrIndex smallint not null, Value sql_variant not null, constraint PK_ArticleAttributes primary key clustered(ArticleId, AttrIndex), constraint FK_ArticleAttributes foreign key(ArticleId) references dbo.Articles(ArticleId) on update cascade on delete cascade ) go /* This table is not required for the demo But it's good idea to have something like that */ create table dbo.CustomAttributesMetadata ( -- internal application EntityId EntityId smallint not null, AttrIndex smallint not null, AttributeName nvarchar(32) not null, -- references sys.types.system_type_id TypeId tinyint not null, -- references sys.types.name TypeName sysname not null, constraint PK_CustomAttributesMetadata primary key clustered(EntityId,AttrIndex) ) go insert into dbo.CustomAttributesMetadata (EntityId, AttrIndex, AttributeName, TypeId, TypeName) values (1 /*Articles*/, 1, 'Color', 231, 'nvarchar'), (1 /*Articles*/, 2, 'Weight', 62, 'float') -- Other attributes go -- Let's put some data begin tran ;with IDs(ID) as ( select 1 union all select ID + 1 from IDs where ID < 10000 ) insert into dbo.Articles(ArticleId,Name,Description) select ID, N'Article ' + CONVERT(nvarchar(6),ID), REPLICATE('Description ',25) from IDs option (maxrecursion 0) ;with IDs(ID) as ( select 1 union all select ID + 1 from IDs where ID < 10000 ) ,Attributes(Idx, Value) as ( select 1, convert(sql_variant,'Black') union all select 2, convert(sql_variant,12.324) union all select 3, convert(sql_variant,0) union all select 4, convert(sql_variant,23.214) union all select 5, convert(sql_variant,N'Some Text Information') union all select 6, convert(sql_variant,100.21454) union all select 7, convert(sql_variant,-28.72) union all select 8, convert(sql_variant,75.13574) union all select 9, convert(sql_variant,'+1 (123) 456-7890') union all select 10, convert(sql_variant,N'John Doe') union all select 11, convert(sql_variant,5712123) union all select 12, convert(sql_variant,0) union all select 13, convert(sql_variant,1) union all select 14, convert(sql_variant,'contact@mydomain.com') union all select 15, convert(sql_variant,10.25) ) insert into dbo.ArticleAttributes(ArticleId, AttrIndex, Value) select IDs.ID, a.Idx, a.Value from IDs cross join Attributes a option (maxrecursion 0) commit set statistics io on go -- Classic approach select a.ArticleId, a.Name, a.Description, CONVERT(nvarchar(32), aa1.Value) as [Color], CONVERT(float, aa2.Value) as [Weight], CONVERT(int, aa3.Value) as [Group], CONVERT(float, aa4.Value) as [UnitCost], CONVERT(nvarchar(max), aa5.Value) as [Comment], CONVERT(float, aa6.Value) as [UnitPrice], CONVERT(decimal(9,6), aa7.Value) as [MadeAtLat], CONVERT(decimal(9,6), aa8.Value) as [MadeAtLon], CONVERT(nvarchar(32), aa9.Value) as [TotalSold], CONVERT(nvarchar(128), aa10.Value) as [FactoryContact], CONVERT(int, aa11.Value) as [FeatureMask], CONVERT(bit, aa12.Value) as [ContactSpeaksSpanish], CONVERT(bit, aa13.Value) as [ContactSpeaksEnglish], CONVERT(nvarchar(128), aa14.Value) as [ContactEmail], CONVERT(money, aa15.Value) as [Shipping] from dbo.Articles a join dbo.ArticleAttributes aa1 on a.ArticleId = aa1.ArticleId and aa1.AttrIndex = 1 join dbo.ArticleAttributes aa2 on a.ArticleId = aa2.ArticleId and aa2.AttrIndex = 2 join dbo.ArticleAttributes aa3 on a.ArticleId = aa3.ArticleId and aa3.AttrIndex = 3 join dbo.ArticleAttributes aa4 on a.ArticleId = aa4.ArticleId and aa4.AttrIndex = 4 join dbo.ArticleAttributes aa5 on a.ArticleId = aa5.ArticleId and aa5.AttrIndex = 5 join dbo.ArticleAttributes aa6 on a.ArticleId = aa6.ArticleId and aa6.AttrIndex = 6 join dbo.ArticleAttributes aa7 on a.ArticleId = aa7.ArticleId and aa7.AttrIndex = 7 join dbo.ArticleAttributes aa8 on a.ArticleId = aa8.ArticleId and aa8.AttrIndex = 8 join dbo.ArticleAttributes aa9 on a.ArticleId = aa9.ArticleId and aa9.AttrIndex = 9 join dbo.ArticleAttributes aa10 on a.ArticleId = aa10.ArticleId and aa10.AttrIndex = 10 join dbo.ArticleAttributes aa11 on a.ArticleId = aa11.ArticleId and aa11.AttrIndex = 11 join dbo.ArticleAttributes aa12 on a.ArticleId = aa12.ArticleId and aa12.AttrIndex = 12 join dbo.ArticleAttributes aa13 on a.ArticleId = aa13.ArticleId and aa13.AttrIndex = 13 join dbo.ArticleAttributes aa14 on a.ArticleId = aa14.ArticleId and aa14.AttrIndex = 14 join dbo.ArticleAttributes aa15 on a.ArticleId = aa15.ArticleId and aa15.AttrIndex = 15 where a.ArticleId between 1100 and 1200 go -- Using PIVOT -- Plan with possible Merge Join ;with PivotedAttributes(ArticleId,Color,Weight,[Group],UnitCost, Comment,UnitPrice,MadeAtLat,MadeAtLon,TotalSold,FactoryContact, FeatureMask,ContactSpeaksSpanish,ContactSpeaksEnglish, ContactEmail,Shipping) as ( select ArticleId, CONVERT(nvarchar(32), [1]) as [Color], CONVERT(float, [2]) as [Weight], CONVERT(int, [3]) as [Group], CONVERT(float, [4]) as [UnitCost], CONVERT(nvarchar(max), [5]) as [Comment], CONVERT(float, [6]) as [UnitPrice], CONVERT(decimal(9,6), [7]) as [MadeAtLat], CONVERT(decimal(9,6), [8]) as [MadeAtLon], CONVERT(nvarchar(32), [9]) as [TotalSold], CONVERT(nvarchar(128), [10]) as [FactoryContact], CONVERT(int, [11]) as [FeatureMask], CONVERT(bit, [12]) as [ContactSpeaksSpanish], CONVERT(bit, [13]) as [ContactSpeaksEnglish], CONVERT(nvarchar(128), [14]) as [ContactEmail], CONVERT(money, [15]) as [Shipping] from ( select ArticleId, AttrIndex, Value from dbo.ArticleAttributes ) as Source PIVOT (MIN(Value) for AttrIndex in ([1],[2],[3] ,[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14],[15])) as A ) select a.ArticleId, a.Name, a.Description ,p.Color,p.Weight,p.[Group],p.UnitCost,p.Comment ,p.UnitPrice,p.MadeAtLat,p.MadeAtLon,p.TotalSold,p.FactoryContact ,p.FeatureMask,p.ContactSpeaksSpanish,p.ContactSpeaksEnglish ,p.ContactEmail,p.Shipping from PivotedAttributes p join dbo.Articles a on p.ArticleId = a.ArticleId where a.ArticleId between 1100 and 1200 go -- Plan with Nested Loop select a.ArticleId, a.Name, a.Description ,p.Color,p.Weight,p.[Group],p.UnitCost,p.Comment ,p.UnitPrice,p.MadeAtLat,p.MadeAtLon,p.TotalSold,p.FactoryContact ,p.FeatureMask,p.ContactSpeaksSpanish,p.ContactSpeaksEnglish ,p.ContactEmail,p.Shipping from dbo.Articles a cross apply ( select ArticleId, CONVERT(nvarchar(32), [1]) as [Color], CONVERT(float, [2]) as [Weight], CONVERT(int, [3]) as [Group], CONVERT(float, [4]) as [UnitCost], CONVERT(nvarchar(max), [5]) as [Comment], CONVERT(float, [6]) as [UnitPrice], CONVERT(decimal(9,6), [7]) as [MadeAtLat], CONVERT(decimal(9,6), [8]) as [MadeAtLon], CONVERT(nvarchar(32), [9]) as [TotalSold], CONVERT(nvarchar(128), [10]) as [FactoryContact], CONVERT(int, [11]) as [FeatureMask], CONVERT(bit, [12]) as [ContactSpeaksSpanish], CONVERT(bit, [13]) as [ContactSpeaksEnglish], CONVERT(nvarchar(128), [14]) as [ContactEmail], CONVERT(money, [15]) as [Shipping] from ( select attr.ArticleId, attr.AttrIndex, attr.Value from dbo.ArticleAttributes attr where attr.ArticleId = a.ArticleId ) as Source PIVOT (MIN(Value) for AttrIndex in ([1],[2],[3] ,[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14],[15])) as A ) p where a.ArticleId between 1100 and 1200 go -- Add index to support search and paging by one field -- Max Value of the key could be an issue create index IDX_ArticleAttributes_AttrIndex_Value on dbo.ArticleAttributes(AttrIndex,Value) go -- Searching for the attribute value update dbo.ArticleAttributes set Value = 10 where ArticleId between 100 and 110 and AttrIndex = 3 go ;with PivotedAttributes(ArticleId,Color,Weight,[Group],UnitCost, Comment,UnitPrice,MadeAtLat,MadeAtLon,TotalSold,FactoryContact, FeatureMask,ContactSpeaksSpanish,ContactSpeaksEnglish, ContactEmail,Shipping) as ( select ArticleId, CONVERT(nvarchar(32), [1]) as [Color], CONVERT(float, [2]) as [Weight], CONVERT(int, [3]) as [Group], CONVERT(float, [4]) as [UnitCost], CONVERT(nvarchar(max), [5]) as [Comment], CONVERT(float, [6]) as [UnitPrice], CONVERT(decimal(9,6), [7]) as [MadeAtLat], CONVERT(decimal(9,6), [8]) as [MadeAtLon], CONVERT(nvarchar(32), [9]) as [TotalSold], CONVERT(nvarchar(128), [10]) as [FactoryContact], CONVERT(int, [11]) as [FeatureMask], CONVERT(bit, [12]) as [ContactSpeaksSpanish], CONVERT(bit, [13]) as [ContactSpeaksEnglish], CONVERT(nvarchar(128), [14]) as [ContactEmail], CONVERT(money, [15]) as [Shipping] from ( select ArticleId, AttrIndex, Value from dbo.ArticleAttributes ) as Source PIVOT (MIN(Value) for AttrIndex in ([1],[2],[3] ,[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14],[15])) as A ) select a.ArticleId, a.Name, a.Description ,p.Color,p.Weight,p.[Group],p.UnitCost,p.Comment ,p.UnitPrice,p.MadeAtLat,p.MadeAtLon,p.TotalSold,p.FactoryContact ,p.FeatureMask,p.ContactSpeaksSpanish,p.ContactSpeaksEnglish ,p.ContactEmail,p.Shipping from dbo.Articles a join dbo.ArticleAttributes aa on a.ArticleId = aa.ArticleId and aa.AttrIndex = 3 and aa.Value = 10 join PivotedAttributes p on a.ArticleId = p.ArticleId go --Paging ;with Paging(ArticleId, RowNum) as ( select ArticleId, ROW_NUMBER() over (order by Value) from dbo.ArticleAttributes where AttrIndex = 3 ) ,PivotedAttributes(ArticleId,RowNum,Color,Weight,[Group],UnitCost, Comment,UnitPrice,MadeAtLat,MadeAtLon,TotalSold,FactoryContact, FeatureMask,ContactSpeaksSpanish,ContactSpeaksEnglish, ContactEmail,Shipping) as ( select ArticleId, RowNum, CONVERT(nvarchar(32), [1]) as [Color], CONVERT(float, [2]) as [Weight], CONVERT(int, [3]) as [Group], CONVERT(float, [4]) as [UnitCost], CONVERT(nvarchar(max), [5]) as [Comment], CONVERT(float, [6]) as [UnitPrice], CONVERT(decimal(9,6), [7]) as [MadeAtLat], CONVERT(decimal(9,6), [8]) as [MadeAtLon], CONVERT(nvarchar(32), [9]) as [TotalSold], CONVERT(nvarchar(128), [10]) as [FactoryContact], CONVERT(int, [11]) as [FeatureMask], CONVERT(bit, [12]) as [ContactSpeaksSpanish], CONVERT(bit, [13]) as [ContactSpeaksEnglish], CONVERT(nvarchar(128), [14]) as [ContactEmail], CONVERT(money, [15]) as [Shipping] from ( select a.ArticleId, p.RowNum, a.AttrIndex, a.Value from dbo.ArticleAttributes a join Paging p on a.ArticleId = p.ArticleId where p.RowNum between 1 and 10 ) as Source PIVOT (MIN(Value) for AttrIndex in ([1],[2],[3] ,[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14],[15])) as A ) select a.ArticleId, a.Name, a.Description ,p.Color,p.Weight,p.[Group],p.UnitCost,p.Comment ,p.UnitPrice,p.MadeAtLat,p.MadeAtLon,p.TotalSold,p.FactoryContact ,p.FeatureMask,p.ContactSpeaksSpanish,p.ContactSpeaksEnglish ,p.ContactEmail,p.Shipping from PivotedAttributes p join dbo.Articles a on p.ArticleId = a.ArticleId order by p.RowNum go