Assign Values of a column in SubQuery in SQL - sql

I am trying to do following in SQL Server:
SELECT
PRODUCER_NAME, PRODUCER_ID,
(SELECT #X = #X + PRODUCT_NAME
FROM PRODUCT
WHERE PRODUCER_ID = PRODUCER.ID)
FROM
PRODUCER
There are two tables. Producer table is list of all producers. Product table stores product produced by producers. #x is varchar variable
Basically I want a list of all products, comma-separated by producer.
For example
Producer Products
-------- --------------------------
P1 ProductA,ProductB,ProductC
P2 ProductD,ProductE
I don't know if this is possible this way. Do anyone know how to do this without joining tables?

I don't have a way for you to assign multiple output comma-separated lists to a single varchar variable, but maybe you don't actually need that anyway. Try this:
SELECT Producer = PRODUCER.PRODUCER_NAME,
Products = STUFF(
(
SELECT N',' + PRODUCT.PRODUCT_NAME
FROM dbo.PRODUCT
WHERE PRODUCT.PRODUCER_ID = PRODUCER.ID
FOR XML PATH(''),
TYPE).value(N'./text()[1]', N'nvarchar(max)'),1,1,N'')
FROM dbo.PRODUCER;
On a large table, this kind of correlated subquery can be quite expensive. On SQL Server 2017+ we can use STRING_AGG() in a single pass:
SELECT Producer = PRODUCER.PRODUCER_NAME,
Products = STRING_AGG(PRODUCT.PRODUCT_NAME, N',')
FROM dbo.PRODUCT
INNER JOIN dbo.PRODUCER
ON PRODUCT.PRODUCER_ID = PRODUCER.ID
GROUP BY PRODUCER.PRODUCER_NAME;
Example db<>fiddle

If you want to concatenate names, you can do this:
select
P.PRODUCER_NAME, P.PRODUCER_ID,
stuff(
(
select ',' + T.PRODUCT_NAME
from PRODUCT as T
where T.PRODUCER_ID = P.PRODUCER_ID
for xml path(''), type
).value('.', 'nvarchar(max)')
, 1, 1, '') as PRODUCT_NAMES
from PRODUCER as P
Two notes:
Always use the table aliases for such a queries. To see why - delete alias name from query and drop PRODuCER_ID from PRODUCT table.
Use value method instead of implicit conversion to nvarchar to correctly work with names like 'Product & 1'.

Related

Update a column in Table B with multiple row values from Table A using XML PATH

