I have JSON data (no schema) stored in a SQL Server column and need to run search queries on it.
E.g. (not actual data)
[
{
"Color":"Red",
"Make":"Mercedes-Benz"
},
{
"Color":"Green",
"Make":"Ford"
},
]
SQL Server 2017 has JSON_XXXX methods but they work on pre-known schema. In my case, the schema of objects is not defined precisely and could change.
Currently to search the columns e.g. find Make=Mercedes-Benz. I'm using a search phrase "%\"Make\":\"Mercedes-Benz\"%". This works quite well IF exact make name is used. I'd like user to be able to search using partial names as well e.g. just typing 'Benz' or 'merc'.
Is it possible to structure a SQL query using wild cards that'll work for me? Any other options?
One possible approach is to use OPENJSON with default schema twice. With default schema, OPENJSON returns table with columns key, value and type, and you can use them for your WHERE clause.
Table:
CREATE TABLE #Data (
Json nvarchar(max)
)
INSERT INTO #Data
(Json)
VALUES
(N'[
{
"Color":"Red",
"Make":"Mercedes-Benz"
},
{
"Color":"Green",
"Make":"Ford",
"Year": 2000
}
]')
Statement:
SELECT
j1.[value]
-- or other columns
FROM #Data d
CROSS APPLY OPENJSON(d.Json) j1
CROSS APPLY OPENJSON(j1.[value]) j2
WHERE
j2.[key] LIKE '%Make%' AND
j2.[value] LIKE '%Benz%'
Output:
--------------------------
value
--------------------------
{
"Color":"Red",
"Make":"Mercedes-Benz"
}
You can split json by ',' and search like this:
WHERE EXISTS (SELECT *
FROM STRING_SPLIT(json_data, ',')
WHERE value LIKE '%\"Make\":%'
AND value LIKE '%Benz%'
);
If you happen to be running an older version of SQL Server that does not support built-in JSON functions such as OPENJSON(), you can use SQL similar to the following.
You can try testing this SQL at http://sqlfiddle.com/#!18/dd7a5
NOTE: This SQL assumes the key you are searching on only appears ONCE per record/JSON object literal (in other words, you are only storing JSON object literals with unique keys per record/database row). Also note, the SELECT query is UGLY, but it works.
/* see http://sqlfiddle.com/#!18/dd7a5 to test this online*/
/* setup a test data table schema */
CREATE TABLE myData (
[id] [int] IDENTITY(1,1) NOT NULL,
[jsonData] nvarchar(4000)
CONSTRAINT [PK_id] PRIMARY KEY CLUSTERED
(
[id] ASC
)
);
/* Insert some test data */
INSERT INTO myData
(jsonData)
VALUES
('{
"Color":"Red",
"Make":"Mercedes-Benz"
}');
INSERT INTO myData
(jsonData)
VALUES
(
'{
"Color":"White",
"Make":"Toyota",
"Model":"Prius",
"VIN":"123454321"
}');
INSERT INTO myData
(jsonData)
VALUES
(
'{
"Color":"White",
"Make":"Mercedes-Benz",
"Year": 2009
}');
INSERT INTO myData
(jsonData)
VALUES
(
'{
"Type":"Toy",
"Color":"White",
"Make":"Toyota",
"Model":"Prius",
"VIN":"99993333"
}');
/* This select statement searches the 'Make' keys, within the jsonData records, with values LIKE '%oyo%'. This statement will return records such as 'Toyota' as the Make value. */
SELECT id, SUBSTRING(
jsonData
,CHARINDEX('"Make":', jsonData) + LEN('"Make":')
,CHARINDEX(',', jsonData, CHARINDEX('"Make":', jsonData) + LEN('"Make":')) - CHARINDEX('"Make":', jsonData) - LEN('"Make":')
) as CarMake FROM myData
WHERE
SUBSTRING(
jsonData
,CHARINDEX('"Make":"', jsonData) + LEN('"Make":"')
,CHARINDEX('"', jsonData, CHARINDEX('"Make":"', jsonData) + LEN('"Make":"')) - CHARINDEX('"Make":"', jsonData) - LEN('"Make":"')
) LIKE '%oyo%'
Related
I've this table with is basically translations:
Key CultureId Txt
$HELLO en-GB Hello
$HELLO pt-BR Olá
$WELCOME en-GB Welcome
$WELCOME pt-BR Olá
And a select like:
Select Key, CultureId, Txt
From Xlations
Order by Key
This is an endpoint rest api, so I'd like a result like
{
"$HELLO":{
"en-GB":"Hello",
"pt-BR":"Olá"
},
"$WELCOME":{
"en-GB":"Bem Vindo",
"pt-BR":"Welcome"
}
}
So, keys with no arrays, totally in objects where the field key will be the parent of the assigned translations.
I know how to do it by creating few iterations on my code, but I was wondering if there is some shorthand for that because I don't want to keep my code huge and complex with iterates and nested iterates. Not sure if such things are possible, but: Anywone know some easy and simple way ?
JSON output is usually generated using the FOR JSON clause. In your case, the required JSON output has variable key names, so FOR JSON is probably not an option. But, if the SQL Server version is 2017 or higher, you may try to generate the JSON manually, using string concatenation and aggregation. Also, as #Charlieface commented, escape the generated text with STRING_ESCAPE().
Test table:
SELECT *
INTO Xlations
FROM (VALUES
(N'$HELLO', N'en-GB', N'Hello'),
(N'$HELLO', N'pt-BR', N'Olá'),
(N'$WELCOME', N'en-GB', N'Welcome'),
(N'$WELCOME', N'pt-BR', N'Bem Vindo')
) v ([Key], CultureId, Txt)
Statement:
SELECT CONCAT(
N'{',
STRING_AGG(CONCAT(N'"', STRING_ESCAPE([Key], 'json'), N'":', [Value]), N','),
N'}'
) AS Json
FROM (
SELECT DISTINCT x.[Key], a.[Value]
FROM Xlations x
OUTER APPLY (
SELECT CONCAT(
N'{',
STRING_AGG(CONCAT(N'"', STRING_ESCAPE(CultureId, 'json'), N'":"', STRING_ESCAPE(Txt, 'json'), N'"'), N','),
N'}'
) AS [Value]
FROM Xlations
WHERE [Key] = x.[Key]
) a
) t
Result:
{
"$HELLO":{"en-GB":"Hello","pt-BR":"Olá"},
"$WELCOME":{"en-GB":"Welcome","pt-BR":"Bem Vindo"}
}
You can not use your sql functions to do this and you have to do it manually.
SELECT CONCAT('{',string_agg(jsoncol,','),'}') Json
FROM
(SELECT '1' AS col, CONCAT('"',[key],'"',':{' + string_agg(jsoncol,',') ,'}') AS jsoncol
FROM
(SELECT [key],CONCAT('"',CultureId,'":"',txt ,'"') AS jsoncol FROM tb) t
GROUP BY [key]) t
GROUP BY col
demo in dbfiddle<>uk
The answer given by #Zhorov is good, but you can improve it by only querying the table once, aggregating then aggregating again.
This should be more performant than a correlated subquery.
SELECT CONCAT(
N'{',
STRING_AGG(CONCAT(N'"', STRING_ESCAPE([Key], 'json'), N'":', [Value]), N','),
N'}'
) AS Json
FROM (
SELECT x.[Key], CONCAT(
N'{',
STRING_AGG(CONCAT(N'"', STRING_ESCAPE(CultureId, 'json'), N'":"', STRING_ESCAPE(Txt, 'json'), N'"'), N','),
N'}'
) AS [Value]
FROM Xlations x
GROUP BY x.[Key]
) t
db<>fiddle
I would like to us dapper to pass in a list of id's and build a select statement like
SELECT Id FROM (VALUES
('2f1a5d4b-008a-496e-b0cf-ba8b53224247'),
('bf63102b-0244-4c9d-89ae-bdd7b41f135c')) AS tenantWithFile(Id)
WHERE NOT exists( SELECT [Id]
FROM [dbo].[TenantDetail]AS td
WHERE td.Id = tenantWithFile.Id
)
where I get back the items in the list that are not in the database. Is there a simple way to do this with out making a type for TVP?
As Sean mentioned, here is a little snippet which demonstrates how you can parse a delimited string without a TVF. ... easily incorporated into your query.
Example
Declare #YourList varchar(max)='2f1a5d4b-008a-496e-b0cf-ba8b53224247,bf63102b-0244-4c9d-89ae-bdd7b41f135c'
Select Id = xmlnode.n.value('(./text())[1]', 'varchar(max)')
From (values (cast('<x>' + replace(#YourList,',','</x><x>')+'</x>' as xml))) xmldata(xd)
Cross Apply xd.nodes('x') xmlnode(n)
Returns
Id
2f1a5d4b-008a-496e-b0cf-ba8b53224247
bf63102b-0244-4c9d-89ae-bdd7b41f135c
If you're using Azure SQL or SQL Server 2016+, you could just pass a JSON array and then use OPENJSON to turn to array into a table
DECLARE #j AS NVARCHAR(max) = '["2f1a5d4b-008a-496e-b0cf-ba8b53224247", "bf63102b-0244-4c9d-89ae-bdd7b41f135c"]';
SELECT [value] FROM OPENJSON(#j)
I am having difficulty solving my problem of selecting records using WHERE IN (). I am working with C# in back end where I am sending a comma separated string of id values. I am looking to query all records that are in that string.
Here is my sample string:
"548,549,550,551,712,713"
Here is my SQL Stored Procedure
#ids nvarchar(200)
SELECT * FROM Users
WHERE USers.ID IN (#ids)
But I get an error:
Conversion failed when converting the nvarchar value '548, 549, 550,
551, 712, 713' to data type int.
Thank you
SQL Server 2016 and later versions
Declare #ids nvarchar(200) = '123,456';
Select *
From Users
Where ID IN ( Select Value from STRING_SPLIT ( #ids , ',' ))
For older versions SQL Server 2005 - 2014
Declare #ids nvarchar(200) = '123,456'; --<-- Comma delimited list of Client Ids
Select *
From Users
Where ID IN (
SELECT CAST(RTRIM(LTRIM(Split.a.value('.', 'VARCHAR(100)'))) AS INT) IDs
FROM (
SELECT Cast ('<X>'
+ Replace(#ids, ',', '</X><X>')
+ '</X>' AS XML) AS Data
) AS t CROSS APPLY Data.nodes ('/X') AS Split(a)
)
I have found that a table-valued function that unpacks a comma-separated string of numbers into a table to be very useful:
-- =============================================
-- Description: Generates a table of values generated from
-- a given csv list of integer values. Each value
-- in the list is separated by commas. The output
-- table of values has one integer per row. Any non-
-- numeric entries are discarded.
-- =============================================
CREATE FUNCTION [dbo].[fnCSVIntListToTable]
(
#CSVList nvarchar(MAX)
)
RETURNS #Result TABLE(Value int)
AS
BEGIN
-- Ensure the parameter is not null
SET #CSVList = ISNULL(#CSVList, '');
-- Create XML string from the CSV list
DECLARE #x XML
SELECT #x = CAST('<A>'+ REPLACE(#CSVList, ',' , '</A><A>')+ '</A>' AS XML);
-- Unpack the XML string into the table
INSERT INTO #Result
SELECT t.value('.', 'int') AS inVal
FROM #x.nodes('/A') AS x(t)
-- Discard any non-numeric entries
WHERE ISNUMERIC(t.value('.', 'varchar(20)'))=1
RETURN
END
Then in your procedure code you can use the function directly in a join:
Declare #ids nvarchar(200) = '123,456,789';
:
:
Select *
From Users UU
JOIN dbo.fnCSVIntListToTable(#ids) IDS
ON IDS.Value = UU.ID;
I have multiple tables that have int values in them that represent a specific string (Text) and I want to convert the integers to the string values. The goal is to make a duplicate copy of the table and then translate the integers to strings for easy analysis.
For example, I have the animalstable and the AnimalType Field consists of int values.
0 = "Cat", 1 = dog, 2= "bird", 3 = "turtle", 99 = "I Don't Know"
Can someone help me out with some starting code for this translation to animalsTable2 showing the string values?
Any help would be so very much appreciated! I want to thank you in advance for your help!
The best solution would be to create a related table that defines the integer values.
CREATE TABLE [Pets](
[ID] [int] NOT NULL,
[Pet] [varchar](50) NULL,
CONSTRAINT [PK_Pets] PRIMARY KEY CLUSTERED
([ID] ASC) ON [PRIMARY]
Then you can insert your pet descriptions but you can leave out the "I don't Know" item; it can be handled by left joining the Pets table to your main table.
--0 = "Cat", 1 = dog, 2= "bird", 3 = "turtle", 99 = "I Don't Know"`
INSERT INTO [Pets] ([ID],[Pet]) VALUES(0, 'cat');
INSERT INTO [Pets] ([ID],[Pet]) VALUES(1, 'dog');
INSERT INTO [Pets] ([ID],[Pet]) VALUES(2, 'bird');
INSERT INTO [Pets] ([ID],[Pet]) VALUES(3, 'turtle');
Now you can include the [Pets].[Descr] field in the output of your query like so
SELECT [MainTableFiled1]
,[MainTableFieldx]
,isnull([Pet], 'I dont know') as Pet
FROM [dbo].[MainTable] a
LEFT JOIN [dbo].[Pets] b
ON a.[MainTable_PetID] = b.[ID]
Alternatively, you can just define the strings in a case statement inside you query. This however, is not advised if you could be using the strings in more than one query.
Select case SomeField
when 0 then 'cat'
when 1 then 'dog'
when 2 then 'bird'
when 3 then 'turtle'
else 'i dont know' end as IntToString
from SomeTable
The benefit of the related table is you have only one place to maintain your string definitions and any edits would propagate to all queries, views or procedures that use it.
You can create a temp table to store the mappings. Then insert from a join between that table and the original table like so:
-- Create temp table
DECLARE #animalMapping TABLE(
animalType int NOT NULL,
animalName varchar(30) NOT NULL
);
-- Insert values into temp table
INSERT INTO #animalMapping (animalType, animalName)
VALUES (0, 'Cat'),
(1, 'Dog'),
(2, 'Bird'),
(3, 'Turtle'),
(99, 'I don''t know');
-- Insert into new table
INSERT INTO animalsTable2
SELECT id, <other fields from animalstable>,
#animalMapping.animalName
FROM animalstable
JOIN #animalMapping
ON animalstable.AnimalType = #animalMapping.animalType
I want to create a read only view with the following columns:
Id - Unique integer
ActivityKind - Identifies what is in PayloadAsJson. Could be an int, char whatever
PayloadAsJson - Record from corresponding table presented as JSON
The reason for this is that I have a number of tables that have different structures that I want to UNION and present in some kind of date order. So for example:
Table1
Id Date EmailSubject EmailRecipient
-- ----------- -------------- ---------------
1 2014-01-01 "Hello World" "me#there.com"
2 2014-01-02 "Hello World2" "me#there.com"
Table2
Id Date SensorId SensorName
-- ----------- -------- ------------------
1 2014-01-01 1 "Some Sensor Name"
I would have SQL similair to the following for the view of:
SELECT Date, 'E' AS ActivityKind, <SPCallToGetJSONForThisRecord> AS PayloadAsJson
FROM Table1
UNION
SELECT Date, 'S' AS ActivityKind, <SPCallToGetJSONForThisRecord> AS PayloadAsJson
FROM Table2
ORDER BY Date
and I want the view to look like:
1, "E", "{ "Id": 1, "Date": "2014-01-01", "EmailSubject": "Hello World", "EmailRecipient": me#there.com" }"
2, "S", "{ "Id": 1, "Date": "2014-01-01", "SensorId": 1, "SensorName": "Some Sensor Name" }"
3, "E", "{ "Id": 2, "Date": "2014-01-01", "EmailSubject": "Hello World2", "EmailRecipient": me#there.com" }"
The rationale here is that:
I can use the DB server to produce the view by doing whatever SQL needed
This data is going to be read only on the client side
By having a consistent view structure namely Id, ActivityKind, Payload any time I want to add some additional tables in I can do so, client code would be modified to handle decoding the JSON based on ActivityKind
Now there are many Stored Procedure implementations to convert an entire SQL result to JSON http://jaminquimby.com/joomla253/servers/95-sql/sql-2008/145-code-tsql-convert-query-to-json, but what I am struggling with is:
Getting the uniue running sequence for the entire view
The actual implementation of because this has to do it on a record by record basis.
In short I am looking for a solution that shows me how to create the view to the above requirements. All pointers and help greatly appreciated.
While not getting into the debate about whether this should be done in the database or not, it seemed like an interesting puzzle, and more and more people seem to be wanting at least simple JSON translation at the DB level. So, there are a couple of ideas.
The first idea is to have SQL Server do most of the work for us by turning the row into XML via the FOR XML clause. A simple, attribute-based XML representation of a row is very similar in nature to the JSON structure; it just needs a little transformin'. While the parsing could be done purely in T-SQL via PATINDEX, etc, but that just complicates things. So, a somewhat simple Regular Expression Replace makes it rather simple to change the name="value" structure into "name": "value". The following example uses a RegEx function that is available in the SQL# library (which I am the author of, but RegEx_Replace is in the Free version).
And just as I finished that I remembered that you can do transformations via the .query() function against an XML field or variable, and use FLWOR Statement and Iteration to cycle through the attributes. So I added another column for this second use of the XML intermediate output, but this is done in pure T-SQL as opposed to requiring CLR in the case of RegEx. I figured it was easiest to place in the same overall test setup rather than repeat the majority of it just to change a few lines.
A third idea is to go back to SQLCLR, but to create a scalar function that takes in the table name and ID as parameters. You can then make use of the "Context Connection" which is the in-process connection (hence fast) and build a Dynamic SQL statement of "SELECT * FROM {table} WHERE ID = {value}" (obviously check inputs for single-quotes and dashes to avoid SQL Injection). When you call SqlDataReader, you can not only step through each field easily, but you then also have insight into the datatype of each field and can determine if it is numeric, and if so, then don't put the double-quotes around the value in the output. [If I have time tomorrow or over the weekend I will try to put something together.]
SET NOCOUNT ON; SET ANSI_NULLS ON;
DECLARE #Table1 TABLE (
ID INT NOT NULL PRIMARY KEY,
[Date] DATETIME NOT NULL,
[EmailSubject] NVARCHAR(200) NOT NULL,
[EmailRecipient] NVARCHAR(200) NOT NULL );
INSERT INTO #Table1 VALUES (1, '2014-01-01', N'Hello World', N'me#here.com');
INSERT INTO #Table1 VALUES (2, '2014-03-02', N'Hello World2', N'me#there.com');
DECLARE #Table2 TABLE (
ID INT NOT NULL PRIMARY KEY,
[Date] DATETIME NOT NULL,
[SensorId] INT NOT NULL,
[SensorName] NVARCHAR(200) NOT NULL );
INSERT INTO #Table2 VALUES (1, '2014-01-01', 1, N'Some Sensor Name');
INSERT INTO #Table2 VALUES (2, '2014-02-01', 34, N'Another > Sensor Name');
---------------------------------------
;WITH cte AS
(
SELECT tmp.[Date], 'E' AS ActivityKind,
(SELECT t2.* FROM #Table1 t2 WHERE t2.ID = tmp.ID FOR XML RAW('wtf'))
AS [SourceForJSON]
FROM #Table1 tmp
UNION ALL
SELECT tmp.[Date], 'S' AS ActivityKind,
(SELECT t2.*, NEWID() AS [g=g] FROM #Table2 t2 WHERE t2.ID = tmp.ID
FOR XML RAW('wtf')) AS [SourceForJSON]
FROM #Table2 tmp
)
SELECT ROW_NUMBER() OVER (ORDER BY cte.[Date]) AS [Seq],
cte.ActivityKind,
cte.SourceForJSON,
N'{' +
REPLACE(
REPLACE(
REPLACE(
SUBSTRING(SQL#.RegEx_Replace(cte.SourceForJSON,
N' ([^ ="]+)="([^"]*)"',
N' "$1": "$2",', -1, 1, N'IgnoreCase'),
6, 4000),
N'",/>', '"}'),
N'>', N'>'),
N'<', N'<') AS [JSONviaRegEx],
N'{' + REPLACE(CONVERT(NVARCHAR(MAX),
CONVERT(XML, cte.SourceForJSON).query('
let $end := local-name((/wtf/#*)[last()])
for $item in /wtf/#*
return concat(""",
local-name($item),
"": "",
data($item),
""",
if (local-name($item) != $end) then ", " else "")
')), N'>', N'>') + N'}' AS [JSONviaXQuery]
FROM cte;
Please keep in mind that in the above SQL, the cte query can be easily encapsulated in a View, and the transformation (whether via SQLCLR/RegEx or XML/XQuery) can be encapsulated in a T-SQL Inline Table-Valued Function and used in the main SELECT (the one that selects from the cte) via CROSS APPLY.
EDIT:
And speaking of encapsulating the XQuery into a function and calling via CROSS APPLY, here it is:
The function:
CREATE FUNCTION dbo.JSONfromXMLviaXQuery (#SourceRow XML)
RETURNS TABLE
AS RETURN
SELECT N'{'
+ REPLACE(
CONVERT(NVARCHAR(MAX),
#SourceRow.query('
let $end := local-name((/wtf/#*)[last()])
for $item in /wtf/#*
return concat(""",
local-name($item),
"": "",
data($item),
""",
if (local-name($item) != $end) then "," else "")
')
),
N'>',
N'>')
+ N'}' AS [TheJSON];
The setup:
CREATE TABLE #Table1 (
ID INT NOT NULL PRIMARY KEY,
[Date] DATETIME NOT NULL,
[EmailSubject] NVARCHAR(200) NOT NULL,
[EmailRecipient] NVARCHAR(200) NOT NULL );
INSERT INTO #Table1 VALUES (1, '2014-01-01', N'Hello World', N'me#here.com');
INSERT INTO #Table1 VALUES (2, '2014-03-02', N'Hello World2', N'me#there.com');
CREATE TABLE #Table2 (
ID INT NOT NULL PRIMARY KEY,
[Date] DATETIME NOT NULL,
[SensorId] INT NOT NULL,
[SensorName] NVARCHAR(200) NOT NULL );
INSERT INTO #Table2 VALUES (1, '2014-01-01', 1, N'Some Sensor Name');
INSERT INTO #Table2 VALUES (2, '2014-02-01', 34, N'Another > Sensor Name');
The view (or what would be if I wasn't using temp tables):
--CREATE VIEW dbo.GetMyStuff
--AS
;WITH cte AS
(
SELECT tmp.[Date], 'E' AS ActivityKind,
(SELECT t2.* FROM #Table1 t2 WHERE t2.ID = tmp.ID
FOR XML RAW('wtf'), TYPE) AS [SourceForJSON]
FROM #Table1 tmp
UNION ALL
SELECT tmp.[Date], 'S' AS ActivityKind,
(SELECT t2.*, NEWID() AS [g=g] FROM #Table2 t2 WHERE t2.ID = tmp.ID
FOR XML RAW('wtf'), TYPE) AS [SourceForJSON]
FROM #Table2 tmp
)
SELECT ROW_NUMBER() OVER (ORDER BY cte.[Date]) AS [Seq],
cte.ActivityKind,
json.TheJSON
FROM cte
CROSS APPLY dbo.JSONfromXMLviaXQuery(cte.SourceForJSON) json;
The results:
Seq ActivityKind TheJSON
1 E {"ID": "1", "Date": "2014-01-01T00:00:00", "EmailSubject": "Hello World", "EmailRecipient": "me#here.com"}
2 S {"ID": "1", "Date": "2014-01-01T00:00:00", "SensorId": "1", "SensorName": "Some Sensor Name", "g_x003D_g": "3AE13983-6C6C-49E8-8E9D-437DAA62F910"}
3 S {"ID": "2", "Date": "2014-02-01T00:00:00", "SensorId": "34", "SensorName": "Another > Sensor Name", "g_x003D_g": "7E760F9D-2B5A-4FAA-8625-7B76AA59FE82"}
4 E {"ID": "2", "Date": "2014-03-02T00:00:00", "EmailSubject": "Hello World2", "EmailRecipient": "me#there.com"}