How to merge json arrray with table (SQL Server) - json

I have two rows of json that I would like to join on id into a single select.
Sample Table A
a
[{id: 1, name: "Alice"},{id:2, name: "Bob"}]
[{id: 5, name: "Charlie"},{id:6, name: "Dale"}
Sample Table B
id
age
1
30
2
32
3
20
4
14
Desired Output
c
[{id: 1, name: "Alice", age: 30},{id:2, name: "Bob", age: 32}]
[{id: 5, name: "Charlie", age: 20},{id:6, name: "Dale", age: 14}]
I'd like to do something like
select
id,
name,
age
from openJson(tableA) ta
with (
id int '$.id',
name nvarchar(50) '$.name'
)
inner join (
select *
from tableB tb
) on tb.id = ta.id
for json path

Firstly, this assumes that your JSON is actually valid. None of your elements (id and name) are quoted, so the JSON is actually invalid. Also I assume your expected results are wrong, as Charlie is give the age of 20, but that age belongs to someone with an id with the value 3, and Charlie's has an id of 5.
Anyway, we can can achieve this with a subquery:
CREATE TABLE dbo.TableA (a nvarchar(MAX));
INSERT INTO dbo.TableA (a)
VALUES(N'[{"id": 1, "name": "Alice"},{"id":2, "name": "Bob"}]'),
(N'[{"id": 5, "name": "Charlie"},{"id":6, "name": "Dale"}]');
GO
CREATE TABLE dbo.TableB (id int, age int);
INSERT INTO dbo.TableB (id, age)
VALUES (1,30),
(2,32),
(3,20),
(4,14);
GO
SELECT (SELECT OJ.id,
OJ.[name],
B.age
FROM OPENJSON(A.a)
WITH (id int,
[name] nvarchar(50)) OJ
LEFT JOIN dbo.TableB B ON OJ.id = B.id
FOR JSON AUTO) a
FROM dbo.TableA A;
GO
DROP TABLE dbo.TableA;
DROP TABLE dbo.TableB;
db<>fiddle

Related

Merge multiple MySQL tables which are related and return JSON

I've been trying to figure this out for hours. But with no luck.
This works, but not what i exactly want
How it's now:
{"text":"My first report","comment":"Great Report","display_name":"Xavier"},
{"text":"My First report","comment":"Do you call this a report?","display_name":"Logan"}
How I would like it to be:
{"text":"My first report","comments":[{comment: "Great Report","display_name":"Xavier"}, {comment: "Do you call this a report?","display_name":"Logan"}],
Current Setup
Report
ID | User_ID | TEXT |
15 | 3 My first report
Users
ID | DISPLAY_NAME |
1 | Xavier
2 | Logan
3 | Cyclops
How it is now:
Report_Comments
ID | User_ID | Report_ID | TEXT as comment |
3 | 1 15 Great Report
3 | 2 15 Bad Report
My code:
SELECT r.text,
Group_concat(r.user_id) AS User_ID,
Group_concat(u.display_name) AS User_Name,
r.id,
Group_concat(c.text) AS comment
FROM report r
LEFT JOIN users u
ON u.id = r.user_id
LEFT JOIN report_comments c
ON c.report_id = r.id
WHERE r.user_id = :userId
GROUP BY r.id,
r.text
Using JSON_ARRAYAGG and JSON_OBJECT, we can achieve this with only one join.
Recreating your situation:
CREATE TABLE reports (id INT, user_id INT, title VARCHAR(60), PRIMARY KEY (id));
CREATE TABLE users (id INT, name VARCHAR(30), PRIMARY KEY (id));
CREATE TABLE comments (id INT, user_id INT, report_id INT, comment VARCHAR(100), PRIMARY KEY (id));
INSERT INTO users VALUES (1, 'Xavier'), (2, 'Logan'), (3, 'Cyclops');
INSERT INTO reports VALUES (10, 1, 'My First Report');
INSERT INTO comments VALUES (100, 1, 10, 'bad report'), (200, 1, 10, 'good report');
We now can run a SELECT which joins the tables reports and comments, grouping by the id of reports. Using the JSON_OBJECT function we create the JSON document. With the JSON_ARRAYAGG, we aggregate into 'comments'.
SELECT JSON_OBJECT('text', r.title, 'comments',
JSON_ARRAYAGG(JSON_OBJECT('comment', c.comment, 'name', u.name))) as report_comments
FROM reports AS r JOIN comments c on r.id = c.report_id
JOIN users u on c.user_id = u.id
GROUP BY r.id;
Result:
+-------------------------------------------------------------------------------------------------------------------------------------+
| report_comments |
+-------------------------------------------------------------------------------------------------------------------------------------+
| {"text": "My First Report", "comments": [{"name": "Xavier", "comment": "bad report"}, {"name": "Logan", "comment": "good report"}]} |
+-------------------------------------------------------------------------------------------------------------------------------------+
Result using JSON_PRETTY and \G instead of ; executing the query:
*************************** 1. row ***************************
report_comments: {
"text": "My First Report",
"comments": [
{
"name": "Xavier",
"comment": "bad report"
},
{
"name": "Logan",
"comment": "good report"
}
]
}

Use STUFF() on a result set coming from OPENJSON

I have a ClassTable as shown below with a Languages column in which the data is in JSON:
ID
Title
Languages
1
Class1
[{"ID": 1, "Name": "English"},{"ID": 2, "Name": "Hindi"}]
2
Class2
[{"ID": 1, "Name": "Marathi"},{"ID": 2, "Name": "Telugu"}]
and a Master table of Languages as
ID
Name
1
English
2
Hindi
3
Marathi
4
Telugu
I need output as below
ID
Title
LanguageIDs
1
TestTitle1
1,2
I am trying to achieve this with OPENJSON to get data from JSON and then I am applying STUFF() to that data so that I would get comma separated LanguageIDs
This is the query, I have written but I am not getting the expected output
SELECT
A.ID,
A.Title,
LanguageIDs = STUFF ((SELECT CONCAT(',',A.ID)
FROM Master.Languages
WHERE ID IN (A.LanguageID)
FOR XML PATH(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, SPACE(0))
FROM
(SELECT
X.ID,
X.Title,
X.CreatedOn,
B.ID as LanguageID
FROM
ClassTable X
CROSS APPLY
OPENJSON(Languages)
WITH (ID INT '$.ID') as B
WHERE
X.ID = 1) AS A
Can anybody tell me whats the mistake I am making? Or do I have to try a different approach for this problem?
Logically, It should work
SELECT
X.ID,
X.Title,
(
select cast(ID as varchar) +',' from OPENJSON(x.Languages)
WITH (ID INT '$.ID')
for xml path('')
) LanguageID
FROM
ClassTable X where x.id=1

SQL Select to JSON for Menu Structure

I was looking for help with a query which will produce the output in JSON that I could use to populate a Menu, thus the JSON needs to create the menu structure.
Here are the two tables that I have, and the best query I've been able to write (which does not produce the output I need) is:
SELECT
c.ID As CategoryID,
c.Name As CategoryName,
p.ProductName as ProductName
p.ProductID as ProductID
FROM Category c, Product p
WHERE
c.ID = p.CategoryID
FOR JSON PATH, ROOT('Menu')
Table: Category
ID | Name
---------------
1 | Fruit
2 | Vegetable
Table: Product
ProductID | CategoryID | ProductName
----------------------------------------
1 | 1 | Apple
2 | 1 | Orange
3 | 2 | Celery
4 | 2 | Carrot
5 | 1 | Banana
The desired output for this query would be the following
menu:
[
{name: 'Fruit', ID: '1', Items:
[
{productname: 'Apple', productid: '1'},
{productname: 'Orange', productid: '2'},
{productname: 'Banana', productid: '5'}
]
},
{name: 'Vegetable', ID: '2', Items:
[
{productname: 'Celery', productid: '3'},
{productname: 'Carrot', productid: '4'}
]
}
]
Is anyone able to explain the steps in which I could change my query to achieve this?
You may choose between the following options:
FOR JSON AUTO and an appropriate join. The output is automatically generated based on the structure of the SELECT statement.
A combination of two nested FOR JSON AUTO statements:
Table:
CREATE TABLE Category (ID int, Name varchar(50))
INSERT INTO Category (ID, Name)
VALUES
(1, 'Fruit'),
(2, 'Vegetable')
CREATE TABLE Product (ProductID int, CategoryID int, ProductName varchar(50))
INSERT INTO Product (ProductID, CategoryID, ProductName)
VALUES
(1, 1, 'Apple'),
(2, 1, 'Orange'),
(3, 2, 'Celery'),
(4, 2, 'Carrot'),
(5, 1, 'Banana')
Statement (using FOR JSON AUTO and an appropriate join and SELECT structure):
SELECT
c.Name AS name, c.ID as id,
items.ProductName AS productname, items.ProductId AS productid
FROM Category c
JOIN Product items ON (c.ID = items.CategoryId)
ORDER BY c.ID, items.ProductId
FOR JSON AUTO, ROOT ('menu')
Statement (using two FOR JSON AUTO statements):
SELECT
c.Name AS name, c.ID as id,
items = (
SELECT p.ProductName AS productname, p.ProductID AS productid
FROM Product p
WHERE p.CategoryID = c.ID
FOR JSON AUTO
)
FROM Category c
FOR JSON AUTO, ROOT ('menu')
Result:
{
"menu":[
{
"name":"Fruit",
"id":1,
"items":[
{"productname":"Apple","productid":1},
{"productname":"Orange","productid":2},
{"productname":"Banana","productid":5}
]
},
{
"name":"Vegetable",
"id":2,
"items":[
{"productname":"Celery","productid":3},
{"productname":"Carrot","productid":4}
]
}
]
}
Try this:
DECLARE #Category TABLE
(
[ID] TINYINT
,[Name] VARCHAR(12)
);
INSERT INTO #Category ([ID], [Name])
VALUES (1, 'Fruit')
,(2, 'Vegetable');
DECLARE #Product TABLE
(
[ProductID] TINYINT
,[CategoryID] TINYINT
,[ProductName] VARCHAR(12)
);
INSERT INTO #Product ([ProductID], [CategoryID], [ProductName])
VALUES (1, 1, 'Apple')
,(2, 1, 'Orange')
,(3, 2, 'Celery')
,(4, 2, 'Carrot')
,(5, 1, 'Banana');
SELECT
c.ID As CategoryID,
c.Name As CategoryName,
Items.ProductName as ProductName,
Items.ProductID as ProductID
FROM #Category c
INNER JOIN #Product Items
ON
c.ID = Items.CategoryID
FOR JSON AUTO, ROOT('Menu')

mySQL WHERE IN from JSON Array

I have a table with JSON data in it, and a statement that pulls out an array of ID's for each row...
SELECT items.data->"$.matrix[*].id" as ids
FROM items
This results in something like..
+------------+
| ids |
+------------+
| [1,2,3] |
+------------+
Next I want to select from another table where the ID of that other table is in the array, similar to the WHERE id IN ('1,2,3') but using the JSON array...
Something along the lines of...
SELECT * FROM other_items
WHERE id IN (
SELECT items.data->"$.matrix[*].id" FROM items
);
but it needs some JSON magic and I cant work it out...
Below is a complete answer. You may want a 'use <db_name>;' statement at the top of the script. The point is to show that JSON_CONTAINS() may be used to achieve the desired join.
DROP TABLE IF EXISTS `tmp_items`;
DROP TABLE IF EXISTS `tmp_other_items`;
CREATE TABLE `tmp_items` (`id` int NOT NULL PRIMARY KEY AUTO_INCREMENT, `data` json NOT NULL);
CREATE TABLE `tmp_other_items` (`id` int NOT NULL, `text` nvarchar(30) NOT NULL);
INSERT INTO `tmp_items` (`data`)
VALUES
('{ "matrix": [ { "id": 11 }, { "id": 12 }, { "id": 13 } ] }')
, ('{ "matrix": [ { "id": 21 }, { "id": 22 }, { "id": 23 }, { "id": 24 } ] }')
, ('{ "matrix": [ { "id": 31 }, { "id": 32 }, { "id": 33 }, { "id": 34 }, { "id": 35 } ] }')
;
INSERT INTO `tmp_other_items` (`id`, `text`)
VALUES
(11, 'text for 11')
, (12, 'text for 12')
, (13, 'text for 13')
, (14, 'text for 14 - never retrieved')
, (21, 'text for 21')
, (22, 'text for 22')
-- etc...
;
-- Show join working:
SELECT
t1.`id` AS json_table_id
, t2.`id` AS joined_table_id
, t2.`text` AS joined_table_text
FROM
(SELECT st1.id, st1.data->'$.matrix[*].id' as ids FROM `tmp_items` st1) t1
INNER JOIN `tmp_other_items` t2 ON JSON_CONTAINS(t1.ids, CAST(t2.`id` as json), '$')
You should see the following results:
Starting from MySQL 8.0.13, there is MEMBER OF operator, which does exactly what you're looking for.
The query should be rewritten in the form of JOIN, though:
SELECT o.* FROM other_items o
JOIN items i ON o.id MEMBER OF(i.data->>'$.id')
If you want your query to have better performance, consider using multi-valued indexes on your JSON column.
Using of MEMBER OF() can be explained more clearly on the following example:
CREATE TABLE items ( data JSON );
INSERT INTO items
SET data = '{"id":[1,2,3]}';
That is how you find out whether the value is present in the JSON array:
SELECT * FROM items
WHERE 3 MEMBER OF(data->>'$.id');
+-------------------+
| data |
+-------------------+
| {"id": [1, 2, 3]} |
+-------------------+
1 row in set (0.00 sec)
Note that type of the value matters in this case, unlike regular comparison. If you pass it in a form of string, there will be no match:
SELECT * FROM items
WHERE "3" MEMBER OF(data->>'$.id');
Empty set (0.00 sec)
Although regular comparison would return 1:
SELECT 3 = "3";
+---------+
| 3 = "3" |
+---------+
| 1 |
+---------+
1 row in set (0.00 sec)
Before JSON being introduced in MySQL, I use this:
Ur original data: [1,2,3]
After replace comma with '][': [1][2][3]
Wrap ur id in '[]'
Then use REVERSE LIKE instead of IN: WHERE '[1][2][3]' LIKE
'%[1]%'
Answer to your question:
SELECT * FROM other_items
WHERE
REPLACE(SELECT items.data->"$.matrix[*].id" FROM items, ',', '][')
LIKE CONCAT('%', CONCAT('[', id, ']'), '%')
Why wrap into '[]'
'[12,23,34]' LIKE '%1%' --> true
'[12,23,34]' LIKE '%12%' --> true
If wrap into '[]'
'[12][23][34]' LIKE '%[1]%' --> false
'[12][23][34]' LIKE '%[12]%' --> true
Take care that the accepted answer won't use index on tmp_other_items leading to slow performances for bigger tables.
In such case, I usually use an integers table, containing integers from 0 to an arbitrary fixed number N (below, about 1 million), and I join on that integers table to get the nth JSON element:
DROP TABLE IF EXISTS `integers`;
DROP TABLE IF EXISTS `tmp_items`;
DROP TABLE IF EXISTS `tmp_other_items`;
CREATE TABLE `integers` (`n` int NOT NULL PRIMARY KEY);
CREATE TABLE `tmp_items` (`id` int NOT NULL PRIMARY KEY AUTO_INCREMENT, `data` json NOT NULL);
CREATE TABLE `tmp_other_items` (`id` int NOT NULL PRIMARY KEY, `text` nvarchar(30) NOT NULL);
INSERT INTO `tmp_items` (`data`)
VALUES
('{ "matrix": [ { "id": 11 }, { "id": 12 }, { "id": 13 } ] }'),
('{ "matrix": [ { "id": 21 }, { "id": 22 }, { "id": 23 }, { "id": 24 } ] }'),
('{ "matrix": [ { "id": 31 }, { "id": 32 }, { "id": 33 }, { "id": 34 }, { "id": 35 } ] }')
;
-- Put a lot of rows in integers (~1M)
INSERT INTO `integers` (`n`)
(
SELECT
a.X
+ (b.X << 1)
+ (c.X << 2)
+ (d.X << 3)
+ (e.X << 4)
+ (f.X << 5)
+ (g.X << 6)
+ (h.X << 7)
+ (i.X << 8)
+ (j.X << 9)
+ (k.X << 10)
+ (l.X << 11)
+ (m.X << 12)
+ (n.X << 13)
+ (o.X << 14)
+ (p.X << 15)
+ (q.X << 16)
+ (r.X << 17)
+ (s.X << 18)
+ (t.X << 19) AS i
FROM (SELECT 0 AS x UNION SELECT 1) AS a
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS b ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS c ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS d ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS e ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS f ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS g ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS h ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS i ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS j ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS k ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS l ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS m ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS n ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS o ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS p ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS q ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS r ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS s ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS t ON TRUE)
;
-- Insert normal rows (a lot!)
INSERT INTO `tmp_other_items` (`id`, `text`)
(SELECT n, CONCAT('text for ', n) FROM integers);
Now you cna try again the accepted answer's query, which takes about 11seconds to run (but is simple):
-- Show join working (slow)
SELECT
t1.`id` AS json_table_id
, t2.`id` AS joined_table_id
, t2.`text` AS joined_table_text
FROM
(SELECT st1.id, st1.data->'$.matrix[*].id' as ids FROM `tmp_items` st1) t1
INNER JOIN `tmp_other_items` t2 ON JSON_CONTAINS(t1.ids, CAST(t2.`id` as JSON), '$')
;
And compare it to the faster approach of converting the JSON into a (temporary) table of ids, and then doing a JOIN over it (which lead to instant results, 0.000sec according to heidiSQL):
-- Fast
SELECT
i.json_table_id,
t2.id AS joined_table_id,
t2.`text` AS joined_table_text
FROM (
SELECT
j.json_table_id,
-- Don't forget to CAST if needed, so the column type matches the index type
-- Do an "EXPLAIN" and check its warnings if needed
CAST(JSON_EXTRACT(j.ids, CONCAT('$[', i.n - 1, ']')) AS UNSIGNED) AS id
FROM (
SELECT
st1.id AS json_table_id,
st1.data->'$.matrix[*].id' as ids,
JSON_LENGTH(st1.data->'$.matrix[*].id') AS len
FROM `tmp_items` AS st1) AS j
INNER JOIN integers AS i ON i.n BETWEEN 1 AND len) AS i
INNER JOIN tmp_other_items AS t2 ON t2.id = i.id
;
The most inner SELECT retrieves the list of JSON ids, along with their length (for outer join).
The 2nd inner SELECT takes this list of ids, and JOIN on the integers to retrieve the nth id of every JSON list, leading to a table of ids (instead of a table of jsons).
The outer most SELECT now only has to join this table of ids with the table containing the data you wanted.
Below is the same query using WHERE IN, to match the question title:
-- Fast (using WHERE IN)
SELECT t2.*
FROM tmp_other_items AS t2
WHERE t2.id IN (
SELECT
CAST(JSON_EXTRACT(j.ids, CONCAT('$[', i.n - 1, ']')) AS UNSIGNED) AS id
FROM (
SELECT
st1.data->'$.matrix[*].id' as ids,
JSON_LENGTH(st1.data->'$.matrix[*].id') AS len
FROM `tmp_items` AS st1) AS j
INNER JOIN integers AS i ON i.n BETWEEN 1 AND len)
;

One-to-many relationship MySQL - get only one many-record

I got 2 tables, one of my table looks like this (example):
tbl1
-------------
id int PK
name string
The other one looks like:
tbl2
-------------
type int PK FK (tbl1.id)
langid int PK
content string
So what I want: I want to get all rows from tbl1 joined together with results from tbl2. But what I don't want is for every langid add a result to the join. So if tbl1 contains this data:
id: 1, name: test1
id: 2, name: test2
And tbl2 contains this data:
type: 1, langid: 1, content: testcontent
type: 1, langid: 2, content: testcontent2
type: 2, langid: 3, content: testcontent3
I want only the following data:
tbl1.id: 1, tbl1.name: test1, tbl2.type: 1, tbl2.langid: 1, tbl2.content: testcontent
tbl1.id: 2, tbl1.name: test2, tbl2.type: 2, tbl2.langid: 3, tbl2.content: testcontent3
So it has to get one result with the langid that exist. Hopefully I explained it well. I tried this:
SELECT * FROM `tbl1` INNER JOIN `tbl2` ON (`tbl1`.`id` = `tbl2`.`type`) WHERE `tbl2`.`langid` = 1
But sometimes langid 1 doesn't exist, and just langid 2 by that header. I need to get the tbl1 row with one result from tbl2.
Original submission (just gets first response... but that's not what you want...)
SELECT * FROM `tbl1` INNER JOIN `tbl2` ON (`tbl1`.`id` = `tbl2`.`type`) WHERE 1 LIMIT 1
Update -- I think this is what you want. This works on the SQLfiddle
select *
FROM `tbl1` INNER JOIN `tbl2`
ON ( `tbl1`.`id` = `tbl2`.`type`)
Group by tbl1.id
Response:
id name type langid content
1 test1 1 1 content1
2 test2 2 3 content3
I think you want a LEFT JOIN, with the condition on the other table in the join condition:
SELECT *
FROM `tbl1`
LEFT JOIN `tbl2` ON `tbl1`.`id` = `tbl2`.`type`
AND `tbl2`.`langid` = 1
This will give you every row from tbl1 once each, with data from tbl2 only if it exists with langid 1.