Check exist multiple value from table - sql

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

Related

How to combine multiple records from a joined table

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

How to group-concatenate multiple columns?

Assume this table:
PruchaseID | Customer | Product | Method
-----------|----------|----------|--------
1 | John | Computer | Credit
2 | John | Mouse | Cash
3 | Will | Computer | Credit
4 | Will | Mouse | Cash
5 | Will | Speaker | Cash
6 | Todd | Computer | Credit
I want to generate a report on each customer of what they bought, and their payment methods.
But I want that report to be one row per customer, such as:
Customer | Products | Methods
---------|--------------------------|--------------
John | Computer, Mouse | Credit, Cash
Will | Computer, Mouse, Speaker | Credit, Cash
Todd | Computer | Credit
What I've found so far is to group-concatenate using the XML PATH method, such as:
SELECT
p.Customer,
STUFF(
SELECT ', ' + xp.Product
FROM Purchases xp
WHERE xp.Customer = p.Customer
FOR XML PATH('')), 1, 1, '') AS Products,
STUFF(
SELECT ', ' + xp.Method
FROM Purchases xp
WHERE xp.Customer = p.Customer
FOR XML PATH('')), 1, 1, '') AS Methods
FROM Purchases
This gives me the result, but my concern is the speed of this.
At first glance there are three different selects going on here, two would each multiply by the number of rows Purchases has. Eventually this would slow down expenentially.
So, is there a way to do this with better performance?
I want to add even more columns to aggregate, should I do this STUFF() block for every column? That doesn't sound fast enough for me.
Siggestions?
Just an idea:
DECLARE #t TABLE (
Customer VARCHAR(50),
Product VARCHAR(50),
Method VARCHAR(50),
INDEX ix CLUSTERED (Customer)
)
INSERT INTO #t (Customer, Product, Method)
VALUES
('John', 'Computer', 'Credit'),
('John', 'Mouse', 'Cash'),
('Will', 'Computer', 'Credit'),
('Will', 'Mouse', 'Cash'),
('Will', 'Speaker', 'Cash'),
('Todd', 'Computer', 'Credit')
SELECT t.Customer
, STUFF(CAST(x.query('a/text()') AS NVARCHAR(MAX)), 1, 2, '')
, STUFF(CAST(x.query('b/text()') AS NVARCHAR(MAX)), 1, 2, '')
FROM (
SELECT DISTINCT Customer
FROM #t
) t
OUTER APPLY (
SELECT DISTINCT [a] = CASE WHEN id = 'a' THEN ', ' + val END
, [b] = CASE WHEN id = 'b' THEN ', ' + val END
FROM #t t2
CROSS APPLY (
VALUES ('a', t2.Product)
, ('b', t2.Method)
) t3 (id, val)
WHERE t2.Customer = t.Customer
FOR XML PATH(''), TYPE
) t2 (x)
Output:
Customer Product Method
---------- -------------------------- ------------------
John Computer, Mouse Cash, Credit
Todd Computer Credit
Will Computer, Mouse, Speaker Cash, Credit
Another idea with more performance benefits:
IF OBJECT_ID('tempdb.dbo.#EntityValues') IS NOT NULL
DROP TABLE #EntityValues
DECLARE #Values1 VARCHAR(MAX)
, #Values2 VARCHAR(MAX)
SELECT Customer
, Product
, Method
, RowNum = ROW_NUMBER() OVER (PARTITION BY Customer ORDER BY 1/0)
, Values1 = CAST(NULL AS VARCHAR(MAX))
, Values2 = CAST(NULL AS VARCHAR(MAX))
INTO #EntityValues
FROM #t
UPDATE #EntityValues
SET
#Values1 = Values1 =
CASE WHEN RowNum = 1
THEN Product
ELSE #Values1 + ', ' + Product
END
, #Values2 = Values2 =
CASE WHEN RowNum = 1
THEN Method
ELSE #Values2 + ', ' + Method
END
SELECT Customer
, Values1 = MAX(Values1)
, Values2 = MAX(Values2)
FROM #EntityValues
GROUP BY Customer
But with some limitations:
Customer Values1 Values2
------------- ----------------------------- ----------------------
John Computer, Mouse Credit, Cash
Todd Computer Credit
Will Computer, Mouse, Speaker Credit, Cash, Cash
Also check my old post about string aggregation:
http://www.codeproject.com/Articles/691102/String-Aggregation-in-the-World-of-SQL-Server
Another solution is the CLR method for group concatenation #aaron bertrand has done a performance comparison on this here.
If you can deploy CLR then download the script from https://orlando-colamatteo.github.io/ms-sql-server-group-concat-sqlclr/ which is free.
and all details are there in the documentation.
Your query will just change into like this
SELECT Customer,dbo.GROUP_CONCAT(product),dbo.GROUP_CONCAT(method)
FROM Purchases
GROUP BY Customer
This query is short, easy to remember and use, XML method also does the job but remembering the code is a bit difficult(atleast for me) and creeps in the problem like XML entitization which can be solved sure and some pitfalls also described in his blog.
Also from a performance view point using .query is time consuming I had the same issues with performance. I hope you can find this question I raised here in https://dba.stackexchange.com/questions/125771/multiple-column-concatenation
check the version 2 given by kenneth fisher a nested xml concatenation method or a unpivot /pivot method suggested by spaggettidba.
This is one of the use cases for recursive CTEs (Common Table Expressions). You can learn more here https://technet.microsoft.com/en-us/library/ms190766(v=sql.105).aspx
;
WITH CTE1 (PurchaseID, Customer, Product, Method, RowID)
AS
(
SELECT
PurchaseID, Customer, Product, Method,
ROW_NUMBER() OVER (PARTITION BY Customer ORDER BY Customer)
FROM
#tbl
/* This table holds source data. I ommited declaring and inserting
data into it because that's not important. */
)
, CTE2 (PurchaseID, Customer, Product, Method, RowID)
AS
(
SELECT
PurchaseID, Customer,
CONVERT(VARCHAR(MAX), Product),
CONVERT(VARCHAR(MAX), Method),
1
FROM
CTE1
WHERE
RowID = 1
UNION ALL
SELECT
CTE2.PurchaseID, CTE2.Customer,
CONVERT(VARCHAR(MAX), CTE2.Product + ',' + CTE1.Product),
CONVERT(VARCHAR(MAX), CTE2.Method + ',' + CTE1.Method),
CTE2.RowID + 1
FROM
CTE2 INNER JOIN CTE1
ON CTE2.Customer = CTE1.Customer
AND CTE2.RowID + 1 = CTE1.RowID
)
SELECT Customer, MAX(Product) AS Products, MAX(Method) AS Methods
FROM CTE2
GROUP BY Customer
Output:
Customer Products Methods
John Computer,Mouse Credit,Cash
Todd Computer Credit
Will Computer,Mouse,Speaker Credit,Cash,Cash

