In a sql query I want to output one column as a json array of child values - json

I have a product table that I query for various fields. There is a ProductTagMapping table that assigns several tags to each product. I would like to add a Tags column to my output row that contains a JSON Array of the associated Tag Names from a ProductTag table. So the result would be:
ProductId: 0
ProductName: Pretty Necklace
Tags: ["gold", "topaz", "fire"]
With a join I would get many identical product rows for each tag:
SELECT p.ProductId, p.ProductName. pt.TagName
FROM Product AS p
INNER JOIN ProductTagMapping as ptm ON ptm.ProductId = p.ProductId
INNER JOIN ProductTag as pt ON pt.TagId = ptm.TagId
I found an almost answer, but it's not correct:
SELECT ROW_NUMBER() OVER (ORDER BY p.SKU) AS Id,
p.Id AS ProductId,
p.Name AS ProductName,
REPLACE(REPLACE((SELECT
pt.Name as TagNames
FROM
ProductTag AS pt
INNER JOIN
dbo.Product_ProductTag_Mapping as ptm ON ptm.Product_Id = p.Id AND ptm.ProductTag_Id = pt.Id
WHERE
ptm.Product_Id = p.Id
ORDER BY
pt.Name
FOR
JSON AUTO
), N'{"pt.Name":', N''), '"}', '"') AS [Tags]
FROM dbo.Product AS p WITH (NOLOCK)
Which yields:
"productid": 8,
"productName": "Gold Tone Earrings",
"tags": "[{\"TagNames\":\"blue\",{\"TagNames\":\"clipon\",{\"TagNames\":\"gold\"]"
How would I approach creating the Tags column correctrly?
Thanks
Abbott

Your code looks alright, but you need to do a JSON_QUERY on your "replace"-string, otherwise it will JSON-ize it as a string which you don't want.
See my version below for what i mean. You also replace wrong word, you probably ment: REPLACE(..., "TagNames", '').
This stuff is if you're using older SQL Server that doesn't support creating arrays or STRING_AGGs. The 2022 has support for creating arrays directly:
create table #products (productid int, productname nvarchar(100))
create table #producttag (productid int, tagid int)
create table #tag (tagid int, tagname nvarchar(100))
insert into #products
values (1, 'Earring')
, (2, 'Blouse')
insert into #tag
values (1, 'Gold')
, (2, 'Metal')
, (3, 'Blue')
, (4, 'Red')
insert into #producttag
VALUES (1, 1)
, (1, 2)
, (1, 3)
, (2, 4)
select productid, productname,
JSON_QUERY(stuff((select ',"' + STRING_ESCAPE(tagname, 'json') + '"'
FROM #producttag pt
INNER JOIN #tag t
ON t.tagid = pt.tagid
WHERE pt.productid = p.productid
for xml path(''), type
).value('.', 'nvarchar(max)'), 1, 1, '[') + ']') tags
from #products p
for json path
I don't like replaces, so instead I create the array string manually by using xml path, and then converts it into a json array.

Related

How to filter records based on specific value in subquery?

