How to select JSON Properties as column using T-SQL - json

I have a table with a JSON column. I want to select JSON properties as column. The property names will be unknown. So I have to use dynamic SQL. Based on this SO suggestion, I was able to get properties.
CREATE TABLE [Templates]
(
[ID] [INT] NOT NULL,
[Template] [NVARCHAR](MAX)
)
INSERT INTO Templates(ID,Template)
VALUES (1, '{"FirName":"foo"}'),
(2, '{"FirName":"joe","LastName":"dow"}'),
(3, '{"LastName":"smith","Address":"1234 Test Drive"}'),
(4, '{"City":"New York"}')
// SELECT Keys
SELECT DISTINCT(j.[key])
FROM Templates T
CROSS APPLY OPENJSON(T.Template) AS j
How do I create fitting statement/WITH-clause dynamically to select properties as column? If property doesn't exist then it should return null
SQL FIDDLE

Another possible approach is to use OPENJSON() with dynamically generated WITH clause. Note, that in this case you need to use lax mode in the path expression to guarantee that OPENJSON() doesn't raise an error if the object or value on the specified path can't be found.
Table:
CREATE TABLE [Templates](
[ID] [int] NOT NULL,
[Template] [nvarchar](max)
)
INSERT INTO Templates(ID,Template)
VALUES
(1,'{"FirName":"foo"}'),
(2,'{"FirName":"joe","LastName":"dow"}'),
(3,'{"LastName":"smith","Address":"1234 Test Drive"}'),
(4,'{"City":"New York"}')
Statement:
DECLARE #stm nvarchar(max) = N''
-- Dynamic explicit schema (WITH clause)
SELECT #stm = CONCAT(
#stm,
N', [',
[key],
N'] nvarchar(max) ''lax $."',
[key],
'"'''
)
FROM (
SELECT DISTINCT j.[key] FROM Templates t
CROSS APPLY OPENJSON(T.Template) AS j
) cte
-- Statement
SELECT #stm = CONCAT(
N'SELECT j.* ',
N'FROM Templates t ',
N'CROSS APPLY OPENJSON(t.Template) WITH (',
STUFF(#stm, 1, 2, N''),
N') j '
)
-- Execution
PRINT #stm
EXEC sp_executesql #stm
Output:
--------------------------------------------
Address City FirName LastName
--------------------------------------------
foo
joe dow
1234 Test Drive smith
New York

