Select from list of tables in SQL Server - sql

I have a list of tables and I ultimately want to build up some JSON that looks like this:
{
"Table1": [
{ "col1": "value", "col2": "value" },
{ "col1": "value", "col2": "value" },
{ "col1": "value", "col2": "value" },
{ "col1": "value", "col2": "value" }
],
"Table2": [
{ "col1": "value", "col2": "value" },
{ "col1": "value", "col2": "value" },
{ "col1": "value", "col2": "value" },
{ "col1": "value", "col2": "value" }
],
"Table3": [
{ "col1": "value", "col2": "value" },
{ "col1": "value", "col2": "value" },
{ "col1": "value", "col2": "value" },
{ "col1": "value", "col2": "value" }
]
}
Creating the JSON is not the problem for me (although I can't use FOR JSON on SQL Server 2014) and I even have a naive implementation for the SQL:
declare #tables table (
tabname varchar(100)
)
insert into #tables(tabname)
select 'Table1' union all
select 'Table2' union all
select 'Table3'
/* Master dataset */
select tabname from #tables
declare tabcur cursor for select tabname from #tables
open tabcur
declare #tabname varchar(100)
fetch tabcur into #tabname
while ##FETCH_STATUS = 0
begin
/* Detail dataset */
execute ('select * from [' + #tabname + ']')
fetch tabcur into #tabname
end
close tabcur
deallocate tabcur
The idea behind this is that I get a list of tables as master dataset and for each row I also get a detail dataset. I can match those by index, or eliminate the master dataset by adding the table name to the detail dataset.
Whichever will work. But my actual list contains 724 tables and the performance is lousy.
So then I changed it to build a SQL string of union all statements instead and only execute that string after the cursor is deallocated. That mostly fixed my performance problem.
But it still seems like a very nineties way of doing this. With all the interesting things that SQL Server has added since then, I would imagine there must be a simpler way. I am basically looking to use the master dataset as input to a second dataset that contains the rows from all the tables (the columns I'm interested in are the same in all the tables, so they will work as one result set).
My next attempt was to create a function that accepts the table name as parameter and returns the result set. I thought I could cross apply to that:
select tabname, result.col1, result.col2
from #tables
cross apply dbo.SelectFromTable(#tables.tabname) result
But that was stillborn, because SQL Server does not allow dynamic SQL within a function.
So is there a well-performing way to do this without a cursor?

Its not pretty and you really should be looking at a different design, but if you are stuck doing it this way you can avoid the cursor with for xml (which is used to put all rows into one delimited column) and stuff (which is used to remove the leading delimiter, in this case ' union all ':
declare #tables table (
tabname varchar(100)
);
insert into #tables(tabname)
select 'Table1' union all
select 'Table2' union all
select 'Table3';
select stuff((select ' union all select * from [' + tabname + ']'
from #tables
for xml path('')
)
,1,11,'') as query
Which outputs:
select * from [Table1] union all select * from [Table2] union all select * from [Table3]

Related

Convert nested JSON to SQL table in SQL Server

I am trying to convert a JSON file into a SQL table in SQL Server and running across an error that I cannot seem to find a solution for to save my life.
In addition to the error resolution, I would also like to go a step further and breakdown the employee ID values to not be in a single cell but rather split into separate rows.
Below is the code I am using which includes a sample JSON structure:
drop table if exists newtable1;
declare #json NVARCHAR(MAX)
--select #json=BulkColumn
--from openrowset (bulk 'C:\Users\hamza\Documents\Price Transparency\banking_sample.json',single_clob) as j;
= N'{
"comapny_name": "chase",
"company_type": "banking",
"last_updated_on": "2020-08-27",
"institutional":[{
"region_id": 1,
"groups": [{
"employee_id": [1111111111, 2222222222, 3333333333, 4444444444, 5555555555],
"site":{
"type": "atm",
"id": "11-1111111"
}
},{
"employee_id": [1111111111, 2222222222, 3333333333, 4444444444, 5555555555],
"site":{
"type": "branch",
"id": "22-2222222"
}
}]
},{
"region_id": 2,
"location": "new york city, ny"
}]
}';
select
JSON_VALUE(a.value,'$.company_name') as company_name,
JSON_VALUE(a.value,'$.company_type') as reporting_entity_type,
JSON_VALUE(a.value,'$.last_updated_on') as plan_name,
JSON_VALUE(b.value,'$.region_id') as prov_grp_id,
JSON_VALUE(b.value,'$.location') as loc,
JSON_VALUE(c.value,'$.employee_id') as npi,
JSON_VALUE(c.value,'$.site.type') as tin_type,
JSON_VALUE(c.value,'$.site.id') as tin_value
into newtable1
from openjson (#json) as a
cross apply openjson(a.value,'$.institutional') as b
cross apply openjson(b.value,'$.groups') as c;

Using JSON_VALUE for parse column in SQL Server table

I have never worked with JSON in SQL Server before that's why need some help.
I have written a simple snippet of code:
DECLARE #json NVARCHAR(4000)
SET #json =
N'{
"id":"40476",
"tags":[
{
"id":"5f5883",
},
{
"id":"5fc8",
}
],
"type":"student",
"external_id":"40614476"
}'
SELECT
JSON_value(#json, '$.tags[0].id') as tags
In sample above I write code how get first "id" from "tags".
But how looks like script if in "tags" not 2 "id", but an unknown number this "id" and result should be in column like this:
1 5f5883
2 5fc8
You may use OPENJSON() with explicit schema to parse the $.tags JSON array:
DECLARE #json NVARCHAR(4000)
SET #json =
N'{
"id":"40476",
"tags":[
{
"id":"5f5883"
},
{
"id":"5fc8"
}
],
"type":"student",
"external_id":"40614476"
}'
SELECT id
FROM OPENJSON(#json, '$.tags') WITH (id varchar(10) '$.id')
Result:
id
------
5f5883
5fc8
If you want to get the index of each id in the $.tags JSON array, then you need a combination of OPENJSON() with default schema and JSON_VALUE():
SELECT CONVERT(int, [key]) AS rn, JSON_VALUE([value], '$.id') AS id
FROM OPENJSON(#json, '$.tags')
Result:
rn id
----------
0 5f5883
1 5fc8

MSSQL Query JSON displays Null value

I have a table PublicRelations with a column called Students in a SQL Server database called Subjects.
[
{ "Label": "Name", "ColumnValue": "Trudie" },
{ "Label": "Class", "ColumnValue": "PublicRelations" },
{ "Label": "Room", "ColumnValue": "8049" },
{ "Label": "HttpPath", "ColumnValue": "https://www.google.com/" }
]
I only get NULL when I run the below query using the Json_value. I'd like to get it to display the value from the array. I believe this may have to do with the 4000 character limit?
SELECT [StuduentID],
[Students],
--JSON_VALUE([Students],'$.ColumnValue') AS Name --Only returns NULL
FROM [Subjects].[dbo].[PublicRelations] c
CROSS APPLY OPENJSON(c.Students)
WITH ( Name int '$.Name',
Value nvarchar(255) '$.ColmunValue'
) AS jsonValues
WHERE jsonValues.ColumnValue = 'Trudie'
The query works and I can find what I need, but again, I only get NULL when I want to display that part of the JSON column in my results.
The statement is wrong and you has the following issues (as #MartinSmith already mentioned):
Syntax error - '$.ColmunValue' should be '$.ColumnValue'.
Wrong schema definition (the WITH clause) - I can't see Name key in the input JSON.
Wrong use of JSON_VALUE() - this function extracts scalar value from a JSON string, so JSON_VALUE([Students],'$.ColumnValue') returns NULL with this JSON input in lax mode.
You may try with the following statement (based on the statement in the question):
Table:
CREATE TABLE PublicRelations (
StudentID int,
Students nvarchar(1000))
INSERT INTO PublicRelations (StudentID, Students)
VALUES (1, N'[
{ "Label": "Name", "ColumnValue": "Trudie" },
{ "Label": "Class", "ColumnValue": "PublicRelations" },
{ "Label": "Room", "ColumnValue": "8049" },
{ "Label": "HttpPath", "ColumnValue": "https://www.google.com/" }
]')
Statement:
SELECT p.StudentID, j.*
FROM [PublicRelations] p
CROSS APPLY OPENJSON(p.Students) WITH (
Name nvarchar(50) '$.Label',
Value nvarchar(255) '$.ColumnValue'
) j
WHERE EXISTS (
SELECT 1
FROM OPENJSON(p.Students) WITH (Value nvarchar(255) '$.ColumnValue')
WHERE Value = N'Trudie'
) AND (j.Name IN ('Name', 'Class', 'Room'))
Result:
StudentID Name Value
1 Name Trudie
1 Class PublicRelations
1 Room 8049

Insert an object into a JSON array in SQL Server

Every example that I've seen for JSON_MODIFY shows inserting a simple value such as a string into an array.
Suppose I have the following JSON stored in my SQL Server column:
[{"id": 1, "name": "One"}, {"id": 2, "name": "Two"}]
How do I append {"id": 3, "name": "Three"} to it?
When I try using JSON_MODIFY as shown below, a string is inserted:
UPDATE TheTable SET TheJSON = JSON_MODIFY(TheJSON, 'append $', N'{"id": 3, "name": "Three"}') WHERE Condition = 1;
Here is the resulting value for TheJSON column:
[{"id": 1, "name": "One"}, {"id": 2, "name": "Two"}, "{\"id\":3, \"name\": \"Three\"}"]
Other Attempts:
I noticed that I can create the JSON string that I want like this:
SELECT json.*
FROM TheTable t
CROSS APPLY OPENJSON(t.TheJSON) WITH (
id int N'$.id',
name nvarchar(100) N'$.name'
)
UNION ALL
SELECT 3 as id, N'Three' as name
FOR JSON AUTO;
However, when I go to try and use it in an update statement, it doesn't work:
UPDATE TheTable
SET TheJSON = (
SELECT json.* FROM TheTable t
CROSS APPLY OPENJSON(t.TheJSON) WITH (
id int N'$.id',
name nvarchar(100) N'$.name'
) as json
UNION ALL -- NO ERROR (and no update) when I remove UNION ALL+SELECT
SELECT 3 as id, N'Three' as name
FOR JSON AUTO
);
I get the following error:
Msg 1086, Level 15, State 1, Line 1: The FOR XML and FOR JSON clauses are invalid in views, inline functions, derived tables, and subqueries when they contain a set operator. To work around, wrap the SELECT containing a set operator using derived table or common table expression or view and apply FOR XML or FOR JSON on top of it.
You should wrap the third parameter of your JSON_MODIFY statement with JSON_QUERY():
UPDATE TheTable
SET TheJSON = JSON_MODIFY(TheJSON, 'append $', JSON_QUERY(N'{"id": 3, "name": "Three"}'))
WHERE Condition = 1;
Here is a complete sample:
DECLARE #TheTable table(TheJSON nvarchar(max), Condition int )
DECLARE #mystring nvarchar(100)='{"id": 3, "name": "Three"}'
INSERT INTO #TheTable SELECT '[{"id": 1, "name": "One"}, {"id": 2, "name": "Two"}]', 1
UPDATE #TheTable
SET TheJSON = JSON_MODIFY(TheJSON, 'append $', JSON_QUERY(N'{"id": 3, "name": "Three"}'))
WHERE Condition = 1;
SELECT TheJSON FROM #TheTable
This is the final output:
[{"id": 1, "name": "One"}, {"id": 2, "name": "Two"},{"id": 3, "name": "Three"}]
More info on JSON_QUERY here, and the explanation of the issue is here.

How to make JSON from SQL query in MS SQL 2014

Question: What is best solution to generate JSON from a SQL query in MS SQL 2014? I created a procedure, but it is very slow.
My Example:
DECLARE #customers xml;
DECLARE #json NVARCHAR(max);
SET #customers = (SELECT * FROM dbo.Customers FOR XML path, root)
EXEC [dbo].[HTTP_JSON] #customers, #json
EXEC [dbo].[HTTP_JSON](#Shopping)
Create PROCEDURE [dbo].[HTTP_JSON]
#parameters xml, #response NVARCHAR(max) OUTPUT
WITH EXEC AS CALLER
AS
set #response = (SELECT Stuff(
(SELECT * from
(SELECT ',
{'+
Stuff((SELECT ',"'+coalesce(b.c.value('local-name(.)', 'NVARCHAR(MAX)'),'')+'":"'+
b.c.value('text()[1]','NVARCHAR(MAX)') +'"'
from x.a.nodes('*') b(c)
for xml path(''),TYPE).value('(./text())[1]','NVARCHAR(MAX)')
,1,1,'')+'}'
from #parameters.nodes('/root/*') x(a)
) JSON(theLine)
for xml path(''),TYPE).value('.','NVARCHAR(MAX)' )
,1,1,''))
GO
Just for fun, I created a scalar function based off of my prior answer.
Aside from the obvious XML parameter, I added two additional: 1) Include Header (illustrated below), and 2) ToLower case (I prefer my JSON field names in lower case which links to my classes and such).
If the query is more than one record, a formatted array will be returned.
Declare #Table table (ID int,Active bit,First_Name varchar(50),Last_Name varchar(50),EMail varchar(50))
Insert into #Table values
(1,1,'John','Smith','john.smith#email.com'),
(2,0,'Jane','Doe' ,'jane.doe#email.com')
Select A.ID
,A.Last_Name
,A.First_Name
,B.JSON
From #Table A
Cross Apply (Select JSON=[dbo].[udf-Str-JSON](0,1,(Select A.* For XML Raw)) ) B
Returns
ID Last_Name First_Name JSON
1 Smith John {"id":"1","active":"1","first_name":"John","last_name":"Smith","email":"john.smith#email.com"}
2 Doe Jane {"id":"2","active":"0","first_name":"Jane","last_name":"Doe","email":"jane.doe#email.com"}
Or even more simply
Select JSON=[dbo].[udf-Str-JSON](0,1,(Select * From #Table for XML RAW))
Returns with Header ON
{
"status": {
"successful": "true",
"timestamp": "2016-10-09 06:08:16 GMT",
"rows": "2"
},
"results": [{
"id": "1",
"active": "1",
"first_name": "John",
"last_name": "Smith",
"email": "john.smith#email.com"
}, {
"id": "2",
"active": "0",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe#email.com"
}]
}
Returns with Header Off
[{
"id": "1",
"active": "1",
"first_name": "John",
"last_name": "Smith",
"email": "john.smith#email.com"
}, {
"id": "2",
"active": "0",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe#email.com"
}]
The UDF
ALTER FUNCTION [dbo].[udf-Str-JSON] (#IncludeHead int,#ToLowerCase int,#XML xml)
Returns varchar(max)
AS
Begin
Declare #Head varchar(max) = '',#JSON varchar(max) = ''
; with cteEAV as (Select RowNr=Row_Number() over (Order By (Select NULL))
,Entity = xRow.value('#*[1]','varchar(100)')
,Attribute = xAtt.value('local-name(.)','varchar(100)')
,Value = xAtt.value('.','varchar(max)')
From #XML.nodes('/row') As R(xRow)
Cross Apply R.xRow.nodes('./#*') As A(xAtt) )
,cteSum as (Select Records=count(Distinct Entity)
,Head = IIF(#IncludeHead=0,IIF(count(Distinct Entity)<=1,'[getResults]','[[getResults]]'),Concat('{"status":{"successful":"true","timestamp":"',Format(GetUTCDate(),'yyyy-MM-dd hh:mm:ss '),'GMT','","rows":"',count(Distinct Entity),'"},"results":[[getResults]]}') )
From cteEAV)
,cteBld as (Select *
,NewRow=IIF(Lag(Entity,1) over (Partition By Entity Order By (Select NULL))=Entity,'',',{')
,EndRow=IIF(Lead(Entity,1) over (Partition By Entity Order By (Select NULL))=Entity,',','}')
,JSON=Concat('"',IIF(#ToLowerCase=1,Lower(Attribute),Attribute),'":','"',Value,'"')
From cteEAV )
Select #JSON = #JSON+NewRow+JSON+EndRow,#Head = Head From cteBld, cteSum
Return Replace(#Head,'[getResults]',Stuff(#JSON,1,1,''))
End
-- Parameter 1: #IncludeHead 1/0
-- Parameter 2: #ToLowerCase 1/0 (converts field name to lowercase
-- Parameter 3: (Select * From ... for XML RAW)
**EDIT - Corrected Typo
The following should create the JSON array for just about any data set. However, I have not created a way to convert bit to true/false yet.
Just one point to consider: The FIRST column in the initial SELECT has to be the Primary Key which is equates to the ENTITY field. In this case, Select * from #User for XML RAW ... ID is the Entity and just so happens to be the first field in the table
As far as performance, 500 records with 19 fields creates a JSON string 191,987 bytes in 0.694 seconds (50 records in 0.098 seconds)
Consider the following:
Declare #User table (ID int,Active bit,First_Name varchar(50),Last_Name varchar(50),EMail varchar(50),LastOn DateTime)
Insert into #User values
(1,1,'John','Smith','john.smith#email.com','2016-10-05 17:32:41.903'),
(2,0,'Jane','Doe' ,'jane.doe#email.com','2016-10-05 08:25:18.203')
Declare #XML xml = (Select * From #User for XML RAW)
Declare #JSON varchar(max) = ''
;with cteEAV as (
Select RowNr = Row_Number() over (Order By (Select NULL))
,Entity = xRow.value('#*[1]','varchar(100)')
,Attribute = xAtt.value('local-name(.)','varchar(100)')
,Value = xAtt.value('.','varchar(max)')
From #XML.nodes('/row') As A(xRow)
Cross Apply A.xRow.nodes('./#*') As B(xAtt) )
,cteBld as (
Select *
,NewRow = IIF(Lag(Entity,1) over (Partition By Entity Order By (Select NULL))=Entity,'',',{')
,EndRow = IIF(Lead(Entity,1) over (Partition By Entity Order By (Select NULL))=Entity,',','}')
,JSON = Concat('"',Attribute,'":','"',Value,'"')
From cteEAV )
Select #JSON = #JSON+NewRow+JSON+EndRow
From cteBld
Select '['+Stuff(#JSON,1,1,'')+']'
Returns
[{"ID":1, "Active":1, "First_Name":"John", "Last_Name":"Smith", "EMail":"john.smith#email.com", "LastOn":"2016-10-05T17:32:41.903", "TotalSales":25569.0000} ,{"ID":2, "Active":0, "First_Name":"Jane", "Last_Name":"Doe", "EMail":"jane.doe#email.com", "LastOn":"2016-10-05T08:25:18.203", "TotalSales":22888.0000}]
A more readable version
cteEAV will dynamically unpivot the data and generate the following:
cteBLD will extend and add flags New/End Row
The Final Select
This will put it all together and generate one final string which can be wrapped or nested as you please.