XQuery with dynamic XPath from XML Sql Server Column - sql-server

I have a log table has xml column which contains log contents
There is also table called logType which is the type of log
I need to create query descripes the xml contents as readable string
I added a column with name logXPath to logtype table
and i created the following query
SELECT contents.value(LogXPath, 'nvarchar(max)')
FROM dbo.Log
JOIN dbo.LogType ON dbo.Log.logTypeID = dbo.LogType.logTypeID
and I got the following error
The argument 1 of the XML data type method "value" must be a string literal
and I searched for a way to do this with no results!!
Is there any do dynamic xpath in Sql Server XML Column?
Edit
for example assume the following schema and data
CREATE TABLE [dbo].[logType]
(
[logTypeID] [int] NOT NULL ,
[logTypeName] [nvarchar](50) NOT NULL ,
[xPath] [nvarchar](MAX) NOT NULL ,
CONSTRAINT [PK_logType] PRIMARY KEY CLUSTERED ( [logTypeID] ASC )
)
GO
INSERT [dbo].[logType]
( [logTypeID] ,
[logTypeName] ,
[xPath]
)
VALUES ( 1 ,
N'Patient Data' ,
N'(/Patient/PatientName)[1]'
)
INSERT [dbo].[logType]
( [logTypeID] ,
[logTypeName] ,
[xPath]
)
VALUES ( 2 ,
N'Clinic Data' ,
N'(/Clinic/ClinicName)[1]'
)
/****** Object: Table [dbo].[log] Script Date: 02/04/2015 13:58:47 ******/
GO
CREATE TABLE [dbo].[log]
(
[logID] [int] NOT NULL ,
[logTypeID] [int] NOT NULL ,
[Contents] [xml] NULL ,
CONSTRAINT [PK_log] PRIMARY KEY CLUSTERED ( [logID] ASC )
)
GO
INSERT [dbo].[log]
( [logID] ,
[logTypeID] ,
[Contents]
)
VALUES ( 1 ,
1 ,
N'<Patient><PatientID>1</PatientID><PatientName>john</PatientName></Patient>'
)
INSERT [dbo].[log]
( [logID] ,
[logTypeID] ,
[Contents]
)
VALUES ( 2 ,
2 ,
N'<Clinic><ClinicID>1</ClinicID><ClinicName>Clinic 1</ClinicName></Clinic>'
)
When I make query like the following ,it gives me the error
SELECT logTypeName ,
[Contents].value(dbo.logType.xPath, 'nvarchar(max)') AS data
FROM dbo.[log]
JOIN dbo.logType ON dbo.[log].logTypeID = dbo.logType.logTypeID