Scenario:
I have a table of jobs which I want to fetch. Each job has one or more items associated with it which are stored in jobItems table. item's code and details are saved in items table. There is one-to-many relationship between jobs and jobItems tables. Also keep in mind, it has large dataset.
I want to show all jobs which has one specific item in their jobItems.
MySQL fiddle: http://sqlfiddle.com/#!9/4a5a47
Schema:
CREATE TABLE jobs (
`id` INT,
`jobRef` VARCHAR (55)
);
INSERT INTO jobs (`id`, `jobRef`)
VALUES
(1, 'job1'),
(2, 'job2'),
(3, 'job3');
CREATE TABLE jobItems (
`id` INT,
`itemId` INT,
`jobId` INT
);
INSERT INTO jobItems (`id`, `itemId`, `jobId`)
VALUES
(1, 1, 1),
(2, 2, 1),
(3, 3, 1),
(4, 1, 2),
(5, 2, 2),
(6, 3, 3);
CREATE TABLE items (
`id` INT,
`itemCode` VARCHAR (55)
);
INSERT INTO items (`id`, `itemCode`)
VALUES
(1, 'item1'),
(2, 'item2'),
(3, 'item3');
Query:
SELECT
jobs.*, ji.allItems
FROM
jobs
LEFT JOIN (
SELECT
jobItems.jobId,
GROUP_CONCAT(items.itemCode) AS allItems
FROM
jobItems
INNER JOIN items ON jobItems.itemId = items.id
GROUP BY
jobItems.jobId
) AS ji ON ji.jobId = jobs.id
As you noticed, there is a LEFT JOIN as well as a GROUP BY on jobItems.jobId which are creating issues in implementing this item based job filter.
Tried Options:
I tried to remove the GROUP BY and GROUP_CONCAT, so it will return
all the possible job-item combinations. I thought to manipulate them
using php at backend. But it has one drawback that it disturbs the
pagination.
I also tried to dynamically change LEFT JOIN into INNER JOIN and
inside the subquery, I added a condition on the INNER JOIN INNER JOIN
items ON jobItems.itemId = items.id AND jobItems.itemId IN (1) But,
it doesn't fetch desired result because of the GROUP BY as it will
only return the jobs which have only one item with the given itemId.
It doesn't return jobs which have multiple items including item with
itemId= 1.
In short, I want to fetch all jobs which have items containing item with itemId = 1. And expected result is job1 and job2 as they both have item1 in them.
Try this
SELECT jobs.*, ji.allItems
FROM jobs
INNER JOIN (
SELECT
jobItems.jobId,
GROUP_CONCAT(items.itemCode) AS allItems
FROM
jobItems
INNER JOIN items ON jobItems.itemId = items.id
GROUP BY
jobItems.jobId
HAVING MAX(CASE WHEN jobItems.itemId = 1 THEN 1 ELSE 0 END) > 0
) AS ji ON ji.jobId = jobs.id
SQLFiddle
For multiple item filters (for example you want to find jobs associated with both items 1 and 2)
SELECT jobs.*, ji.allItems
FROM jobs
INNER JOIN (
SELECT
jobItems.jobId,
GROUP_CONCAT(items.itemCode) AS allItems
FROM
jobItems
INNER JOIN items ON jobItems.itemId = items.id
GROUP BY
jobItems.jobId
HAVING
MAX(CASE WHEN jobItems.itemId = 1 THEN 1 ELSE 0 END) > 0 AND
MAX(CASE WHEN jobItems.itemId = 2 THEN 1 ELSE 0 END) > 0
) AS ji ON ji.jobId = jobs.id
Is this work for you.
select j.*, i.itemCode from jobs j
inner join jobItems m on m.jobid = j.id
inner join items i on i.id = m.itemId
where exists (select 'x' from jobItems a
where a.itemId = 2
and a.jobId = j.id)
Hope you want to get jobs which particular item binded.
Let me to know if i miss understand your requirement.
http://sqlfiddle.com/#!9/4a5a47/11

How do I write a query to count records with multiple constraints and matches?

