convert xml string in a sql table to dynamic columns - c#

I have two tables (using table variables for illustration. You can run these directly in management studio) that are related by the Id column.
Items in the first table has some standard set of columns and the second table has some extended parameter data for the same record. I'm storing the extended set as xml as it is dynamic in all aspects (different per product or new values being added etc).
I'm able to join these two tables and flatten out the column list as you can see in the example below. But my query requires to have the dynamic columns be defined beforehand. I would like to have this truly dynamic in the sense that if I were to add a new column in the #extended table, it should automatically come out as a new column in the output column list.
Basically the list of additional columns should be determined by the xml for that record. column name should be the xml tag and value should be value for the xml tag for each id.
Any pointers? (and can it be fast too with around 100k records or more in each table)
declare #standard table
(
Id INT,
Column1 varchar(10),
Column2 varchar(10),
Column3 varchar(10)
)
declare #extended table
(
Id INT,
column1 xml
)
insert into #standard values (1,'11', '12', '13')
insert into #standard values (2,'21', '22', '23')
insert into #extended values (1,'<FieldSet><Field><id>1</id><column4>1x</column4><column5>4x</column5></Field></FieldSet>')
insert into #extended values (2,'<FieldSet><Field><id>2</id><column4>2x</column4><column5>5x</column5></Field></FieldSet>')
select s.column1, s.column2,
(
SELECT Item2.value('(column4)[1]', 'varchar(50)')
FROM
e.column1.nodes('/FieldSet') AS T(Item)
CROSS APPLY e.column1.nodes('/FieldSet/Field') AS T2(Item2)
) column4,
(
SELECT Item2.value('(column5)[1]', 'varchar(50)')
FROM
e.column1.nodes('/FieldSet') AS T(Item)
CROSS APPLY e.column1.nodes('/FieldSet/Field') AS T2(Item2)
) column5
from #extended e
join #standard s on s.Id = e.Id