You can build a query dynamically using the table LogType.
declare #SQL nvarchar(max);
set #SQL = 'select case L.logTypeID'+
(
select ' when '+cast(LT.logTypeID as varchar(11))+
' then L.Contents.value('''+LT.xPath+''', ''nvarchar(max)'')'
from LogType as LT
for xml path('')
)+' end as Name from dbo.[Log] as L;';
exec (#SQL);
It will give you a query that looks like this:
select case L.logTypeID
when 1 then L.Contents.value('(/Patient/PatientName)[1]', 'nvarchar(max)')
when 2 then L.Contents.value('(/Clinic/ClinicName)[1]', 'nvarchar(max)')
end as Name
from dbo.[Log] as L;

Okay, so this has been here a while (a year), but this might be helpful...
A VERY helpful TABLE function: http://beyondrelational.com/modules/2/blogs/28/posts/10495/xquery-lab-58-select-from-xml.aspx
Using that function, you can get the values you're after something like this:
Select l.Contents, t.XPath, x.Value
From [log] l With (NoLock)
Inner Join [LogType] t With (NoLock)
on t.LogTypeID=l.LogTypeID
CROSS APPLY XMLTable(l.Contents) AS x
Where REPLACE(REPLACE(REPLACE(t.XPath,'[1]',''),'(',''),')','')=REPLACE('/'+x.XPath,'[1]','')

SQL Server does not allow replacing entire XPath expression with a variable, but you can use sql:variable and sql:column extensions inside the expression (I can't say how exactly without seeing your xml structure and what information you want to query from XML column).
Or, as mentioned above, you can use dynamic SQL:
DECLARE #xpath NVARCHAR(MAX);
SET #xpath = ... //Calculate xpath expression here
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'SELECT contents.value(''' + #xpath + ''', ''NVARCHAR(MAX)''
FROM dbo.Log
JOIN dbo.LogType ON dbo.Log.logTypeID = dbo.LogType.logTypeID';
EXEC sp_executesql #sql;

Related

Adventure Works 2019: Adding New Person to Person.Person Table

Right now I am using 3 stored procedures to Add a person to the person.person table. I would like to cut this down to a single stored procedure to resolve this issue.
INSERT new GUID and DateModified to table Person.BusinessEntity
SELECT the auto generate BusinessEntityID form table Person.BusinessEntity
INSERT new Person to Person.Person Table
The Stored Procedures all use parameters which I pass via a C# application and I have confirmed that the a user is in fact added to the AdventureWorks2019 Db.
Procedure: Person.CreateNewBusinessEntity
INSERT INTO [Person].[BusinessEntity]
(
[BusinessEntity].rowguid
, [BusinessEntity].ModifiedDate
)
VALUES
(
#RowGUID
, GetDate()
)
Procedure: Person.GetBusinessEntityID
SELECT
[BusinessEntityID]
FROM
[AdventureWorks2019].[Person].[BusinessEntity]
WHERE [rowguid] = #RowGuid
Procedure: Person.CreateNewPerson
INSERT INTO [Person].[Person]
(
[BusinessEntityID]
,[PersonType]
,[NameStyle]
,[Title]
,[FirstName]
,[MiddleName]
,[LastName]
,[Suffix]
,[EmailPromotion]
,[AdditionalContactInfo]
,[Demographics]
,[rowguid]
,[ModifiedDate]
)
VALUES
(
#BusinessEntityID
, #PersonType
, #NameStyle
, #Title
, #FirstName
, #MiddleName
, #LastName
, #Suffix
, #EmailPromotion
, #AdditionalContactInfo
, #Demographics
, #RowGUID
, GetDate()
)
Any help here is appreciated. Thanks!
Thanks to HABO, I am now using this solution. Now I only need two procedures.
DECLARE #Inserted table ( [BusinessEntityID] int );
INSERT INTO [Person].[BusinessEntity]
(
[BusinessEntity].rowguid
, [BusinessEntity].ModifiedDate
)
OUTPUT inserted.[BusinessEntityID] INTO #Inserted([BusinessEntityID])
VALUES
(
#RowGUID
, GetDate()
)
The below answer shows you how to get the inserted id and add it to the next insert in side the Same SP.
USE AdventureWorks2012
GO
CREATE PROC CreateNewPerson
AS
BEGIN
DECLARE #OutputTbl TABLE ([BusinessEntityID] INT, ModifiedDate DATETIME)
DECLARE #BusinessEntityID AS INT
INSERT INTO [Person].[BusinessEntity]
(
[BusinessEntity].rowguid
, [BusinessEntity].ModifiedDate
)
--Get the output value inserted to a table.
OUTPUT inserted.[BusinessEntityID], inserted.ModifiedDate INTO
#OutputTbl([BusinessEntityID],[ModifiedDate])
VALUES
(
NEWID()
, GetDate()
)
--Assigned to a variable. You can get this using subquery as well inside the insert statment.
SELECT #BusinessEntityID = [BusinessEntityID] FROM #OutputTbl
INSERT INTO [Person].[Person]
(
[BusinessEntityID]
,[PersonType]
,[NameStyle]
,[Title]
,[FirstName]
,[MiddleName]
,[LastName]
,[Suffix]
,[EmailPromotion]
,[AdditionalContactInfo]
,[Demographics]
,[rowguid]
,[ModifiedDate]
)
VALUES
(
#BusinessEntityID
, #PersonType --These columns with # sign needed to be declared or supply values
, #NameStyle
, #Title
, #FirstName
, #MiddleName
, #LastName
, #Suffix
, #EmailPromotion
, #AdditionalContactInfo
, #Demographics
, NEWID() --This will generate a new GUID for each row.
, GetDate()
)
END
GO
Second way is using Scope_identity(). Replace the
"SELECT #BusinessEntityID = [BusinessEntityID] FROM #OutputTbl'
from below lines in the Sp will do the same thing for you.
SELECT #BusinessEntityID = SCOPE_IDENTITY() --[BusinessEntityID] FROM #OutputTbl
Select #BusinessEntityID

How to insert data into a temporary table using an existing table and new columns

I am trying to insert data into a temporary table within my stored procedure. The data is selected from an existing table and creating new columns with concatenated data. I'm getting an error that the column name or number of supplied values does not match table definition. I'm pretty certain that the code in my application is correct so I believe the issue is with the way I'm storing the data in a temporary table.
Here is my proc:
AS
BEGIN
CREATE TABLE #TempTable
(
[ID] [varchar](10),
[FIRST_NAME] varchar(50),
[LAST_NAME] varchar(50),
[WEBSITE_LINK] varchar(200)
)
INSERT INTO #TempTable
SELECT USER.ID,USER.FIRSTNAME AS [FIRST_NAME], USER.LASTNAME AS
[LAST_NAME]
FROM USER
WHERE USER.Registered = 'Yes'
DECLARE #Link1 NVARCHAR(100)
DECLARE #Link2 VARCHAR(10)
DECLARE #Link3 NVARCHAR(4)
SET #Link1 = 'http://www.mywebsite.com/user/'
SET #Link2 = (SELECT USER.ID FROM USER WHERE USER.Registered =
'Yes')
SET #Link3 ='/document.doc'
SET #WEBSITE_LINK = (SELECT concat(#Link1,#Link2,#Link3 )AS
[WEBSITE_LINK])
DROP TABLE #TempTable
END
I think this is your problem:
SET #Link2 = (SELECT USER.ID FROM USER WHERE USER.Registered = 'Yes')
What if there are six of them? A single variable can't hold all of them. You can do:
SELECT TOP(1) #Link2 = USER.ID FROM USER WHERE USER.Registered = 'Yes' ORDER BY [SOMETHING];
If the goal is to create a temp table with a full [WEBSITE_LINK], you can do that without all those variables:
BEGIN
CREATE TABLE #TempTable
(
[ID] [varchar](10),
[FIRST_NAME] varchar(50),
[LAST_NAME] varchar(50),
[WEBSITE_LINK] varchar(200)
)
INSERT INTO #TempTable
SELECT DISTINCT u.ID
, [FIRST_NAME] = u.FIRSTNAME
, [LAST_NAME] = u.LASTNAME
, [WEBSITE_LINK] = 'http://www.mywebsite.com/user/' +
CAST(u.ID AS VARCHAR(10)) +
'/document.doc'
FROM [USER] u
WHERE u.Registered = 'Yes'
-- Do something with these values...
DROP TABLE #TempTable
END