I have 5 tables:
Files
Tags
join_File_Tags
Profiles
join_Profile_Tags
The Profiles and the join_Profile_Tags tables determine which Files a user can see.
Data might look like this:
File1 has tags One, Three
File2 has tags One, Five
File3 has tags One, Three, Six
Profile1 has access to tags Two, Three, Four
Profile2 has access to tags One, Two, Three, Four, Five
I needed a query that returns the files that match ALL of its tags inside a Profile's list of tags.
And I came up with this:
SELECT
files.id,
files.FileName,
(SELECT COUNT(j_file_tags.id) from j_file_tags WHERE j_file_tags.fileID = files.id ) as fileTagCount
FROM files
LEFT JOIN j_file_tags ON j_file_tags.fileID = files.id
LEFT JOIN tags ON tags.id = j_file_tags.tagID
WHERE
files.caseID = '123456'
AND
j_file_tags.tagID IN ("One, Two, Three, Four, Five") /* i can get these before hand */
GROUP BY
files.id
HAVING
COUNT(j_file_tags.id) = fileTagCount /* this makes sure that the user has access to ALL the tags applied to the file*/
This gives me exactly what I need. BUT what I'm trying to get now is: how many files are there in each tag?
So in my example data at the top, the Profile2 user will see File1 and File2 but not File3 because that one has tag six, which profile2 doesn't have access to. I need a query that builds a tag cloud (no biggie based on the j_profile_tags table) but I need the tags to include file count. I need tag One to show the number 2, Three shows 1, Five shows 1. The queries I attempted so far include the count for all the files, so tag One comes in with a count of 3 even though Profile2 doesn't have access to that third file.
Here's my half working query:
SELECT
tags.id,
tags.TagName,
COUNT(tags.id) as tagCount
FROM
tags
INNER JOIN j_file_tags ON tags.id = j_file_tags.tagID
INNER JOIN files ON j_file_tags.fileID = files.id
INNER JOIN j_profile_tags ON j_profile_tags.tagID = tags.id AND j_profile_tags.profileID = 'Profile2'
WHERE
files.caseID = '123456'
GROUP BY
tags.id
I dont fully understand about your tables, especially that "Employee" Column.
But if what you mean is getting How many Files that contained in each Tag filtered by a Profile, then please try below queries:
DECLARE #TblFiles AS TABLE(ID int identity(1, 1) primary key, TheFileName varchar(50))
DECLARE #TblTags AS TABLE(ID int identity(1, 1) primary key, TheTag varchar(50))
DECLARE #TblProfiles AS TABLE(ID int identity(1, 1) primary key, TheProfile varchar(50))
DECLARE #TblFileTagMapper AS TABLE(ID int identity(1, 1) primary key, FileID int, TagID int)
DECLARE #TblProfileTagMapper AS TABLE(ID int identity(1, 1) primary key, ProfileID int, TagID int)
INSERT INTO #TblFiles(TheFileName) VALUES ('File1'), ('File2'), ('File3')
INSERT INTO #TblTags(TheTag) VALUES ('One'), ('Two'), ('Three'), ('Four'), ('Five'), ('Six')
INSERT INTO #TblProfiles(TheProfile) VALUES ('Profile1'), ('Profile2')
INSERT INTO #TblFileTagMapper(FileID, TagID) VALUES (1,1), (1,3), (2,1), (2,5), (3,1), (3,3), (3,6)
INSERT INTO #TblProfileTagMapper(ProfileID, TagID) VALUES (1,2), (1,3), (1,4), (2,1), (2,2), (2,3), (2,4), (2,5)
--Display File-Tag
SELECT TheFileName
,STUFF((SELECT ', ' + CAST(TheTag AS VARCHAR(10)) [text()]
FROM (SELECT F.TheFileName, T.TheTag FROM #TblFileTagMapper FT INNER JOIN #TblFiles F ON FT.FileID = F.ID INNER JOIN #TblTags T ON FT.TagID = T.ID) FT
WHERE TheFileName = t.TheFileName
FOR XML PATH(''), TYPE)
.value('.','NVARCHAR(MAX)'),1,2,' ') Tags
FROM (SELECT F.TheFileName, T.TheTag FROM #TblFileTagMapper FT INNER JOIN #TblFiles F ON FT.FileID = F.ID INNER JOIN #TblTags T ON FT.TagID = T.ID) t
GROUP BY TheFileName
--Display Profile-Tag
SELECT TheProfile
,STUFF((SELECT ', ' + CAST(TheTag AS VARCHAR(10)) [text()]
FROM (SELECT P.TheProfile, T.TheTag FROM #TblProfileTagMapper PT INNER JOIN #TblProfiles P ON PT.ProfileID = P.ID INNER JOIN #TblTags T ON PT.TagID = T.ID) PT
WHERE TheProfile = t.TheProfile
FOR XML PATH(''), TYPE)
.value('.','NVARCHAR(MAX)'),1,2,' ') Tags
FROM (SELECT P.TheProfile, T.TheTag FROM #TblProfileTagMapper PT INNER JOIN #TblProfiles P ON PT.ProfileID = P.ID INNER JOIN #TblTags T ON PT.TagID = T.ID) t
GROUP BY TheProfile
--> Param Input
DECLARE #ParamProfile VARCHAR(50) = 'Profile2'
--> The Solution Query
SELECT T.TheTag, COALESCE(X.FileCount, 0) AS NumberOfFilesWhichContainTheTag
FROM #TblTags T
LEFT JOIN(
SELECT FT.TagID, COUNT(FT.FileID) AS FileCount
FROM #TblFileTagMapper FT
WHERE EXISTS(SELECT 1 FROM #TblProfileTagMapper PT INNER JOIN #TblProfiles P ON PT.ProfileID = P.ID WHERE PT.TagID = FT.TagID AND P.TheProfile = #ParamProfile)
GROUP BY FT.TagID
) X ON T.ID = X.TagID
These might be not the real answer to your question, but might be a help to get you there.

Mysql trying to insert rows and ignore duplicate entries with lot of issues

I have tried many answers from what I have seen on this site including 'where not exists' clause. Here is the original sql I am dealing with
INSERT INTO tbl_Feed
(FeedTypeID, CustomerID, Name, Code, Color, Icon, FeedTypeSortOrder, IsUsingEmailAddress, IsActive)
select DISTINCT
3, c.CustomerID, "Unattached Image", "Unattached Image", "0x0093D0", "offline_01.png", COALESCE((select MAX(FeedTypeSortOrder) as FeedTypeSortOrderMax from tbl_Feed where FeedTypeID = 3 and customerid=c.CustomerID) + 1, 1), 0, 1
from tbl_Customer c
join tbl_Feed f on f.customerid = c.customerid
where f.code != "Unattached Image"
If I run this twice, it enters another row for "Unattached Image". I want it to only join tables that don't already have this value... I thought for sure changing the where clause to where not exist(select name from tbl_Feed where name != "Unattached Image") but this updated 0 rows. Please advise.
You need to use NOT EXISTS:
INSERT INTO tbl_Feed
(FeedTypeID, CustomerID, Name, Code, Color, Icon, FeedTypeSortOrder, IsUsingEmailAddress, IsActive)
select DISTINCT
3, c.CustomerID, 'Unattached Image', 'Unattached Image', '0x0093D0',
'offline_01.png', COALESCE((select MAX(FeedTypeSortOrder) as FeedTypeSortOrderMax from tbl_Feed where FeedTypeID = 3 and customerid=c.CustomerID) + 1, 1), 0, 1
from tbl_Customer c
join tbl_Feed f on f.customerid = c.customerid
where NOT EXISTS(select 1 from tbl_Feed where name = 'Unattached Image' AND CustomerID = c.CustomerID)

MySQL JOIN Statement to filter rows by selected linked attributes

lets say i have the following two MySQL-Tables:
item: _ID,_CAT_ID,...
item_attribute:
_ID,_ITEM_ID,...
i want to (filter items) get ONLY items which have ALL selected attributes (1,32,555,...an array of selected attributes )
something like this:
SELECT _I.*
FROM item _I
INNER JOIN item_attribute _IA
ON (_I._ID = _IA._ITEM_ID AND (_IA._ID=1 OR _IA._ID=132, ...))
WHERE _I._CAT_ID=? ORDER BY _I._LAST_UPDATE ASC;"
this "wrong" statement returns items when one(due to OR) of the linked ids found, what i want is: only items which have all linked attributes.
if i change
(_IA._ID=1 OR _IA._ID=132 OR...)
to
(_IA._ID=1 AND _IA._ID=132 AND ...)
no matches and this makes sense, but how to rewrite the statement to get correct matches?
UPDATE:
here is a sqlfiddle: http://sqlfiddle.com/#!9/0f8ebe/6
CREATE TABLE item
(`id` int, `pid` int, `name` varchar(55))
;
INSERT INTO item
(`id`, `pid`, `name`)
VALUES
(1, 2, 'A'),
(2, 2, 'B'),
(3, 2, 'C')
;
CREATE TABLE att
(`id` int, `pid` int, `name` varchar(55))
;
INSERT INTO att
(`id`, `pid`, `name`)
VALUES
(7, 1, 'red'),
(7, 3, 'red'),
(2, 1, '30cm'),
(1, 3, '40cm'),
(5, 2, 'blue'),
(1, 2, '40cm')
;
SELECT *
FROM item;
SELECT *
FROM att;
/* expected: items which are red AND 40cm, result should be then only item C (id=3)*/
SELECT _I.name
FROM item _I
INNER JOIN att _IA
ON (_I.id = _IA.pid AND (_IA.id=7 AND _IA.id=1))
WHERE _I.pid=2 GROUP BY _I.id;
SOLUTION http://sqlfiddle.com/#!9/0f8ebe/8 (using Drews Answer):
SELECT _I.name,count(_IA.id) as theCount
FROM item _I
INNER JOIN att _IA
ON (_I.id = _IA.pid) AND _IA.id in (7,1)
WHERE _I.pid=2
group by _I.id
having theCount=2
I think it is 7 lines of code, reading the tidbits from your question:
SELECT _I.col1,_I.col2,count(_IA._ID) as theCount
FROM item _I
INNER JOIN item_attribute _IA
ON (_I._ID = _IA._ITEM_ID) AND _IA._ID in (1,32,555)
WHERE _I._CAT_ID=? ORDER BY _I._LAST_UPDATE ASC
group by _I.col1,_I.col2
having theCount=3
Note that theCount alias is allowable in a having clause.
Also note I put in col1 and col2 in line one, expand accordingly. The point is to list them, so the group by in line6 can mimic them for the non-aggregated columns. The value in line 7 must match the count of the values in the in clause
It should bring all the items that have 3 particular item attributes (red, 40cm, large).
SQL Fiddle
SELECT kk.*
FROM item AS kk
INNER JOIN (
SELECT aa.id
FROM (
SELECT DISTINCT bb.id
FROM item AS bb
INNER JOIN att AS cc
ON bb.id = cc.pid
WHERE cc.name IN ('red', '40cm', 'large')
GROUP BY bb.id
HAVING COUNT(*) = 3
) AS aa
) AS _aa
ON kk.id = _aa.id;
Result:
id pid name
3 2 C

Make sure records from one table always overwrite others when joining

Here's a Fiddle example
I want to combine five EAV tables into a big table which stores all product attributes. Let's say there are two phones A and B from the same series called Nokia 1150. Both phones share some identical information like price,brand,ram. The series use green plastic cases. But B phone is a special edition. Its case is made of gold and is black. I want to pivot the tables and use IFNULL or coalesce to make sure that table model_attr always overwrites series_attr
I want the output to be like this:
MODEL_NAME SERIES_NAME PRICE RAM BRAND MATERIAL COLOR
A Nokia Series 5000 512 Nokia Plastic Green
B Nokia Series 5000 512 Nokia Gold Black
My Code:
Select m.model_name,s.series_name,s.price,s.ram,s.brand,
GROUP_CONCAT(
IF(a.attr_name = 'material',a.attr_value, NULL)) AS material,
GROUP_CONCAT(
IF(a.attr_name = 'color',a.attr_value, NULL)) AS color
FROM model m INNER JOIN series s
LEFT JOIN series_attr sa ON sa.series_id = s.series_id
LEFT JOIN model_attr ma ON ma.model_id = m.model_id
LEFT JOIN attr a ON a.attr_id = ma.attr_id
GROUP BY m.model_name
What I'm getting is this:
MODEL_NAME SERIES_NAME PRICE RAM BRAND MATERIAL COLOR
A Nokia Series 5000 512 Nokia (null) (null)
B Nokia Series 5000 512 Nokia Gold,Gold,Gold,Gold Black,Black,Black,Black
Table Schema
CREATE TABLE series
(`series_id` int, `series_name` varchar(20),`price` int,`ram`int,`brand`varchar(20))
;
INSERT INTO series
(`series_id`,`series_name`,`price`,`ram`,`brand`)
VALUES
(1,'Nokia Series',5000,512,'Nokia'),
(2,'Sony Series',2500,1024,'Sony')
;
CREATE TABLE model
(`model_id` int, `model_name` varchar(20),`series_id` int)
;
INSERT INTO model
(`model_id`,`model_name`,`series_id`)
VALUES
(1,'A',1),
(2, 'B',1),
(3, 'C',2)
;
CREATE TABLE attr
(`attr_id` int, `attr_name` varchar(20),`attr_value` varchar(20))
;
INSERT INTO attr
(`attr_id`,`attr_name`,`attr_value`)
VALUES
(1, 'material','Gold'),
(2, 'material','Plastic'),
(3, 'color','Grey'),
(4, 'color','Black'),
(5, 'color','Green')
;
CREATE TABLE series_attr
(`series_id` int, `attr_id` int )
;
INSERT INTO series_attr
(`series_id`,`attr_id`)
VALUES
(1,2),
(1,5),
(2,2),
(2,3)
;
CREATE TABLE model_attr
(`model_id` int, `attr_id` int,`series_group`int )
;
INSERT INTO model_attr
(`model_id`,`attr_id`,`series_group`)
VALUES
(2,1,1),
(2,4,1)
;
I've create a field called series_group in table model_attr. I hope that would make joining easier. Any help would be appreciate
You actually did the bulk of the work. The key change, though, is to join in the attribute table twice, once for the series and once for the models. Then you can look at the attributes from each separately and choose the model, if they exist, or then the series:
Select m.model_name, s.series_name, s.price, s.ram, s.brand,
(case when sum(a.attr_name = 'material') > 0
then GROUP_CONCAT(DISTINCT IF(a.attr_name = 'material', a.attr_value, NULL))
else GROUP_CONCAT(DISTINCT IF(saa.attr_name = 'material', saa.attr_value, NULL))
end) AS material,
(case when sum(a.attr_name = 'color') > 0
then GROUP_CONCAT(DISTINCT IF(a.attr_name = 'color', a.attr_value, NULL))
else GROUP_CONCAT(DISTINCT IF(saa.attr_name = 'color', saa.attr_value, NULL))
end) AS color
FROM model m INNER JOIN
series s
on m.series_id = s.series_id LEFT JOIN
series_attr sa
ON sa.series_id = s.series_id LEFT JOIN
attr saa
on saa.attr_id = sa.attr_id LEFT JOIN
model_attr ma
ON ma.model_id = m.model_id LEFT JOIN
attr a
ON a.attr_id = ma.attr_id
GROUP BY m.model_name;