First off you can simplify your current query a bit.
select s.column1,
s.column2,
e.column1.value('(/FieldSet/Field/column4)[1]', 'varchar(50)') as column4,
e.column1.value('(/FieldSet/Field/column5)[1]', 'varchar(50)') as column5
from extended as e
join standard as s
on s.Id = e.Id
To do what you want will not be easy or fast. You need to get a list of all name/value pairs in your XML.
select T1.X.value('.', 'int') as Id,
T2.X.value('local-name(.)', 'sysname') as Name,
T2.X.value('.', 'varchar(10)') as Value
from extended as e
cross apply e.column1.nodes('/FieldSet/Field/id') as T1(X)
cross apply e.column1.nodes('/FieldSet/Field/*[position() > 1]') as T2(X)
Use that in a pivot query and join to standard.
select S.column1,
S.column2,
P.column4,
P.column5
from standard as s
inner join
(
select id, P.column4, P.column5
from (
select T1.X.value('.', 'int') as Id,
T2.X.value('local-name(.)', 'sysname') as Name,
T2.X.value('.', 'varchar(10)') as Value
from extended as e
cross apply e.column1.nodes('/FieldSet/Field/id') as T1(X)
cross apply e.column1.nodes('/FieldSet/Field/*[position() > 1]') as T2(X)
) as e
pivot (min(Value) for Name in (column4, column5)) P
) P
on S.Id = P.Id
To do this with a dynamic number of columns returned you need to build this pivot query dynamically.
Store the Name/Value pairs in temp table, use that table to figure out the columns you need and to build your query.
create table #ext
(
Id int,
Name sysname,
Value varchar(10),
primary key(Id, Name)
)
insert into #ext(Id, Name, Value)
select T1.X.value('.', 'int') as Id,
T2.X.value('local-name(.)', 'sysname') as Name,
T2.X.value('.', 'varchar(10)') as Value
from extended as e
cross apply e.column1.nodes('/FieldSet/Field/id') as T1(X)
cross apply e.column1.nodes('/FieldSet/Field/*[position() > 1]') as T2(X)
declare #SQL nvarchar(max)
set #SQL =
'select S.column1,
S.column2,
[COLLIST]
from standard as s
inner join
(
select id, [COLLIST]
from #ext as e
pivot (min(Value) for Name in ([COLLIST])) P
) P
on S.Id = P.Id'
declare #ColList nvarchar(max)
set #ColList =
(select ','+Name
from #ext
group by Name
for xml path(''), type).value('.', 'nvarchar(max)')
set #SQL = replace(#SQL, '[COLLIST]', stuff(#ColList, 1, 1, ''))
exec (#SQL)
drop table #ext

I hope it will help you
SELECT #COUNT_XML=0
SELECT #COUNT_XML=(SELECT #xxxxx_GROUP_ID.value('count(/NewDataSet/position/ID)', 'INT'))
IF(#COUNT_XML > 0)
BEGIN
IF OBJECT_ID('tempdb..#TBL_TEMPOSITION') IS NOT NULL
DROP TABLE #TBL_TEMPOSITION
CREATE TABLE #TBL_TEMPOSITION (ID NUMERIC(18,0))
INSERT INTO #TBL_TEMPOSITION (ID)
SELECT XMLxxxxGroup.PositionGPItem.value('.','NUMERIC(18,0)')
FROM #xxxxx_GROUP_ID.nodes('/NewDataSet/position/ID') AS XMLPositionGroup(PositionGPItem)
SELECT #emp_cond =#emp_cond+ N' AND CM.STATIC_EMP_INFO.POSITION_ID IN (SELECT ID FROM #TBL_TEMPOSITION) '
END

Related

Dynamic SQL Pivot parameters query Entity Framework

I have a query which is working great when I used it in SQL Server Management Studio. It is worth to mention here that in PIVOT [MedicalInformation.MedicalInformationForm.Name.firstName] and [MedicalInformation.MedicalInformationForm.Name.lastName] are NOT static and can vary a.k.a they can be change during run time with different columns
WITH pivot_data AS
(
SELECT
[p].RecordId, -- Grouping Column
[Key], -- Spreading Column
[Value] -- Aggregate Column
FROM
[RecordDatas] AS [p]
INNER JOIN
(SELECT [p0].*
FROM [Records] AS [p0]
WHERE [p0].[IsDeleted] = 0) AS [t] ON [p].[RecordId] = [t].[ID]
WHERE
[p].DatasetId = 1386
AND [p].[IsDeleted] = 0
AND ([t].[ProjectID] = 191)
AND [Key] IN ('MedicalInformation.MedicalInformationForm.Name.firstName',
'MedicalInformation.MedicalInformationForm.Name.lastName')
)
SELECT
RecordId,
[MedicalInformation.MedicalInformationForm.Name.firstName],
[MedicalInformation.MedicalInformationForm.Name.lastName]
FROM
pivot_data
PIVOT
(MAX([Value])
FOR [Key] IN ([MedicalInformation.MedicalInformationForm.Name.firstName],
[MedicalInformation.MedicalInformationForm.Name.lastName])) AS p
The problem occurs when I'm trying to build the same query dynamic from C#:
var sql1 = #"
WITH pivot_data AS
(
SELECT [p].RecordId, -- Grouping Column
[Key], -- Spreading Column
[Value] -- Aggregate Column
FROM [RecordDatas] AS [p]
INNER JOIN (
SELECT [p0].*
FROM [Records] AS [p0]
WHERE [p0].[IsDeleted] = 0
) AS [t] ON [p].[RecordId] = [t].[ID]
where [p].DatasetId = 1386
AND [p].[IsDeleted] = 0
AND ([t].[ProjectID] = 191)
AND [Key] IN( {0} , {1}))
SELECT RecordId, {2},{3}
FROM pivot_data
PIVOT (max([Value]) FOR [Key] IN ({2},{3})) AS p;";
await repositoryMapper.Repository.Context.Database.ExecuteSqlCommandAsync(sql1,
"MedicalInformation.MedicalInformationForm.Name.firstName",
"MedicalInformation.MedicalInformationForm.Name.lastName",
"[MedicalInformation.MedicalInformationForm.Name.firstName]",
"[MedicalInformation.MedicalInformationForm.Name.lastName]");
And here is coming my problem - when query is generated with ExecuteSqlCommandAsync and adding parameters for the PIVOT columns ([MedicalInformation.MedicalInformationForm.Name.firstName] and [MedicalInformation.MedicalInformationForm.Name.lastName]) there is something wrong with syntax. I'm getting an error:
Incorrect syntax near '#p2'.
I have tried to get the generated query which Entity Framework is doing and it looks like this :
exec sp_executesql N'
WITH pivot_data AS
(
SELECT [p].RecordId, -- Grouping Column
[Key], -- Spreading Column
[Value] -- Aggregate Column
FROM [RecordDatas] AS [p]
INNER JOIN (
SELECT [p0].*
FROM [Records] AS [p0]
WHERE [p0].[IsDeleted] = 0
) AS [t] ON [p].[RecordId] = [t].[ID]
where [p].DatasetId = 1386
AND [p].[IsDeleted] = 0
AND ([t].[ProjectID] = 191)
AND [Key] IN( #p0 , #p1))
SELECT RecordId, #p2,#p3
FROM pivot_data
PIVOT (max([Value]) FOR [Key] IN (#p2,#p3)) AS p;
',N'#p0 nvarchar(4000),
#p1 nvarchar(4000),
#p2 nvarchar(4000),
#p3 nvarchar(4000)',
#p0=N'MedicalInformation.MedicalInformationForm.Name.firstName',
#p1=N'MedicalInfo rmation.MedicalInformationForm.Name.lastName',
#p2=N'[MedicalInformation.MedicalInformationForm.Name.firstName]',
#p3=N'[MedicalInformation.MedicalInformationForm.Name.lastName]'
Please help me I'm not sure what is the problem here
The problem is that a line like this:
SELECT RecordId, #p2,#p3
is not valid. Statements are precompiled and you can not change field names in parameters - field names for a statement are static.
You have to go into dynamic SQL, which EntityFramework does not support (outside of the functions allowing you to submit whatever SQL you want). Basically you must create the valid statement in a way that is NOT using parameters are field names.
This does NOT mean you can not use placeholders and - in C# - run a replace operation on the string, but it means that the replacement has to be finished when you THEN submit the changed script to the database.
Limitation of SQL Server.
You can not assign value of these #p2,#p3 in SELECT RecordId, #p2,#p3 from parameter. You need to set these statement before running query as follow:
var sql1 = #"
WITH pivot_data AS
(
SELECT [p].RecordId, -- Grouping Column
[Key], -- Spreading Column
[Value] -- Aggregate Column
FROM [RecordDatas] AS [p]
INNER JOIN (
SELECT [p0].*
FROM [Records] AS [p0]
WHERE [p0].[IsDeleted] = 0
) AS [t] ON [p].[RecordId] = [t].[ID]
where [p].DatasetId = 1386
AND [p].[IsDeleted] = 0
AND ([t].[ProjectID] = 191)
AND [Key] IN( {0} , {1}))
SELECT RecordId, [MedicalInformation.MedicalInformationForm.Name.lastName],[MedicalInformation.MedicalInformationForm.Name.lastName]
FROM pivot_data
PIVOT (max([Value]) FOR [Key] IN ({2},{3})) AS p;";
await repositoryMapper.Repository.Context.Database.ExecuteSqlCommandAsync(sql1,
"MedicalInformation.MedicalInformationForm.Name.firstName",
"MedicalInformation.MedicalInformationForm.Name.lastName",
"[MedicalInformation.MedicalInformationForm.Name.firstName]",
"[MedicalInformation.MedicalInformationForm.Name.lastName]");

SQL Server : sorting a distinct table which has XML column

I have a table which I need to do a distinct on as it has some duplicates and then needs to sort it and join the XML into one column.
Here is what I tried :
Select *
From TableA
Where TableID ='1234'
And Convert(nvarchar(max), xmlcolumn) in (Select Distinct Convert(nvarchar(max), xmlcolumn))
The above does sort out the list for me nor it removes the duplicates.
Select distinct convert (nvarchar(max), xmlcolumn)
From TableA
Where TableID ='1234'
Order By TableID
The above returns what is required, but if I do an order by (as shown above) I get an error:
ORDER by items must appear in the select list if select district is specified
I don't want to enter the TableID in the select statement because I don't want this in the results. Secondly, how do I convert the XML (which is converted to nvarchar to do the distinct) back to XML for exporting to C#.
If i understand correctly, you could remove duplicate rows by using ROW_NUMBER like this
DECLARE #SampleData AS TABLE
(
TableID varchar(10),
ColumnA int,
ColumnB varchar(10),
XmlColumn xml
)
;WITH temp AS
(
SELECT sd.TableID,
CAST(sd.XmlColumn AS nvarchar(max)) AS XmlColumn,
ROW_NUMBER() OVER(PARTITION BY CAST(sd.XmlColumn AS nvarchar(max)) ORDER BY sd.TableID) AS Rn
FROM #SampleData sd
Where TableID = '1234'
)
SELECT t.XmlColumn
FROM temp t
WHERE Rn = 1 -- remove duplicate rows
ORDER BY t.TableID -- order by any columns

LINQ adding columns to base result set

I have three classes, Code,CodeContact and Contact which map to tables in a database. In my code the classes have one static method which returns the data you'll find in the following SQL script
create table #Contacts
(ContactId int
,Name varchar(50)
,Phone varchar(50)
)
insert into #Contacts(ContactId,Name,Phone)
values (1,'jon',111 )
,(2,'jim',222)
,(3,'james',333)
,(4,'john',444)
,(5,'tim',555)
,(6,'jane',666)
go
create table #ContactsCodes
(ContactId int
,CodeId int
)
insert into #ContactsCodes(ContactId,CodeId)
values (1,1 )
,(1,2 )
,(1,3 )
,(1,4 )
,(2,2 )
,(2,3 )
,(3,1 )
,(3,4 )
,(4,1 )
,(4,5 )
create table #Codes
(CodeId int
,CodeText varchar(50)
)
insert into #Codes
values (1,'Code Red' )
,(2,'Code Blue' )
,(3,'Code Green' )
,(4,'Code Gray' )
,(5,'Code Black' )
,(6,'code clear' )
I'm looking for some advice on how to get LINQ code to acieve something like that following
declare #filter varchar(50) = (select 1)
select c.ContactId
,c.Name
,c.Phone
,case when varColumn.ContactId is null
then 'Not Present'
else 'Present' end as [Code Red]
,case when varColumn2.ContactId is null
then 'Not Present'
else 'Present' end as [Code Blue]
from Contacts as c
--****************end of 'cohort' base set of IDs
--first item in filter list becomes a
--column on a DataTable
left join
(
select distinct cc.ContactId
from Codes as codes
left join ContactsCodes as cc
on cc.CodeId = codes.CodeId
where codes.CodeId =#filter
) as varColumn on varColumn.ContactId = c.ContactId
--second item in filter becomes another column
left join
(
select distinct cc.ContactId
from Codes as codes
left join ContactsCodes as cc
on cc.CodeId = codes.CodeId
where codes.CodeId =#filter+1
) as varColumn2 on varColumn2.ContactId = c.ContactId
So let's say I have an IEnumerable with 1,2,3 in it. I would like to iterate over the collection and left join to the Contact.ContactId column and simply give an indication if that particular CodeId was found in the ContactsCodes table. Here's what the result looks like if you run the above SQL:
I will eventually need the tackd on column data to be pretty dynamic. But for the time being, can anyone provide a direction to go? I think the easiest choice might be to go the LINQ to SQL route and have the stuff inside the left join a stored procedure and call it X number of times for whatever's in the list, but I'm welcome to any suggestions.

Parameters to the EXISTS clause in a stored procedure

I have a table DEPT, which holds 2 columns - ID, NAME.
A search form is presented with the IDs from the DEPT table and the user can chose any number of IDs and submit the form, to get the related NAMEs.
Clarification/Inputs:
I don't want to build a dynamic query - its not manageable.
I prefer a stored procedure using table-valued parameters
Any other solutions to proceed?
NOTE:
This example is simple with 1 table - in real life, I have to deal with more than 6 tables!
Thanks for any suggestions
CREATE TYPE dbo.DeptList
AS TABLE
(
ID INT
);
GO
CREATE PROCEDURE dbo.RetrieveDepartments
#dept_list AS dbo.DeptList READONLY
AS
BEGIN
SET NOCOUNT ON;
SELECT Name FROM dbo.table1 WHERE ID IN (SELECT ID FROM #dept)
UNION ALL
SELECT Name FROM dbo.table2 WHERE ID IN (SELECT ID FROM #dept)
-- ...
END
GO
Now in your C# code, create a DataTable, fill it in with the IDs, and pass it in to the stored procedure. Assuming you already have a list called tempList and the IDs are stored in id:
DataTable tvp = new DataTable();
tvp.Columns.Add(new DataColumn("ID"));
foreach(var item in tempList)
{
tvp.Rows.Add(item.id);
}
using (connObject)
{
SqlCommand cmd = new SqlCommand("StoredProcedure", connObject);
cmd.CommandType = CommandType.StoredProcedure;
SqlParameter tvparam = cmd.Parameters.AddWithValue("#dept_list", tvp);
tvparam.SqlDbType = SqlDbType.Structured;
...
}
You can also use a split function. Many exist, this is the one I like if you can guarantee that the input is safe (no <, >, & etc.):
CREATE FUNCTION dbo.SplitInts_XML
(
#List VARCHAR(MAX),
#Delimiter CHAR(1)
)
RETURNS TABLE
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'int')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>') + '</i>').query('.')
) AS a
CROSS APPLY x.nodes('i') AS y(i)
);
GO
Now your procedure can be:
CREATE PROCEDURE dbo.RetrieveDepartments
#dept_list VARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
;WITH d AS (SELECT ID = Item FROM dbo.SplitInts(#dept_list, ','))
SELECT Name FROM dbo.table1 WHERE ID IN (SELECT ID FROM d)
UNION ALL
SELECT Name FROM dbo.table2 WHERE ID IN (SELECT ID FROM d)
-- ...
END
GO

SQL insert into not exists on multiple results

I have Plan table that has a one to many relationship with a Price table.
(1 plan can have many prices)
However the problem is I have another table called "StandardPrices" that holds the names of all the prices (The reason for this is I want to be able to add or remove prices at any time)
Plan Table:
ID int primary key,
plan Name varchar(200),
...
PriceTable:
ID int primary key,
PlanId int foreign key references plan(ID)
PriceName ID foreign key standardprices(id)
StandardPrices:
ID int primary key,
PriceName varchar(200),
DefaultPrice money
So whenever a plan is created, it automatically creates a List of all the prices in the StandardPrice list (with default values).
The problem I have, Is I need, whenever I create a new StandardPrice, it automatically checks if that price exists in every plan, and if it doesnt, create an entry in the price table for that planid.
I use Stored procedures and thought the best way to do this would be through SQL.
When StandardPrices are created:
begin
insert into StandardPrices (PriceName, Defaultprice)
values (#priceName, #DefaultPrice)
end
begin
//list all plans.
//cross reference PriceTable to see if plan exists
//if not insert priceplan with default value
end
I am a bit confused how i can implement such a sql command?
I think it looks something like this:
insert into PriceTable (PlanId, PriceName)
select PlanId, #priceName
from Plan
where not exists
(select null from PriceTable where PriceTable.PlanId = Plan.PlanId)
And you should probably do this as part of an INSERT trigger on your database.
Do something like this:
if not exists
(
select *
from PriceTable
where PriceName = #priceName
)
begin
insert into PriceTable(PriceName)
values(#priceName)
end
What this does is it conditionally checks to see if that PriceName is already in the PriceTable. If it is not, then it will insert that new #priceName into the PriceTable. I wasn't sure by your original post what the value would be for PlanID, but you should get the idea from my above query.
You need to get a list of all plans, LEFT OUTER joined to the appropriate price. If the price record is null, there isn't an appropriate association so add it.
You'll probably need to use a cursor to iterate through the results and add prices.
Something like..
SELECT Plan.Id, Price.Id
FROM Plan LEFT OUTER JOIN Price ON Price.PlanId = Plan.Id
WHERE Price.Id = [Your New Price Id]
Using MERGE and assuming your ID columns have the IDENTITY property:
MERGE INTO PriceTable
USING
(
SELECT p1.ID AS PlanId, sp1.PriceName
FROM Plan p1
CROSS JOIN StandardPrices sp1
) AS SourceTable
ON PriceTable.PlanId = SourceTable.PlanId
AND PriceTable.PriceName = SourceTable.PriceName
WHEN NOT MATCHED
THEN
INSERT ( PlanId, PriceName )
VALUES ( PlanId, PriceName ) ;
Here is a fuller sketch, where I've drooped the seemingly redundant ID columns, promoted the name columns to relational keys, added constraints, used consistent naming between tables, changed MONET to DECIMAL, etc:
CREATE TABLE Plans
(
plan_name VARCHAR(200) NOT NULL
UNIQUE
CHECK ( plan_name <> ' ' )
) ;
CREATE TABLE StandardPrices
(
price_name VARCHAR(200) NOT NULL
UNIQUE
CHECK ( price_name <> ' ' ),
default_price DECIMAL(19, 4) NOT NULL
CHECK ( default_price >= 0 )
) ;
CREATE TABLE Prices
(
plan_name VARCHAR(200) NOT NULL
REFERENCES Plans ( plan_name ),
price_name VARCHAR(200) NOT NULL
REFERENCES StandardPrices ( price_name ),
UNIQUE ( plan_name, price_name )
) ;
DECLARE #plan_name_new VARCHAR(200) ;
DECLARE #price_name_1 VARCHAR(200) ;
DECLARE #default_price_1 DECIMAL(19, 4) ;
DECLARE #price_name_2 VARCHAR(200) ;
DECLARE #default_price_2 DECIMAL(19, 4) ;
SET #plan_name_new = 'One'
SET #price_name_1 = 'Day'
SET #default_price_1 = 55 ;
SET #price_name_2 = 'When'
SET #default_price_2 = 99
INSERT INTO Plans ( plan_name )
VALUES ( #plan_name_new ) ;
INSERT INTO StandardPrices ( price_name, default_price )
VALUES ( #price_name_1, #default_price_1 ) ;
INSERT INTO StandardPrices ( price_name, default_price )
VALUES ( #price_name_2, #default_price_2 ) ;
MERGE INTO Prices
USING
(
SELECT p1.plan_name, sp1.price_name
FROM Plans p1
CROSS JOIN StandardPrices sp1
) AS SourceTable
ON Prices.plan_name = SourceTable.plan_name
AND Prices.price_name = SourceTable.price_name
WHEN NOT MATCHED
THEN
INSERT ( plan_name, price_name )
VALUES ( plan_name, price_name ) ;
(MySql) I'll suggest you to create with temporary table; It is clean and readable (I will add without alias to be more readable):
CREATE TEMPORARY TABLE tempTable
(
table1FK varchar(250),
table2Value varchar(250)
);
INSERT INTO tempTable(table1FK, table2Value)
VALUES ('it', '+39'),
('lt', '+370'),
('gb', '+44'),
('cn', '+86');
INSERT INTO table2(table1FK_id, table2Value)
SELECT table1.id, tempTable.prefix
FROM tempTable
inner join table1 ON table1.code = tempTable.country_code
left join table2 ON table2.country_id = table1.id
where table2.id is null;
DROP temporary TABLE tempTable;

Categories