Select on a table with 2 possible structures

I'm trying to write a query that will select data from a table. due to different versions of the database, there are 2 possible structures for the source table, where the newer version has 2 more fields than the old one.
I've tried identifying the older structure and replacing the columns with NULL and also tried writing 2 separate queries with and IF statement directing to the correct one. Neither of these solutions work and in both cases it seems that the SQL engine is failing on validating these 2 columns.
Examples of my attempted solutions:
IF NOT EXISTS (SELECT *
FROM sys.objects
WHERE object_id = Object_id(N'[dbo].[Test2]')
AND type IN ( N'U' ))
BEGIN
CREATE TABLE [dbo].[test2]
(
[id] [INT] IDENTITY(1, 1) NOT NULL,
[statusid] [INT] NULL
)
END
go
DECLARE #Flag INT = 0
IF EXISTS(SELECT 1
FROM sys.columns
WHERE NAME = N'TestId'
AND object_id = Object_id(N'dbo.Test2'))
SET #Flag = 1
--Solution #1
IF #Flag = 1
SELECT id,
statusid,
testid
FROM dbo.test2
ELSE
SELECT id,
statusid
FROM dbo.test2
--Solution #2
SELECT id,
statusid,
CASE
WHEN #Flag = 1 THEN testid
ELSE NULL
END AS TestId
FROM dbo.test2
you can use Dynamic SQL and generate the query accordingly depends on value of #flag
declare #sql nvarchar(max)
select #sql = N'select id, statusid, '
+ case when #flag = 1 then 'testid' else 'NULL' end + ' as testid'
+ ' from dbo.test2'
print #sql
exec sp_executesql #sql
But it will not be that easy to code and maintain Dynamic Query if you have a complex query.

