Use STUFF() on a result set coming from OPENJSON - json

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

Related

How to merge json arrray with table (SQL Server)

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

Split values to multiple row on MySQL

Here is my data in mysql table:
+---------+-------------------+------------+
| ID | Name | Class |
+---------+-------------------+------------+
| 1, 2, 3 | Alex, Brow, Chris | Aa, Bb, Cc |
+---------+-------------------+------------+
I want split values to multiple rows to get data as the below format.
1 Alex Aa
2 Brow Bb
3 Chris Cc
How can I do that?
One trick is to join to a Tally table with numbers.
Then use SUBSTRING_INDEX to get the parts.
If you don't already have a numbers table, here's one way.
drop table if exists Digits;
create table Digits (n int primary key not null);
insert into Digits (n) values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9);
drop table if exists Nums;
create table Nums (n int primary key not null);
insert into Nums (n)
select (n3.n*100+n2.n*10+n1.n) as n
from Digits n1
cross join Digits n2
cross join Digits n3;
Then it can be used to unfold those columns
Sample data:
drop table if exists YourTable;
create table YourTable (
ID varchar(30) not null,
Name varchar(30) not null,
Class varchar(30) not null
);
insert into YourTable
(ID, Name, Class) values
('1, 2, 3', 'Alex, Brow, Chris', 'Aa, Bb, Cc')
, ('4, 5, 6', 'Drake, Evy, Fiona', 'Dd, Ee, Ff')
;
The query:
SELECT
LTRIM(SUBSTRING_INDEX( SUBSTRING_INDEX( t.ID, ',', Nums.n), ',', -1)) AS Id,
LTRIM(SUBSTRING_INDEX( SUBSTRING_INDEX( t.Name, ',', Nums.n), ',', -1)) AS Name,
LTRIM(SUBSTRING_INDEX( SUBSTRING_INDEX( t.Class, ',', Nums.n), ',', -1)) AS Class
FROM YourTable t
LEFT JOIN Nums ON n BETWEEN 1 AND (LENGTH(ID)-LENGTH(REPLACE(ID, ',', ''))+1);
Result:
Id Name Class
1 Alex Aa
2 Brow Bb
3 Chris Cc
4 Drake Dd
5 Evy Ee
6 Fiona Ff
Multiple values are not recommended for a one field but still if you want a solution you can split string by comma and insert to the table.
Please see this blog post which shows how to split https://nisalfdo.blogspot.com/2019/02/mysql-how-to-insert-values-from-comma.html#more

OpenJson in SQL Server for nested json data?

I have a json data like this:
[
{"id": 1}, {"id": 3}, {"id": 2, "children": [{"id": 4}, {"id": 5}]}
]
Please help me how to parse this data into relational data:
Column: Id Order/Index ParentId
---------------------------------------
1 1 0
3 2 0
2 3 0
4 4 2
5 5 2
There are a couple of non-trivial things in this request. First is to order the resulting rows by the document position, which is not visible when you use OPENJSON … WITH to project the columns. And the second one is that you need a hierarchical query (assuming there could be multiple levels).
Anyway, something like this:
declare #doc nvarchar(max) = N'[{"id":1},{"id":3},{"id":2,"children":[{"id":4},{"id":5}]}]';
with q as
(
select [key] nodePath,
cast(json_value(d.[value],'$.id') as int) Id,
cast(null as int) ParentId,
cast(json_query(d.[value],'$.children') as nvarchar(max)) children
from openjson(#doc) d
union all
select q.nodePath + '.' + d.[key] nodePath,
cast(json_value(d.[value],'$.id') as int) Id,
q.id ParentId,
cast(json_query(d.[value],'$.children') as nvarchar(max)) children
from q
outer apply openjson(q.children) d
where q.children is not null
)
select Id, row_number() over (order by nodePath) [Order/Index], ParentId
from q
order by [Order/Index]
outputs
Id Order/Index ParentId
----------- -------------------- -----------
1 1 NULL
3 2 NULL
2 3 NULL
4 4 2
5 5 2
(5 rows affected)

Retrieve Distinct concat values from MySQL table

I have an SQL table advert
id name cat
11 abc ab
12 acb ab, bc
13 abb bcd
14 abcd ad
15 acbd de
16 abbd ad
On using DISTINCT function I am getting an output like this
Query:
SELECT DISTINCT cat FROM advert;
Output:
ab
ab, bc
bcd
ad
de
WHAT changes do I need to make in my query for output like this
ab
bc
bcd
ad
de
select distinct trim(substring_index(substring_index(cat,',',n),',',-1)) as cat
from t join (select 1 as n union all select 2 union all select 3) r
on cat like concat('%',repeat(',%',n-1))
I think you should change your table structure and make it like this.
tblName
id | name
11 abc
12 acb
13 abb
14 abcd
15 acbd
16 abbd
tblCat
id | name_id | cat
some ids* 11 ab
12 ab
12 bc
13 bcd
14 ad
15 de
16 ad
In this way you can easily query and manage your data in your tables.
You should fix your data structure so you are not storing comma-delimited lists in columns. That is the wrong way to store data in a relational database . . . as you can see by the problems for answering this simple question. What you want is a junction table.
Sometimes, we are stuck with other peoples bad designs. You say that there are only two or values, then you can do:
select cat
from ((select substring_index(cat, ', ', 1) as cat
from advert
) union all
(select substring_index(substring_index(cat, ', ', 2), ', ', -1) as cat
from advert
where cat like '%, %'
) union all
(select substring_index(substring_index(cat, ', ', 3), ', ', -1) as cat
from advert
where cat like '%, %, %'
)
) c
group by cat;
First... I would create a statement that would turn all the rows into one big massive comma delimited list.
DECLARE #tmp VarChar(max)
SET #tmp = ''
SELECT #tmp = #tmp + ColumnA + ',' FROM TableA
Then use the table valued udf split described by this SO article to turn that massive string back into a table with a distinct clause to ensure that it's unique.
https://stackoverflow.com/a/2837662/261997
SELECT DISTINCT * FROM dbo.Split(',', #tmp)
Full code example:
if object_id('dbo.Split') is not null
drop function dbo.Split
go
CREATE FUNCTION dbo.Split (#sep char(1), #s varchar(512))
RETURNS table
AS
RETURN (
WITH Pieces(pn, start, stop) AS (
SELECT 1, 1, CHARINDEX(#sep, #s)
UNION ALL
SELECT pn + 1, stop + 1, CHARINDEX(#sep, #s, stop + 1)
FROM Pieces
WHERE stop > 0
)
SELECT pn,
SUBSTRING(#s, start, CASE WHEN stop > 0 THEN stop-start ELSE 512 END) AS s
FROM Pieces
)
go
declare #t table (colA varchar(max))
insert #t select '111, 223'
union all select '333'
union all select '444'
union all select '777,999';
select ltrim(rtrim(s.s)) as colC
from #t t
cross apply
dbo.split(',', t.colA) s

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)
;