SQL SELECT with JOINS and Multiple Rows for one Column

The issue at hand is that I have a basic query (from a document software database), taking from multiple tables with some LEFT JOINS, but I have one column in question that needs to take from a table where there are multiple results per unique document id (DocGUID).
CURRENT QUERY
SELECT doc.[Doc #]
, '' AS 'Authors'
, ud.[Lead Author]
, doc.[Title]
, ud.[Publication]
, ud.[Citation]
, ud.[Year]
, ud.[Month]
, ud.[Comments]
, notes.[Note]
FROM [tblDocuments] doc
LEFT JOIN [tblNotes] notes ON notes.[DocGUID] = doc.[DocGUID]
LEFT JOIN [tblUserData] ud ON ud.[MasterGUID] = doc.[DocGUID]
WHERE doc.[DocGUID] = '12345678'
As you can see, I have simply queried '' for "Authors". Here's where my issue comes in. I have a table named tblMultiValues where there are two or more authors listed per DocGUID.
Table Example: (for tblMultiValues)
|------|-------------|-------------|-------------------|
| Id | DocGUID | FieldName | Value |
|------|-------------|-------------|-------------------|
| 123 | 12345678 | Authors | Collins, Nick |
| 456 | 12345678 | Authors | Williams, Robert |
| 321 | 87654321 | Authors | Smith, Kate |
| 654 | 87654321 | Authors | Hanks, Tom |
|------|-------------|-------------|-------------------|
So, what I want to show for the 2nd column of 'Authors', is the result of:
Collins, Nick; Williams, Robert
Specifically for DocGUID of '12345678'
How might one go about doing this, mixed in with the query that is already built?
(I hope this was enough info... if more is needed, please advise).
-Nick
:::EDIT:::
I was able to get things running with the following code... (very well guided from the answer given by #mohan111
SELECT DISTINCT
STUFF((
SELECT '; ' + mv2.Value
FROM [dbo].[tblMultiValues] mv2
WHERE mv1.DocGUID = mv2.DocGUID
FOR XML PATH ('')),1,2,'') AS 'Authors', mv1.FieldName, mv1.DocGUID
INTO #TempMultival
FROM [dbo].[tblMultiValues] mv1
SELECT doc.[Doc #]
, tmv.[Authors]
, ud.[Lead Author]
, doc.[Title]
, ud.[Publication]
, ud.[Citation]
, ud.[Year]
, ud.[Month]
, ud.[Comments]
, notes.[Note]
FROM [tblDocuments] doc
LEFT JOIN [tblNotes] notes ON notes.[DocGUID] = doc.[DocGUID]
LEFT JOIN [tblUserData] ud ON ud.[MasterGUID] = doc.[DocGUID]
LEFT JOIN #TempMultiVal tmv ON tmv.DoCGUID = doc.[DocGUID]
DROP TABLE #TempMultiVal
Declare #table TABLE
(
Id INT,
DocGUID int,
FieldName VARCHAR(25),
Value VARCHAR(200)
);
INSERT INTO #table
( Id,
DocGUID,
FieldName,
Value
)
VALUES
(123,12345678,'Authors','Collins, Nick'),
(456,12345678,'Authors','Williams, Robert'),
(321,87654321,'Authors','Smith, Kate'),
(654,87654321,'Authors','Hanks, Tom');
Select distinct DocGUID,
(SELECT
Substring((SELECT ', ' + CAST(i.id AS VARCHAR(1024))
FROM
#table i
WHERE i.DocGUID = tt.DocGUID
ORDER BY i.id
FOR XML PATH('')), 3, 10000000) AS list) AS ID,
FieldName,
STUFF((Select distinct t.Value + ','
from #table t
where t.DocGUID = tt.DocGUID
FOR XML PATH(''),TYPE).value('.', 'NVARCHAR(MAX)')
, 1, 0, ' ') from #table tt

Flattening 1-to-many relationship

My current schema looks like this:
PersonType (PersonTypeID, Name, Description)
Person (PersonID,PersonTypeID, FirstName, ... )
PersonDynamicField (PersonDynamicFieldID, PersonTypeID, Name, Description, DataType, DefaultValue, ...)
PersonDynamicFieldValue (PersonDynamicFieldValueID, PersonDynamicFieldID, PersonID, Value, ...)
That is, a person is of a certain type. For example, Customer. For each PersonType, there can dynamically be added additional fields to store about a PersonType. For a Customer, we might want to add fields to PersonDynamicField such as LikesChocolate, FavoriteColor, HappinessScale, etc. The value for those fields would then be stored in PersonDynamicFieldValue.
I hope my writing makes sense.
What I would like to do, is a query that can flatten this structure and return a result looking like this:
PersonID, PersonTypeID, FirstName, LikesChocolate, FavoriteColor, HappinessScale
1, 2, Robert, 1, Green, 9
2, 2, John, 0, Orange, 5
...
I'm kind of stuck and don't really know where to even start.
Can you help?
In order to get the result that you want there are several ways that you can convert the rows of data into columns.
Starting in SQL Server 2005, you can use the PIVOT function. The basic structure of the code will be:
SELECT personid, persontypeid, firstname,[FavoriteColor],[HappinessScale],[LikesChocolate]
from
(
select p.personid, p.persontypeid, p.firstname, f.name fields, v.value
from person p
inner join persontype pt
on p.persontypeid = pt.persontypeid
left join PersonDynamicField f
on p.PersonTypeID = f.PersonTypeID
left join PersonDynamicFieldValue v
on f.PersonDynamicFieldID = v.PersonDynamicFieldID
and p.personid = v.personid
) x
pivot
(
max(value)
for fields in ([FavoriteColor],[HappinessScale],[LikesChocolate])
) p;
See SQL Fiddle with Demo. The one issue that you are going to have with PIVOT is that it requires that the values being converted to columns are known at run-time. For your situation, this seems impossible since the values can change. As a result, you will have to use dynamic SQL to get the result:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(Name)
from PersonDynamicField
where PersonTypeID = 2
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT personid, persontypeid, firstname,' + #cols + '
from
(
select p.personid,
p.persontypeid,
p.firstname,
f.name fields,
v.value
from person p
inner join persontype pt
on p.persontypeid = pt.persontypeid
left join PersonDynamicField f
on p.PersonTypeID = f.PersonTypeID
left join PersonDynamicFieldValue v
on f.PersonDynamicFieldID = v.PersonDynamicFieldID
and p.personid = v.personid
) x
pivot
(
max(value)
for fields in (' + #cols + ')
) p '
execute(#query);
See SQL Fiddle with Demo. These will give a result:
| PERSONID | PERSONTYPEID | FIRSTNAME | FAVORITECOLOR | HAPPINESSSCALE | LIKESCHOCOLATE |
-----------------------------------------------------------------------------------------
| 1 | 2 | Robert | Green | 9 | 1 |
| 2 | 2 | John | Orange | 5 | 0 |
What you want is commonly called a pivot and SQL Server has an operation that may help. Take a look at this article on MSDN for examples: http://msdn.microsoft.com/en-us/library/ms177410(v=sql.105).aspx

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.