Based on a variable: SQL Query to fetch data for multiple values from one column OR get all values from that column

Problem Statement :
when #a has a single word(Ex. 'name1') OR comma separated string (Example 'name1,name2,name3') then the query should return the manager names of employees with name1 and name2 and name3
when #a has an empty string then return the manager names of all the employees in the emp_master table
I have defined a stored procedure where I pass a variable.
This variable can be a comma separated string, a single word or an empty string.
If the string is comma separated then I split that string and get values based on the return table of split statement
else
I get the related value of the non comma separated data using normal subquery
I have tried to achieve this in the following way
Declare #a varchar(50)= ''
select emp.Name from
emp_master emp
where
(LEN(#a)=0 AND emp.Name in
(
SELECT DISTINCT [Name] FROM
[dbo].[Emp_Master] WHERE [EmpId] IN
(
SELECT
DISTINCT [MGR_ID]
FROM [dbo].[Emp_Master]
)
)
)
OR
emp.Name in (Select * from [dbo].[SplitString](#a, ','))
Details for the above sample:
[dbo].[SplitString] - custom written function : returns a table of split values. So
Select * from [dbo].SplitString
will return
SplitTable
----------
name1
name2
name3
and
Select * from [dbo].[SplitString](',','name1')
will return
SplitTable
----------
name1
[dbo].[Emp_Master] contains data for all the employees
[MGR_ID] is the column which has the employeeID of the employee manager
#a is the input variable
The Database is MS SQL 2008
My current solution(the above insane query) solves my purpose but it is very slow, it would be helpful to get an optimized and faster working solution for the problem
Emp_master Table has 400 000 rows, 30 columns
There are 18 000 managers in that table
CREATE NONCLUSTERED INDEX ix ON dbo.Emp_Master ([MGR_ID])
GO
DECLARE #a VARCHAR(50) = ''
DECLARE #t TABLE (val VARCHAR(50) PRIMARY KEY WITH(IGNORE_DUP_KEY=ON))
INSERT INTO #t
SELECT item = t.c.value('.', 'INT')
FROM (
SELECT txml = CAST('<r>' + REPLACE(#a, ',', '</r><r>') + '</r>' AS XML)
) r
CROSS APPLY txml.nodes('/r') t(c)
SELECT /*DISTINCT*/ [Name]
FROM dbo.Emp_Master e1
WHERE (
#a = ''
AND
e1.[EmpId] IN (SELECT DISTINCT MGR_ID FROM dbo.Emp_Master)
)
OR (
#a != ''
AND
e.Name IN (SELECT * FROM #t)
)
OPTION(RECOMPILE)
TRY THIS
CREATE NONCLUSTERED INDEX IX_MGR_ID_Emp_Master ON dbo.Emp_Master ([MGR_ID])
GO
Create Procedure searchname (#a varchar(255))
as
IF (#a = '')
BEGIN
EXEC Searchname1 #a
END
ELSE
BEGIN
EXEC Searchname2 #a
END
GO
Create Procedure Searchname1 (#a varchar(255))
AS
SELECT DISTINCT [Name] FROM
[dbo].[Emp_Master] m1 WHERE
exists
(
SELECT
*
FROM [dbo].[Emp_Master] m2
WHERE
m1.[EmpId]= m2.[MGR_ID]
)
GO
Create Procedure Searchname2 (#a varchar(max))
AS
Select #a = ' SELECT '''+replace( #a,',',''' Union ALL SELECT ''')+' '''
Create table #names (name varchar(255))
insert into #names
EXEC ( #a )
select emp.Name from
emp_master emp
WHERE
emp.Name in( Select name FRom #names)
option (recompile)
IF YOU ARE ALREADY DEALING WITH SQL INJECTION AT APPLICATION LEVEL
THEN
ALTER procedure [dbo].[Searchname2] (#a varchar(max))
AS
select #a = ''''+replace ( #a,',',''',''')+''''
DECLARE #sql NVARCHAR(MAX) = N'
select distinct emp.Name from
emp_master emp
WHERE
emp.Name in( '+#a+')'
EXEC (#sql)

Generate CREATE TABLE statement from arbitrary SQL query

Is there script/procedure/function that can generate a CREATE TABLE statement from an arbitrary SQL query?
When building procedures, I'd like to have a quick way to generate a temporary table, rather than having to review the table definitions of all of the tables referenced in the query.
Simple example:
SELECT p.pat_id, pat_name,
enc_id, admsn_time, disch_time
FROM patient p
INNER JOIN encounter e ON p.pat_id=e.pat_id
WHERE admsn_time >= '01/01/2014'
Would generate (columns' data definition is retrieved from the system table):
-- randomly-generated table name
CREATE TABLE #random_name (
PAT_ID VARCHAR(18) NOT NULL,
PAT_NAME VARCHAR(200),
ENC_ID NUMERIC(18,0) NOT NULL,
ADMSN_TIME DATE,
DISCH_TIME DATE
)
SSMS workflow:
select the text
right click, select Generate CREATE TABLE statement (function/script/proc called; result placed on clipboard)
place cursor in desired location
paste
This is a bit of a hack, but you could try selecting into a table (see line #3):
SELECT p.pat_id, pat_name,
enc_id, admsn_time, disch_time
INTO delete_me
FROM patient p
INNER JOIN encounter e ON p.pat_id=e.pat_id
WHERE admsn_time >= '01/01/2014'
Then you can highlight the delete_me table in SSMS, right-click, and generate the CREATE TABLE script.
Finally, you'd want to DROP TABLE delete_me to clean up.
use
SELECT p.pat_id, pat_name,
enc_id, admsn_time, disch_time
into #randomtable
FROM patient p
INNER JOIN encounter e ON p.pat_id=e.pat_id
WHERE admsn_time >= '01/01/2014'
and your table will be created when you execute the statement.
First of all, I would make a stored procedure of the query. (It keeps me from forgetting it later)
Secondly, I would write a query to generate the table for me:
DECLARE #CREATE_TABLE_QUERY NVARCHAR(MAX) = N'';
SELECT
#CREATE_TABLE_QUERY += ', ' + name + ' ' + UPPER(system_type_name) + CHAR(13) + CHAR(10) + CHAR(9)
FROM
sys.dm_exec_describe_first_result_set('YOUR_PROCEDURE_NAME_HERE', NULL, 1);
SELECT
#CREATE_TABLE_QUERY = N'CREATE TABLE TABLE_NAME_HERE(' + CHAR(13) + CHAR(10) + CHAR(9) + STUFF(#CREATE_TABLE_QUERY, 1, 1, N'') + ');';
PRINT #CREATE_TABLE_QUERY;
Note: Replace 'YOUR_PROCEDURE_NAME_HERE' with the name of your own stored procedure.
Note: Replace TABLE_NAME_HERE with the table name of your choice.
The above will generate something like this:
CREATE TABLE TABLE_NAME_HERE(
WeekName VARCHAR(40)
, Line Name VARCHAR(50)
, TheDate DATETIME
, ReceivedAll INT
, Answered INT
, Abandoned INT
, Call Length INT
, WaitTimeAnswer INT
, WaitTimeAbandon INT
, PeriodName VARCHAR(10)
, Week SMALLINT
, Period SMALLINT
, Year SMALLINT
, WeekInPeriod SMALLINT
, NumWeeksInPeriod SMALLINT
, WeekendDate DATETIME
, CRCOperative VARCHAR(100)
, CallType VARCHAR(20)
, Charge Time INT
, SourceNumber VARCHAR(80)
, DestinationNumber VARCHAR(80)
, CallStart DATETIME
, Out of Hours VARCHAR(12)
, IsWorkingDay BIT
);

Resources