How to combine multiple records from a joined table - sql

I'm out of my depth with this SQL query.
I have two tables A and B with shared data based on a serial number. In table A, there is a unique Serial Nr field, while in B, details relating to a particular serial number are linked vertically over multiple records by a common Group ID. The serial number entry occurs as one of those records in the MyData field. I want to concatenate all records that share the same "Group ID" to a single field in A. For example:
Table A
Serial Nr Name Part Nr
2950 Prod1 1234
2955 Prod2 2345
Table B
Group ID MyData Comments
1 2950 serial nr
1 2016-10 build month
2 2955 serial nr
2 2015-11 build month
and I want Table AxB
Serial Nr Name Part Nr Table B data
2950 Prod1 1234 serial nr, 2950, build month, 2016-10
2955 Prod2 2345 serial nr, 2955, build month, 2015-11
I don't actually want the shared Group ID, but need it as concatenation key.
I have tried to do this with STUFF, but to no avail. Any ideas?

I assume your "TableB" also has a "SerialNr" field (which you just did not model);
That is to say, your Table B actually looks like this:
Serial Nr Group ID MyData Comments
2950 1 2950 serial nr
2950 1 2016-10 build month
2955 2 2955 serial nr
2955 2 2015-11 build month
If so, the following query will aggregate the "Comments" and "MyData" columns into a single row per SerialNumber:
SELECT serialno ,STUFF((SELECT ', ' + Comments + ' - ' + MyData [text()]
FROM TableB
WHERE SerialNo = t.SerialNo
FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)'),1,2,' ') AggregatedData
FROM TableB t
GROUP BY serialno
You could then join that query to your original TableA to obtain the result set you posted, ie:
select *
from TableA
join (SELECT serialno ,STUFF((SELECT ', ' + Comments + ' - ' + MyData [text()]
FROM TableB
WHERE SerialNo = t.SerialNo
FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)'),1,2,' ') AggregatedData
FROM TableB t
GROUP BY serialno
) AggregatedTableB
on TableA.SerialNo = AggregatedTableB.SerialNo
UPDATED:
Ok - so based on the fact that your "TableB" doesn't have its own SerialNr row, and instead its buried within the table data.. You need to find a way to extract that row-level data into a column.
Here is a query that can do that:
select tableB.GroupId, MyData, Comments, SerialNo
from tableB
join
(select MyData as serialNo, groupId
from tableb
where Comments ='serial nr') TableBWithSerialNo on tableB.GroupId = TableBWithSerialNo.GroupId
Now that you have this query which adds the Serial No as a column, you can use it in place of just using TableB in the above query. Here's what it would look like:
SELECT SerialNo ,STUFF((SELECT ', ' + Comments + ' - ' + MyData [text()]
FROM
(select tableB.GroupId, MyData, Comments, SerialNo
from tableB
join
(select MyData as serialNo, groupId
from tableb
where Comments ='serial nr') TableBWithSerialNo on tableB.GroupId = TableBWithSerialNo.GroupId
) t1
WHERE t1.SerialNo = t2.SerialNo
FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)'),1,2,' ') AggregatedData
FROM (select tableB.GroupId, MyData, Comments, SerialNo
from tableB
join
(select MyData as serialNo, groupId
from tableb
where Comments ='serial nr') TableBWithSerialNo on tableB.GroupId = TableBWithSerialNo.GroupId
) t2
GROUP BY SerialNo
Granted - this is one ugly query - but that's one ugly table you are working with ;-)
If anything, I'd recommend making the first query into a view, and then using that view in the second query - that way you aren't repeating so much code, ie;
create view TableBView as
select tableB.GroupId, MyData, Comments, SerialNo
from tableB
join
(select MyData as serialNo, groupId
from tableb
where Comments ='serial nr') TableBWithSerialNo on tableB.GroupId = TableBWithSerialNo.GroupId
go
SELECT SerialNo ,STUFF((SELECT ', ' + Comments + ' - ' + MyData [text()]
FROM
TableBView t1
WHERE t1.SerialNo = t2.SerialNo
FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)'),1,2,' ') AggregatedData
FROM TableBView t2
GROUP BY SerialNo

Related

Check exist multiple value from table

I am trying to get data from table by checking some conditions:
Table Detail
CODE | PRODUCT |
FWD 4X4 | PROD1 |
Table Header
CODE | GROUP |
FWD | AAA |
4X4 | AAA |
FWD | CCC |
Expected Result
CODE | PRODUCT | GROUP |
FWD 4X4 | PROD1 | AAA |
Because group AAA have two codes: FWD & 4X4. Group CCC not qualified because only have one code.
Is it possible to do this by SQL query? I've tried with split string and cross apply not even close though.
Maybe I will use programming language if it's too complex. Since I am not really good with SQL.
Code combination maybe become longer too (3 words or more).
Thanks in advance.
Use String_agg function if your sql server version is 17
select a.code,a.prod,a.group from tabledetail a
inner join
(SELECT [group], STRING_AGG (code, ' ') as c
FROM tableheader
GROUP BY [group]) b on a.code=b.c
for lower version you can use stuff function:
select a.code,a.prod,a.group from tabledetail a
inner join
(SELECT [group], c= STUFF(
(SELECT ' ' + code
FROM tableheader t1
WHERE t1.id = t2.id
FOR XML PATH (''))
, 1, 1, '') from tableheader t2
group by [group])b on a.code=b.c
build the schema
create table Detail (CODE varchar(300) ,PRODUCT varchar(100) );
insert into Detail values ('FWD 4X4','PROD1');
insert into Detail values ('FWD','PROD2');
insert into Detail values ('FWD 4X4 FM','PROD3');
create table Header (CODE varchar(300) ,[GROUP] varchar(100) );
insert into Header values ('FWD','AAA');
insert into Header values ('4X4','AAA');
insert into Header values ('FWD','CCC');
insert into Header values ('4X4','DDD');
insert into Header values ('FM','DDD');
insert into Header values ('FWD','DDD');
solution sql
select d.CODE, d.PRODUCT, h.[GROUP]
from Detail d
inner join Header h on CHARINDEX(' ' +h.code+ ' ', ' ' + d.Code + ' ') > 0
inner join (
select [Group],count(Code) GroupCodesCount
from Header
Group By [Group]
) GroupCodes on GroupCodes.[Group] = h.[GROUP]
group by d.CODE, d.PRODUCT, h.[GROUP],GroupCodesCount
having len(d.CODE) - len(replace(d.CODE, ' ', '')) +1 = count(h.code) and count(h.code) = GroupCodesCount
output result
CODE PRODUCT GROUP
FWD PROD2 CCC
FWD 4X4 PROD1 AAA
FWD 4X4 FM PROD3 DDD
I join Detail table with Header when it contains at least one code on the group CHARINDEX(' ' +h.code+ ' ', ' ' + d.Code + ' ') > 0 then join it to count codes of each group view after that I group it by product and group then filter result groups to return groups that match exactly count of group codes having len(d.CODE) - len(replace(d.CODE, ' ', '')) +1 = count(h.code) and count(h.code) = GroupCodesCount
Hope it helps

Comma separated column ids should show values or text(with function or query)

I have a table like this
Foreign table:
select * from table1
ID......NameIds
-------------------
1 ......1, 2 (its comma separated values)
Primary table(table2)
ID Name
-------------------
1 Cleo
2 Smith
I want to show table 1 as like (I require SQL function or query for it)
ID......NameIds
-------------------
1........Cleo, smith (show text/Name instead of values)
As per stated in comments - you should really rethink your table design, but it was interesting enough to try and write a query for that:
SELECT T1.ID, NameID, Name
INTO #Temporary
FROM #Table1 AS T1
CROSS APPLY (
SELECT CAST(('<X>' + REPLACE(T1.NameIDs, ',', '</X><X>') + '</X>') AS XML)
) AS X(XmlData)
CROSS APPLY (
SELECT NameID.value('.', 'INT')
FROM XmlData.nodes('X') AS T(NameID)
) AS T(NameID)
INNER JOIN #Table2 AS T2
ON T2.ID = T.NameID
SELECT ID, STUFF(T.Names, 1, 1, '') AS Names
FROM #Table1 AS T1
CROSS APPLY (
SELECT ',' + Name
FROM #Temporary AS T
WHERE T.ID = T1.ID
ORDER BY T.NameID
FOR XML PATH('')
) AS T(Names)
Result:
ID Names
--------------
1 Cleo,Smith
What it does, it splits your comma seperated list into rows, joins them on NameIDs and then concatenates them again. Guess how efficient is that?
It's probably not the most best way to do that, but it works.

SQL Query to Find Most Effective Data

I want to write query for find most effective rows. I have these tables:
Sellers
Id Name
1 Mark
2 Julia
3 Peter
Stocks
Id SellerId ProductCode StockCount
1 1 30A 10
2 2 20A 4
3 1 20A 2
4 3 42B 3
And there sqlfiddle http://sqlfiddle.com/#!6/fe5b1/1/0
My Intent find optimum Seller for stock.
If client want 30A, 20A and 42B products. I need return to "Mark" and "Peter" because Mark have both product(30A and 20A), so not need there Julia.
How can i solve this in sql ?
Got it to work with the help of temporary tables
SELECT
s.SellerId,
ProductList = STUFF((
SELECT ',' + ProductCode FROM Stocks
WHERE s.SellerId = Stocks.SellerId
ORDER BY ProductCode FOR XML PATH('')
)
, 1, 1, ''), COUNT(*) AS numberOfProducts
INTO #tmptable
FROM
Stocks s
WHERE
s.ProductCode IN ('30A','20A','42B')
AND s.StockData > 0
GROUP BY s.SellerId;
/*this second temp table is necessary, so we can delete from one of them*/
SELECT * INTO #tmptable2 FROM #tmptable;
DELETE t1 FROM #tmptable t1
WHERE EXISTS (SELECT 1 FROM #tmptable2 t2
WHERE t1.SellerId != t2.SellerId
AND t2.ProductList LIKE '%' + t1.ProductList + '%'
AND t2.numberOfProducts > t1.numberOfProducts)
;
SELECT Name FROM #tmptable t INNER JOIN Sellers ON t.SellerId = Sellers.Id;
UPDATE:
Please have a try with static tables:
CREATE TABLE tmptable (SellerId int, ProductList nvarchar(max), numberOfProducts int);
same for tmpTable2. Then change above code to
INSERT INTO tmpTable
SELECT
s.SellerId,
ProductList = STUFF((
SELECT ',' + ProductCode FROM Stocks
WHERE s.SellerId = Stocks.SellerId
ORDER BY ProductCode FOR XML PATH('')
)
, 1, 1, ''), COUNT(*) AS numberOfProducts
FROM
Stocks s
WHERE
s.ProductCode IN ('30A','20A','42B')
AND s.StockData > 0
GROUP BY s.SellerId;
INSERT INTO tmpTable2 SELECT * FROM tmpTable;
DELETE t1 FROM tmptable t1
WHERE EXISTS (SELECT 1 FROM tmptable2 t2
WHERE t1.SellerId != t2.SellerId
AND t2.ProductList LIKE '%' + t1.ProductList + '%'
AND t2.numberOfProducts > t1.numberOfProducts)
;
SELECT * FROM tmpTable;
DROP TABLE tmpTable, tmpTable2;
I think this might be what you are looking for?
Select name,sum(stockdata) as stockdata from sellers s1 join Stocks s2 on s1.id=s2.sellerid
where ProductCode in ('30A','20A','42B')
group by name
order by sum(stockdata) desc
I hope it helps.
if you only want the top 2 ppl. You write
Select top 2 name,sum(stockdata) as stockdata from sellers s1 join Stocks s2 on s1.id=s2.sellerid
where ProductCode in ('30A','20A','42B')
group by name
order by sum(stockdata) desc
I think this is what you are looking for, since how I see it, you want to select the two people who has the highest stockdata?

Select query to get all data from junction table to one field

I have 2 tables and 1 junction table:
table 1 (Log): | Id | Title | Date | ...
table 2 (Category): | Id | Title | ...
junction table between table 1 and 2:
LogCategory: | Id | LogId | CategoryId
now, I want a sql query to get all logs with all categories title in one field,
something like this:
LogId, LogTitle, ..., Categories(that contains all category title assigned to this log id)
can any one help me solve this? thanks
Try this code:
DECLARE #results TABLE
(
idLog int,
LogTitle varchar(20),
idCategory int,
CategoryTitle varchar(20)
)
INSERT INTO #results
SELECT l.idLog, l.LogTitle, c.idCategory, c.CategoryTitle
FROM
LogCategory lc
INNER JOIN Log l
ON lc.IdLog = l.IdLog
INNER JOIN Category c
ON lc.IdCategory = c.IdCategory
SELECT DISTINCT
idLog,
LogTitle,
STUFF (
(SELECT ', ' + r1.CategoryTitle
FROM #results r1
WHERE r1.idLog = r2.idLog
ORDER BY r1.idLog
FOR XML PATH ('')
), 1, 2, '')
FROM
#results r2
Here you have a simple SQL Fiddle example
I'm sure this query can be written using only one select, but this way it is readable and I can explain what the code does.
The first select takes all Log - Category matches into a table variable.
The second part uses FOR XML to select the category names and return the result in an XML instead of in a table. by using FOR XML PATH ('') and placing a ', ' in the select, all the XML tags are removed from the result.
And finally, the STUFF instruction replaces the initial ', ' characters of every row and writes an empty string instead, this way the string formatting is correct.

Is it possible to concatenate column values into a string using CTE?

Say I have the following table:
id|myId|Name
-------------
1 | 3 |Bob
2 | 3 |Chet
3 | 3 |Dave
4 | 4 |Jim
5 | 4 |Jose
-------------
Is it possible to use a recursive CTE to generate the following output:
3 | Bob, Chet, Date
4 | Jim, Jose
I've played around with it a bit but haven't been able to get it working. Would I do better using a different technique?
I do not recommend this, but I managed to work it out.
Table:
CREATE TABLE [dbo].[names](
[id] [int] NULL,
[myId] [int] NULL,
[name] [char](25) NULL
) ON [PRIMARY]
Data:
INSERT INTO names values (1,3,'Bob')
INSERT INTO names values 2,3,'Chet')
INSERT INTO names values 3,3,'Dave')
INSERT INTO names values 4,4,'Jim')
INSERT INTO names values 5,4,'Jose')
INSERT INTO names values 6,5,'Nick')
Query:
WITH CTE (id, myId, Name, NameCount)
AS (SELECT id,
myId,
Cast(Name AS VARCHAR(225)) Name,
1 NameCount
FROM (SELECT Row_number() OVER (PARTITION BY myId ORDER BY myId) AS id,
myId,
Name
FROM names) e
WHERE id = 1
UNION ALL
SELECT e1.id,
e1.myId,
Cast(Rtrim(CTE.Name) + ',' + e1.Name AS VARCHAR(225)) AS Name,
CTE.NameCount + 1 NameCount
FROM CTE
INNER JOIN (SELECT Row_number() OVER (PARTITION BY myId ORDER BY myId) AS id,
myId,
Name
FROM names) e1
ON e1.id = CTE.id + 1
AND e1.myId = CTE.myId)
SELECT myID,
Name
FROM (SELECT myID,
Name,
(Row_number() OVER (PARTITION BY myId ORDER BY namecount DESC)) AS id
FROM CTE) AS p
WHERE id = 1
As requested, here is the XML method:
SELECT myId,
STUFF((SELECT ',' + rtrim(convert(char(50),Name))
FROM namestable b
WHERE a.myId = b.myId
FOR XML PATH('')),1,1,'') Names
FROM namestable a
GROUP BY myId
A CTE is just a glorified derived table with some extra features (like recursion). The question is, can you use recursion to do this? Probably, but it's using a screwdriver to pound in a nail. The nice part about doing the XML path (seen in the first answer) is it will combine grouping the MyId column with string concatenation.
How would you concatenate a list of strings using a CTE? I don't think that's its purpose.
A CTE is just a temporarily-created relation (tables and views are both relations) which only exists for the "life" of the current query.
I've played with the CTE names and the field names. I really don't like reusing fields names like id in multiple places; I tend to think those get confusing. And since the only use for names.id is as a ORDER BY in the first ROW_NUMBER() statement, I don't reuse it going forward.
WITH namesNumbered as (
select myId, Name,
ROW_NUMBER() OVER (
PARTITION BY myId
ORDER BY id
) as nameNum
FROM names
)
, namesJoined(myId, Name, nameCount) as (
SELECT myId,
Cast(Name AS VARCHAR(225)),
1
FROM namesNumbered nn1
WHERE nameNum = 1
UNION ALL
SELECT nn2.myId,
Cast(
Rtrim(nc.Name) + ',' + nn2.Name
AS VARCHAR(225)
),
nn.nameNum
FROM namesJoined nj
INNER JOIN namesNumbered nn2 ON nn2.myId = nj.myId
and nn2.nameNum = nj.nameCount + 1
)
SELECT myId, Name
FROM (
SELECT myID, Name,
ROW_NUMBER() OVER (
PARTITION BY myId
ORDER BY nameCount DESC
) AS finalSort
FROM namesJoined
) AS tmp
WHERE finalSort = 1
The first CTE, namesNumbered, returns two fields we care about and a sorting value; we can't just use names.id for this because we need, for each myId value, to have values of 1, 2, .... names.id will have 1, 2 ... for myId = 1 but it will have a higher starting value for subsequent myId values.
The second CTE, namesJoined, has to have the field names specified in the CTE signature because it will be recursive. The base case (part before UNION ALL) gives us records where nameNum = 1. We have to CAST() the Name field because it will grow with subsequent passes; we need to ensure that we CAST() it large enough to handle any of the outputs; we can always TRIM() it later, if needed. We don't have to specify aliases for the fields because the CTE signature provides those. The recursive case (after the UNION ALL) joins the current CTE with the prior one, ensuring that subsequent passes use ever-higher nameNum values. We need to TRIM() the prior iterations of Name, then add the comma and the new Name. The result will be, implicitly, CAST()ed to a larger field.
The final query grabs only the fields we care about (myId, Name) and, within the subquery, pointedly re-sorts the records so that the highest namesJoined.nameCount value will get a 1 as the finalSort value. Then, we tell the WHERE clause to only give us this one record (for each myId value).
Yes, I aliased the subquery as tmp, which is about as generic as you can get. Most SQL engines require that you give a subquery an alias, even if it's the only relation visible at that point.