I have 4 columns in Table A viz., Inv_Num1, Inv_Date1, Inv_Amt1, Inv_DocNum1
I have 4 columns in Table B viz., Inv_Num2, Inv_Date2, Inv_Amt2, Inv_Status2
I would like to match the rows between Table A and Table B by using an inner join where condition on is
Invoice_Num1=Invoice_Num2 AND Invoice_Date1=Invoice_Date2 AND
Invoice_Amt1=Invoice_Amt2
When I do this matching I may get more than 1 row as a result in Table
A (Invoice_DocNum1 column)
I tried XML Path code but I dont know how to implement in Update statement
update cis2
set cis2.Inv_Status2 =
(SELECT
TypeName = STUFF((
SELECT '; ' + imd1.Inv_DocNum1
FROM [VRS].[Table_B] cis1
INNER JOIN [Table_A] imd1
ON cis1.Inv_Num1 = imd1.Inv_Num2
WHERE cis1.Inv_Num1 = imd1.Inv_Num2
AND cis1.Inv_Date1 = imd1.Inv_Date2
AND cis1.Inv_Amt1 = imd1.Inv_Amt2
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
) FROM Table_B cis2
Doing this to your database is against good practices since it violates 1NF. But you could still this if you are deadset on doing it. Something along these lines should work.
with myCte as
(
SELECT Inv_Num1
, TypeName = STUFF((
SELECT '; ' + imd1.Inv_DocNum1
FROM [VRS].[Table_B] cis1
INNER JOIN [Table_A] imd1
ON cis1.Inv_Num1 = imd1.Inv_Num2
WHERE cis1.Inv_Num1 = imd1.Inv_Num2
AND cis1.Inv_Date1 = imd1.Inv_Date2
AND cis1.Inv_Amt1 = imd1.Inv_Amt2
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
from Table_A
group by Inv_Num1
)
update tb
set Inv_Status2 = c.TypeName
from Table_B tb
join myCte c on c.Inv_Num1 = tb.Inv_Num2
The answer has two parts. First, you need to produce your comma-separated list per row. The best way to do it is STRING_AGG (https://learn.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql?view=sql-server-2017)
You will need to use it with the group by, like select ..., STRING_AGG(Inv_DocNum1, ',') group by ... where ... stands for your three fields forming unique key.
Second, you need to use update ... from syntax, see https://learn.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql?view=sql-server-2017#l-specifying-a-table-alias-as-the-target-object. In your case, it will be from your target table joining the resultset you computed at step one.

Looking for a way to replace table name in query with Declaring Variable

Would like to know if its possible to replace one of the table name DCI_ARD by declaring a variable in head for the following query.
Have tried multiple ways, but the issue I am encountering is that I have multiple ' within the same query and it breaks off the declaration.
ie. CONCAT('(',M.ManufacturerID,')',
ie. STRING_AGG(CategoryID, ',') as CategoryID
SELECT * FROM (
SELECT [id]
,[brandaaiaid]
,M.ManufacturerID
,CONCAT('(',M.ManufacturerID,')',exppartno) AS TempManufacturerSKU
,[company]
,[fromyear]
,[toyear]
,[makename]
,[modelname]
,[submodelname]
,[engbase]
,[partterminologyname]
,[position]
,[exppartno]
,[mfrlabel]
FROM DCI_ARD AS DS
LEFT JOIN [dbo].[Manufacturer] AS M ON DS.brandaaiaid = M.AAIAID
) AS T
LEFT JOIN [dbo].[ProductVariant] AS PV ON T.TempManufacturerSKU = PV.ManufacturerPartNumber
LEFT JOIN
(select ProductId, STRING_AGG(CategoryID, ',') as CategoryID
from productCategory
GROUP BY ProductId) as PC ON PV.ProductID = PC.ProductID ORDER BY id ASC
Have tried multiple ways, but the issue I am encountering is that I have multiple ' within the same query and it breaks off the declaration.
Based on this I'm assuming that you're trying to build up a string and use sp_executesql, in which case any single quotes that appear in the SQL will need to be escaped by doubling them up. For example, if you wanted to run this:
SELECT CONCAT('(','Test',')') FROM MyTable
Using sp_executesql:
DECLARE #SQLString nvarchar(max) = 'SELECT CONCAT(''('',''Test'','')'') FROM MyTable'
EXEC sp_executesql #SQLString

SQL: Filter XML retrieving using xpath

I have documents table, each row has a XML column Document:
<Document>
<Good GroupId="..."/>
<Good GroupId="..."/>
...
</Document>
I also have temp table with subset of GroupId values:
DECLARE #Groups TABLE (groupId VARCHAR(MAX));
Next, I wrote a select query to documents table, goal - retrieve XML from Document:
SELECT
(SELECT CAST(Document.data as XML)).query('/Document/Good') AS Goods
FROM
Documents as Document
JOIN
#Numbers n ON n.number = Document.number
WHERE
Document.type = #type
--FOR XML AUTO, ROOT('Documents')
As a result, in column Goods I got all Good items for each Document
Task:
In step 3, I need filter Good elements by GroupId attribute (using #Groups) - I need all Good for which GroupId value is not contained in #Groups
Thanks!
Do CROSS APPLY to extract the attribute values from the XML
Need to use LEFT JOIN to find Goods in XML but not in #Goods table.
Here is the SQL Fiddle: http://www.sqlfiddle.com/#!3/a6b9e/5
SELECT G.value('#GroupId', 'varchar(max)') FROM
(
SELECT
CAST(Document.data as XML) AS Goods
FROM
Documents as Document
WHERE type = 1
) T
CROSS APPLY T.Goods.nodes('Document/Good') D(G)
LEFT JOIN #Groups GS
ON G.value('#GroupId', 'varchar(max)') = GS.groupId
WHERE GS.groupId IS NULL

SQL Server dynamically change WHERE clause in a SELECT based on returned data

I'm mainly a presentation/logic tier developer and don't mess around with SQL all that much but I have a problem and am wondering if it's impossible within SQL as it's not a full programming language.
I have a field ContactID which has an CompanyID attached to it
In another table, the CompanyID is attached to CompanyName
I am trying to create a SELECT statement that returns ONE CONTACT ID and in a seperate column, an aggregate of all the Companies attached to this contact (by name).
E.G
ContactID - CompanyID - CompanyName
***********************************
1 001 Lol
1 002 Haha
1 003 Funny
2 002 Haha
2 004 Lmao
I want to return
ContactID - Companies
*********************
1 Lol, Haha, Funny
2 Haha, Lmao
I have found the logic to do so with ONE ContactID at a time:
SELECT x.ContactID, substring(
(
SELECT ', '+y.CompanyName AS [text()]
FROM TblContactCompany x INNER JOIN TblCompany y ON x.CompanyID = y.CompanyID WHERE x.ContactID = 13963
For XML PATH (''), root('MyString'), type
).value('/MyString[1]','varchar(max)')
, 3, 1000)
[OrgNames] from TblContact x WHERE x.ContactID = 13963
As you can see here, I am hardcoding in the ContactID 13963, which is neccessary to only return the companies this individual is linked to.
The issue is when I want to return this aggregate information PER ROW on a much bigger scale SELECT (on a whole table full of ContactID's).
I want to have x.ContactID = (this.ContactID) but I can't figure out how!
Failing this, could I run one statement to return a list of ContactID's, then in the same StoredProc run another statement that LOOPS through this list of ContactID's (essentially performing the second statement x times where x = no. of ContactID's)?
Any help greatly appreciated.
You want a correlated subquery:
SELECT ct.ContactID,
stuff((SELECT ', ' + co.CompanyName AS [text()]
FROM TblContactCompany cc INNER JOIN
TblCompany co
ON cc.CompanyID = co.CompanyID
WHERE cc.ContactID = ct.ContactId
For XML PATH (''), root('MyString'), type
).value('/MyString[1]', 'varchar(max)'),
1, 2, '')
[OrgNames]
from TblContact ct;
Note the where clause on the inner subquery.
I also made two other changes:
I changed the table aliases to better represent the table names. This makes queries easier to understand. (Plus, the aliases had to be changed because you were using x in the outer query and the inner query.)
I replaced the substring() with stuff(), which does exactly what you want.
You could use a table variable to store the required x.ContactID and in your main query in the WHERE clause use IN clause like below
WHERE
...
x.ContactID IN (SELECT ContactID FROM #YourTableVariable)
I guess all you need to do is to use unique table identifiers in your subquery and join the table in subquery with outer table x:
SELECT x.ContactID, substring(
(
SELECT ', '+z.CompanyName AS [text()]
FROM TblContactCompany y, TblCompany z WHERE y.CompanyID = z.CompanyID AND y.ContactId = x.ContactId
For XML PATH (''), root('MyString'), type
).value('/MyString[1]','varchar(max)')
, 3, 1000)
[OrgNames] from TblContact x
Don't loop or you will get performance problems (row by agonising row RBAR). Instead do set based queries.
This is untested but should give you an idea of how it may work:
SELECT
x.ContactID,
substring(
(SELECT ', '+y.CompanyName AS [text()]
FROM TblContactCompany y
WHERE x.CompanyID = y.CompanyID
For XML PATH (''), root('MyString'), type).value('/MyString[1]','varchar(max)')
, 3, 1000)
[OrgNames]
FROM TblContact x
And I have a feeling you can use CONCAT instead of substring

SQL: Nested SELECT with multiple values in a single field

In my SQL 2005 DB, I have a table with values stored as IDs with relationships to other tables. So in my MyDBO.warranty table, I'm storing product_id instead of product_name in order to save space. The product_name is stored in MyDBO.products.
When the marketing department pulls the demographic information, the query selects the corresponding name for each ID from related tables (trimmed down for brevity):
SELECT w1.warranty_id AS "No.",
w1.created AS "Register Date"
w1.full_name AS "Name",
w1.purchase_date AS "Purchased",
(
SELECT p1.product_name
FROM WarrDBO.products p1 WITH(NOLOCK)
WHERE p1.product_id = i1.product_id
) AS "Product Purchased",
i1.accessories
FROM WarrDBO.warranty w1
LEFT OUTER JOIN WarrDBO.warranty_info i1
ON i1.warranty_id = w1.warranty_id
ORDER BY w1.warranty_id ASC
Now, my problem is that the "accessories" column on the warranty_info table stores several values:
No. Register Date Name Purchased Accessories
---------------------------------------------------------------------
1500 1/1/2008 Smith, John Some Product 5,7,9
1501 1/1/2008 Hancock, John Another 2,3
1502 1/1/2008 Brown, James And Another 2,9
I need to do something similar with "Accessories" that I did with "Product" and pull accessory_name from the MyDBO.accessories table using accessory_id. I'm not sure where to start, because first I'd need to extract the IDs and then somehow concatenate multiple values into a string. So each line would have "accessoryname1,accessoryname2,accessoryname3":
No. Register Date Name Purchased Accessories
---------------------------------------------------------------------
1500 1/1/2008 Smith, John Some Product Case,Bag,Padding
1501 1/1/2008 Hancock, John Another Wrap,Label
1502 1/1/2008 Brown, James And Another Wrap,Padding
How do I do this?
EDIT>> Posting my final code:
I created this function:
CREATE FUNCTION SQL_GTOInc.Split
(
#delimited varchar(50),
#delimiter varchar(1)
) RETURNS #t TABLE
(
-- Id column can be commented out, not required for sql splitting string
id INT identity(1,1), -- I use this column for numbering splitted parts
val INT
)
AS
BEGIN
declare #xml xml
set #xml = N'<root><r>' + replace(#delimited,#delimiter,'</r><r>') + '</r></root>'
insert into #t(val)
select
r.value('.','varchar(5)') as item
from #xml.nodes('//root/r') as records(r)
RETURN
END
And updated my code accordingly:
SELECT w1.warranty_id,
i1.accessories,
(
CASE
WHEN i1.accessories <> '' AND i1.accessories <> 'NULL' AND LEN(i1.accessories) > 0 THEN
STUFF(
(
SELECT ', ' + a1.accessory
FROM MyDBO.accessories a1
INNER JOIN MyDBO.Split(i1.accessories, ',') a2
ON a1.accessory_id = a2.val
FOR XML PATH('')
), 1, 1, ''
)
ELSE ''
END
) AS "Accessories"
FROM MyDBO.warranty w1
LEFT OUTER JOIN MyDBO.warranty_info i1
ON i1.warranty_id = w1.warranty_id
You could write a table valued function that simply splits comma separated string into XML and turns XML nodes to rows.
See:
http://www.kodyaz.com/articles//t-sql-convert-split-delimeted-string-as-rows-using-xml.aspx
Join to accessories through the result of function call, and stuff the result back to comma separated list of names.
Untested code:
SELECT w1.warranty_id AS "No.",
w1.created AS "Register Date"
w1.full_name AS "Name",
w1.purchase_date AS "Purchased",
(
SELECT p1.product_name
FROM WarrDBO.products p1 WITH(NOLOCK)
WHERE p1.product_id = i1.product_id
) AS "Product Purchased",
STUFF(
(
SELECT
', ' + a.name
FROM [table-valued-function](i1.accessories) acc_list
INNER JOIN accessories a ON acc_list.id = a.id
FOR XML PATH('')
), 1, 1, ''
) AS [accessories]
FROM WarrDBO.warranty w1
LEFT OUTER JOIN WarrDBO.warranty_info i1
ON i1.warranty_id = w1.warranty_id
ORDER BY w1.warranty_id ASC
Nothing to do with your question. Just a note that your original query can also be written, moving the subqery to a join, as:
SELECT w1.warranty_id AS "No.",
w1.created AS "Register Date"
w1.full_name AS "Name",
w1.purchase_date AS "Purchased",
p1.product_name AS "Product Purchased",
i1.accessories
FROM WarrDBO.warranty w1
INNER JOIN WarrDBO.products p1
ON p1.product_id = i1.product_id
LEFT OUTER JOIN WarrDBO.warranty_info i1
ON i1.warranty_id = w1.warranty_id
ORDER BY w1.warranty_id ASC
You just need to use the FOR XML feature of SQL Server to easily cat strings:
Example from the linked blog post:
SELECT
STUFF(
(
SELECT
' ' + Description
FROM dbo.Brands
FOR XML PATH('')
), 1, 1, ''
) As concatenated_string
To parse a field that has already been stored as comma delimited you will have to write a UDF that parses the field and returns a table which can then be used with an IN predicate in your WHERE clause. Look here for starters, and here.
It seem to be a work for a concatenate aggregate function.
In SQL it can be deployed using CLR
http://www.mssqltips.com/tip.asp?tip=2022