Dynamic columns would require Dynamic SQL. If the desired columns are known, you can use a simple pivot or even a conditional aggregation.
Example
Declare #SQL varchar(max)= stuff((Select ','+QuoteName([key])
From (SELECT DISTINCT(j.[key]) FROM Templates T
CROSS APPLY OPENJSON(T.Template) AS j) A
Order By 1
For XML Path('')),1,1,'')
Set #SQL = '
Select *
From (
Select T.ID
,j.[Key]
,j.[Value]
From Templates T
Cross Apply OpenJSON(T.Template) AS j
) src
Pivot ( max(value) for [Key] in ('+ #SQL+') ) pvt
'
Exec(#SQL)
EDIT - If you don't want ID in the Final Results
Declare #SQL varchar(max)= stuff((Select ','+QuoteName([key])
From (SELECT DISTINCT(j.[key]) FROM Templates T
CROSS APPLY OPENJSON(T.Template) AS j) A
Order By 1
For XML Path('')),1,1,'')
Set #SQL = '
Select '+#SQL+'
From (
Select T.ID
,j.[Key]
,j.[Value]
From Templates T
Cross Apply OpenJSON(T.Template) AS j
) src
Pivot ( max(value) for [Key] in ('+ #SQL+') ) pvt
'
Exec(#SQL)

Related

Get Dynamic PIVOT result executed with in EXEC() to a varchar variable

Below code creates a JSON output.
Note that : The values inside #Person table are generating dynamically. Once I call the stored proc which contains the below code segment, the C# code identifies the output as a JSON eventhough it looks like a VARCHAR.I need the output JSON to be a string(VARCHAR) since the C# model cannot be generated dynamically based on the values returned by #Names.
DROP TABLE #Names
DROP TABLE #PersonInfo
CREATE TABLE #Names (ID INT,Name VARCHAR(MAX))
CREATE TABLE #PersonInfo (ID INT,NameID INT,Subject VARCHAR(100),Marks INT)
INSERT INTO #Names VALUES (1,'Paul');
INSERT INTO #Names VALUES (2,'John');
INSERT INTO #Names VALUES (3,'Tayler');
INSERT INTO #PersonInfo VALUES (1,1,'Maths',95);
INSERT INTO #PersonInfo VALUES (2,2,'Science',32);
INSERT INTO #PersonInfo VALUES (3,3,'History',23);
INSERT INTO #PersonInfo VALUES (4,2,'Maths',32);
INSERT INTO #PersonInfo VALUES (5,3,'Science',60);
INSERT INTO #PersonInfo VALUES (6,1,'Music',60);
DECLARE #DynamicCols NVARCHAR(MAX) = '';
DECLARE #pvt NVARCHAR(MAX) = '';
DECLARE #SQLQuery NVARCHAR(MAX) = '';
SELECT #DynamicCols += ', SUM(' +QUOTENAME([Name])+') AS '+[Name] FROM #Names;
SET #DynamicCols = STUFF(#DynamicCols,1,1,'')
SELECT #pvt += ', ' +QUOTENAME([Name]) FROM #Names;
SET #pvt = STUFF(#pvt,1,1,'')
EXEC ('
SELECT [Subject],' + #DynamicCols+'
FROM (SELECT [NameID], [Subject], [Marks] FROM #PersonInfo) a
INNER JOIN #Names b ON a.NameID = b.ID
PIVOT
(
SUM([Marks])
FOR [Name] IN ('+ #pvt+')
) PIV
GROUP BY [Subject] FOR JSON AUTO, INCLUDE_NULL_VALUES');
Result
[{"Subject":"History","Paul":null,"John":null,"Tayler":23},{"Subject":"Maths","Paul":95,"John":32,"Tayler":null},{"Subject":"Music","Paul":60,"John":null,"Tayler":null},{"Subject":"Science","Paul":null,"John":32,"Tayler":60}]
C# Code in the DAL Layer
public string GetMarks(string partyRoleIdList)
{
return _context.Query<string>(usp_GetMarks, new { IdList }).SingleOrDefault();
}
Is there a way to convert the executed value (EXEC()) to a VARCHAR(MAX) ??
You can easily test that the result of FOR JSON is NVARCHAR(MAX) string:
exec sp_describe_first_result_set N'
SELECT *
FROM
(
VALUES(''[a'')
) DS(col)
FOR JSON AUTO
';
Yields this:
is_hidden column_ordinal name is_nullable system_type_id system_type_name max_length precision scale collation_name
0 1 JSON_F52E2B61-18A1-11d1-B105-00805F49916B 1 231 nvarchar(max) -1 0 0 SQL_Latin1_General_CP1_CI_AS
And it is string, so you can cast it to VARCHAR(MAX) or something else easily:
EXEC ('
SELECT CAST(
(
SELECT [Subject],' + #DynamicCols+'
FROM (SELECT [NameID], [Subject], [Marks] FROM #PersonInfo) a
INNER JOIN #Names b ON a.NameID = b.ID
PIVOT
(
SUM([Marks])
FOR [Name] IN ('+ #pvt+')
) PIV
GROUP BY [Subject] FOR JSON AUTO, INCLUDE_NULL_VALUES)
AS VARCHAR(400))');

How to get a specific column by its ordinal position in table using SQL? [duplicate]

Is there any SQL lingo to return JUST the first two columns of a table WITHOUT knowing the field names?
Something like
SELECT Column(1), Column(2) FROM Table_Name
Or do I have to go the long way around and find out the column names first? How would I do that?
You have to get the column names first. Most platforms support this:
select column_name,ordinal_position
from information_schema.columns
where table_schema = ...
and table_name = ...
and ordinal_position <= 2
There it´s
declare #select varchar(max)
set #select = 'select '
select #select=#select+COLUMN_NAME+','
from information_schema.columns
where table_name = 'TABLE' and ordinal_position <= 2
set #select=LEFT(#select,LEN(#select)-1)+' from TABLE'
exec(#select)
A dynamic query using for xml path will also do the job:
declare #sql varchar(max)
set #sql = (SELECT top 2 COLUMN_NAME + ',' from information_schema.columns where table_name = 'YOUR_TABLE_NAME_HERE' order by ordinal_position for xml path(''))
set #sql = (SELECT replace(#sql +' ',', ',''))
exec('SELECT ' + #sql + ' from YOUR_TABLE_NAME_HERE')
I wrote a stored procedure a while back to do this exact job. Even though in relational theory there is no technical column order SSMS is not completely relational. The system stores the order in which the columns were inserted and assigns an ID to them. This order is followed using the typical SELECT * statement which is why your SELECT statements appear to return the same order each time. In practice its never a good idea to SELECT * with anything as it doesn't lock the result order in terms of columns or rows. That said I think people get so stuck on 'you shouldn't do this' that they don't write scripts that actually can do it. Fact is there is predictable system behavior so why not use it if the task isn't super important.
This SPROC of course has caveats and is written in T-SQL but if your looking to just return all of the values with the same behavior of SELECT * then this should do the job pretty easy for you. Put in your table name, the amount of columns, and hit F5. It returns them in order from left to right the same as you'd be expecting. I limited it to only 5 columns but you can edit the logic if you need any more. Takes both temp and permanent tables.
EXEC OnlySomeColumns 'MyTable', 3
/*------------------------------------------------------------------------------------------------------------------
Document Title: The Unknown SELECT SPROC.sql
Created By: CR
Date: 4.28.2013
Purpose: Returns all results from temp or permanent table when not knowing the column names
SPROC Input Example: EXEC OnlySomeColumns 'MyTable', 3
--------------------------------------------------------------------------------------------------------------------*/
IF OBJECT_ID ('OnlySomeColumns', 'P') IS NOT NULL
DROP PROCEDURE OnlySomeColumns;
GO
CREATE PROCEDURE OnlySomeColumns
#TableName VARCHAR (1000),
#TotalColumns INT
AS
DECLARE #Column1 VARCHAR (1000),
#Column2 VARCHAR (1000),
#Column3 VARCHAR (1000),
#Column4 VARCHAR (1000),
#Column5 VARCHAR (1000),
#SQL VARCHAR (1000),
#TempTable VARCHAR (1000),
#PermanentTable VARCHAR (1000),
#ColumnNamesAll VARCHAR (1000)
--First determine if this is a temp table or permanent table
IF #TableName LIKE '%#%' BEGIN SET #TempTable = #TableName END --If a temporary table
IF #TableName NOT LIKE '%#%' BEGIN SET #PermanentTable = #TableName END --If a permanent column name
SET NOCOUNT ON
--Start with a few simple error checks
IF ( #TempTable = 'NULL' AND #PermanentTable = 'NULL' )
BEGIN
RAISERROR ( 'ERROR: Please select a TempTable or Permanent Table.',16,1 )
END
IF ( #TempTable <> 'NULL' AND #PermanentTable <> 'NULL' )
BEGIN
RAISERROR ( 'ERROR: Only one table can be selected at a time. Please adjust your table selection.',16,1 )
END
IF ( #TotalColumns IS NULL )
BEGIN
RAISERROR ( 'ERROR: Please select a value for #TotalColumns.',16,1 )
END
--Temp table to gather the names of the columns
IF Object_id('tempdb..#TempName') IS NOT NULL DROP TABLE #TempName
CREATE TABLE #TempName ( ID INT, Name VARCHAR (1000) )
--Select the column order from a temp table
IF #TempTable <> 'NULL'
BEGIN
--Verify the temp table exists
IF NOT EXISTS ( SELECT 1
FROM tempdb.sys.columns
WHERE object_id = object_id ('tempdb..' + #TempTable +'') )
BEGIN
RAISERROR ( 'ERROR: Your TempTable does not exist - Please select a valid TempTable.',16,1 )
RETURN
END
SET #SQL = 'INSERT INTO #TempName
SELECT column_id AS ID, Name
FROM tempdb.sys.columns
WHERE object_id = object_id (''tempdb..' + #TempTable +''')
ORDER BY column_id'
EXEC (#SQL)
END
--From a permanent table
IF #PermanentTable <> 'NULL'
BEGIN
--Verify the temp table exists
IF NOT EXISTS ( SELECT 1
FROM syscolumns
WHERE id = ( SELECT id
FROM sysobjects
WHERE Name = '' + #PermanentTable + '' ) )
BEGIN
RAISERROR ( 'ERROR: Your Table does not exist - Please select a valid Table.',16,1 )
RETURN
END
SET #SQL = 'INSERT INTO #TempName
SELECT colorder AS ID, Name
FROM syscolumns
WHERE id = ( SELECT id
FROM sysobjects
WHERE Name = ''' + #PermanentTable + ''' )
ORDER BY colorder'
EXEC (#SQL)
END
--Set the names of the columns
IF #TotalColumns >= 1 BEGIN SET #Column1 = (SELECT Name FROM #TempName WHERE ID = 1) END
IF #TotalColumns >= 2 BEGIN SET #Column2 = (SELECT Name FROM #TempName WHERE ID = 2) END
IF #TotalColumns >= 3 BEGIN SET #Column3 = (SELECT Name FROM #TempName WHERE ID = 3) END
IF #TotalColumns >= 4 BEGIN SET #Column4 = (SELECT Name FROM #TempName WHERE ID = 4) END
IF #TotalColumns >= 5 BEGIN SET #Column5 = (SELECT Name FROM #TempName WHERE ID = 5) END
--Create a select list of only the column names you want
IF Object_id('tempdb..#FinalNames') IS NOT NULL DROP TABLE #FinalNames
CREATE TABLE #FinalNames ( ID INT, Name VARCHAR (1000) )
INSERT #FinalNames
SELECT '1' AS ID, #Column1 AS Name UNION ALL
SELECT '2' AS ID, #Column2 AS Name UNION ALL
SELECT '3' AS ID, #Column3 AS Name UNION ALL
SELECT '4' AS ID, #Column4 AS Name UNION ALL
SELECT '5' AS ID, #Column5 AS Name
--Comma Delimite the names to insert into a select statement. Bracket the names in case there are spaces
SELECT #ColumnNamesAll = COALESCE(#ColumnNamesAll + '], [' ,'[') + Name
FROM #FinalNames
WHERE Name IS NOT NULL
ORDER BY ID
--Add an extra bracket at the end to complete the string
SELECT #ColumnNamesAll = #ColumnNamesAll + ']'
--Tell the user if they selected to many columns
IF ( #TotalColumns > 5 AND EXISTS (SELECT 1 FROM #FinalNames WHERE Name IS NOT NULL) )
BEGIN
SELECT 'This script has been designed for up to 5 columns' AS ERROR
UNION ALL
SELECT 'Only the first 5 columns have been selected' AS ERROR
END
IF Object_id('tempdb..#FinalNames') IS NOT NULL DROP TABLE ##OutputTable
--Select results using only the Columns you wanted
IF #TempTable <> 'NULL'
BEGIN
SET #SQL = 'SELECT ' + #ColumnNamesAll + '
INTO ##OutputTable
FROM ' + #TempTable + '
ORDER BY 1'
EXEC (#SQL)
END
IF #PermanentTable <> 'NULL'
BEGIN
SET #SQL = 'SELECT ' + #ColumnNamesAll + '
INTO ##OutputTable
FROM ' + #PermanentTable + '
ORDER BY 1'
EXEC (#SQL)
END
SELECT *
FROM ##OutputTable
SET NOCOUNT OFF
SQL doesn't understand the order of columns. You need to know the column names to get them.
You can look into querying the information_schema to get the column names. For example:
SELECT column_name
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'tbl_name'
ORDER BY ordinal_position
LIMIT 2;
You can query the sysobject of the table to find out the first two column then dynamically generate the SQL statement you need.
If you want a permant object that you can query over and over again make a view for each table that only returns the first 2 columns. You can name the columns Column1 and Column2 or use the existing names.
If you want to return the first two columns from any table without any preprocessing steps create a stored procedure that queries the system information and executes a dynamic query that return the first two columns from the table.
Or do I have to go the long way around and find out the column names first? How would I do that?
It's pretty easy to do manually.
Just run this first
select * from tbl where 1=0
This statement works on all major DBMS without needing any system catalogs.
That gives you all the column names, then all you need to do is type the first two
select colname1, colnum2 from tbl

Using CTE with a dynamic pivot

I'm trying to use This question to perform a dynamic pivot, but I want to use a CTE to get the initial data.
My query looks like this:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX);
WITH dataSet (coDate, TransactionDate, TotalBalance, TransDate, collected)
AS
( *SELECT STATEMENT )
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(c.category)
FROM dataSet c
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT coDate, ' + #cols + ' from
(
select coDate
, TotalBalance
, collected
, TransDate
from dataSet
) x
pivot
(
SUM(collected)
for category in (' + #cols + ')
) p '
execute(#query)
And the error SQL gives me is Incorrect syntax near the keyword 'SET'. I did try adding a semicolon and go as well as a comma before the SET statement, but this the first time I've used PIVOT so I'm not sure how CTE interacts with it.

Create a function to split delimited string into a table

In order to split delimited string, I would need the creation of a function returning a table and taking a comma separated string as parameter.
For instance, have a look at this input table:
Using the function, the result would be:
In this case, the delimited string is the Person column that should be taken as parameter.
Thanks in advance.
Try this one
--Build Schema for Your Case
DECLARE #TAB TABLE (ID INT, NAME VARCHAR(1000))
INSERT INTO #TAB
SELECT 1,'Pravesh,Simon'
UNION ALL
SELECT 2,'Me,Myself,I'
--Used Recursive CTE to achieve Tabular view of Comma Separated Column
--Used While loop to take next row
--Used Table Variable #RESULT to store your Result Set
DECLARE #ID INT, #T VARCHAR(100), #SNO INT= 1, #MAX_SNO INT
DECLARE #RESULT TABLE (ID INT, NAME VARCHAR(1000))
SELECT #SNO=1, #MAX_SNO =MAX(ID) FROM #TAB
WHILE(#SNO<=#MAX_SNO)
BEGIN
SELECT #ID = ID, #T = NAME FROM #TAB WHERE ID=#SNO
SET #T =#T+','
;WITH MyCTE(Start,[End]) AS(
SELECT 1 AS Start,CHARINDEX(',',#T,1) AS [End]
UNION ALL
SELECT [End]+1 AS Start,CHARINDEX(',',#T,[End]+1)AS [End]
from MyCTE where [End]<LEN(#T)
)
INSERT INTO #RESULT
Select #SNO AS ID, SUBSTRING(#T,Start,[End]-Start) NAME from MyCTE;
SET #SNO+=1
END
--Getting Result Set
SELECT * FROM #RESULT
Edit: From Your Comments
If you want to do the above with a function, this one far easy than above looping.
CREATE FUNCTION [DBO].[FN_SPLIT_STR_TO_COL] (#T AS VARCHAR(4000) )
RETURNS
#RESULT TABLE(VALUE VARCHAR(250))
AS
BEGIN
SET #T= #T+','
;WITH MYCTE(START,[END]) AS(
SELECT 1 AS START,CHARINDEX(',',#T,1) AS [END]
UNION ALL
SELECT [END]+1 AS START,CHARINDEX(',',#T,[END]+1)AS [END]
FROM MYCTE WHERE [END]<LEN(#T)
)
INSERT INTO #RESULT
SELECT SUBSTRING(#T,START,[END]-START) NAME FROM MYCTE;
RETURN
END
Now just call the function for every row by passing column to it.
With Cross Apply
SELECT ID,FN_RS.VALUE FROM #TAB
CROSS APPLY
(SELECT * FROM [DBO].[FN_SPLIT_STR_TO_COL] (NAME)) AS FN_RS
TRY THIS QUERY
//Which will work only when having 3 comma only
SELECT Id,SUBSTRING_INDEX(SUBSTRING_INDEX(`Person`, ',', 1), ',', -1) as Person
FROM `table`
UNION
select Id,SUBSTRING_INDEX(SUBSTRING_INDEX(`Person`, ',', 2), ',', -1) as Person
FROM `table`
UNION
select Id,SUBSTRING_INDEX(SUBSTRING_INDEX(`Person`, ',', 3), ',', -1) as Person
FROM `table`
order by Id ASC
create function [dbo].[udf_splitstring] (#tokens varchar(max),
#delimiter varchar(5))
returns #split table (
token varchar(200) not null )
as
begin
declare #list xml
select #list = cast('<a>'
+ replace(#tokens, #delimiter, '</a><a>')
+ '</a>' as xml)
insert into #split
(token)
select ltrim(t.value('.', 'varchar(200)')) as data
from #list.nodes('/a') as x(t)
return
end
create table #dup
( id int, name varchar(100)
)
insert into #dup values
(1,'leela,venkatesh,don'),
(2,'john,smith,lewis')
select id,b.token from #dup
cross apply
(select token from udf_splitstring(name,',') )b

How to match any value of search string from a column containing multiple values separated by space in table in sql?

I have a column in table which has multiple values separated by space.
i want to return those rows which has any of the matching values from search string.
Eg:
search string= 'mumbai pune'
This need to return rows matching word 'mumbai' or 'pune' or matching both
Declare #str nvarchar(500)
SET #str='mumbai pune'
create table #tmp
(
ID int identity(1,1),
citycsv nvarchar(500)
)
insert into #tmp(citycsv)Values
('mumbai pune'),
('mumbai'),
('nagpur')
select *from #tmp t
select *from #tmp t
where t.citycsv like '%'+#str+'%'
drop table #tmp
Required Out put:
ID CityCSV
1 mumbai pune
2 mumbai
You can use a splitter function to split your search string out as a table contain the desired search keys. Then you can join your main table with the table containing the search key using the LIKE statement.
For completeness I have included an example of a string splitter function, however there are plenty of example here on SO.
Example string splitter function:
CREATE FUNCTION [dbo].[SplitString]
(
#string NVARCHAR(MAX),
#delimiter CHAR(1)
)
RETURNS #output TABLE(splitdata NVARCHAR(MAX)
)
BEGIN
DECLARE #start INT, #end INT
SELECT #start = 1, #end = CHARINDEX(#delimiter, #string)
WHILE #start < LEN(#string) + 1 BEGIN
IF #end = 0
SET #end = LEN(#string) + 1
INSERT INTO #output (splitdata)
VALUES(SUBSTRING(#string, #start, #end - #start))
SET #start = #end + 1
SET #end = CHARINDEX(#delimiter, #string, #start)
END
RETURN
END
The following query demonstrates how the string splitter function can be combined with regular expressions to get the desired result:
SELECT DISTINCT
C.ID
,C.citycsv
FROM #tmp C
INNER JOIN (
SELECT splitdata + '[ ]%' AS MatchFirstWord -- Search pattern to match the first word in the string with the target search word.
,'%[ ]' + splitdata AS MatchLastWord -- Search pattern to match the last word in the string with the target search word.
,'%[ ]' + splitdata + '[ ]%' AS MatchMiddle -- Search pattern to match any words in the middle of the string with the target search word.
,splitdata AS MatchExact -- Search pattern for exact match.
FROM dbo.SplitString(#str, ' ')
) M ON (
(C.citycsv LIKE M.MatchFirstWord) OR
(C.citycsv LIKE M.MatchLastWord) OR
(C.citycsv LIKE M.MatchMiddle) OR
(C.citycsv LIKE M.MatchExact)
)
ORDER BY C.ID
Another approach , by using ReplaceFunction
Its syntax as following:
REPLACE ( string_expression , string_pattern , string_replacement )
so we could reach the target via replacing the every space that separated the values with the next pattern
'%'' OR t.citycsv like ''%'
An example:
Declare #str nvarchar(500),
#Where nvarchar (1000),
#Query nvarchar (4000)
SET #str='mumbai pune'
create table #tmp
(
ID int identity(1,1),
citycsv nvarchar(500)
)
insert into #tmp(citycsv)Values
('mumbai pune'),
('mumbai'),
('nagpur')
select * from #tmp t
Set #Where = 'where t.citycsv like ' + '''%'+ replace (RTRIM(LTRIM(#str)), ' ', '%'' OR t.citycsv like ''%') +'%'''
Set #Query = 'select * from #tmp t ' + #Where
execute sp_executesql #Query
drop table #tmp
The Result: