Concatenate multiple rows from multiple tables - sql-server

I've reviewed many other posts on here and have become pretty familiar with the Coalesce function, but I haven't been able to figure out how to do this specific task.
So, I have a Commissions table and a Categories table. I've created a gist here so you can see the exact data structure with some example data. Basically, the Commission table has a SalesRepID, LocationID, CategoryID, SurgeonID, and CommissionPercent column.
Using a Coalesce function, I've been able to get something like this by passing in the SalesRepID, LocationID, and SurgeonID:
.05 (Shirts), .05 (Shoes), .05 (Dresses), .10 (Hats), .15 (Pants)
However, I'm trying to get it to look like:
.05 (Shirts, Shoes, Dresses), .10 (Hats), .15 (Pants)
I did try it a few times with STUFF, but I never got the result that I'm looking for.
Which leads me to ask if this is even possible in MsSQL 2008 R2? If it is, any help in getting the result I'm looking for would be greatly appreciated.
Thank you very much for your time & energy,
Andrew

Thank you for the gist! So much better than pulling teeth to get schema and data. :-) If you plug this in to your gist query you should see the results you're after (well, very close - see below).
DECLARE #SalesRepID int = 2,
#SurgeonID int = 1,
#LocationID int = 1;
;WITH x AS
(
SELECT CommissionPercent, Categories = STUFF((SELECT N', '
+ tCat.Category FROM #tCategories AS tCat
INNER JOIN #tCommissions AS tCom
ON tCat.CategoryID = tCom.CategoryID
WHERE tCom.CommissionPercent = com.CommissionPercent
FOR XML PATH,
TYPE).value(N'./text()[1]', N'nvarchar(max)'), 1, 2, N'')
FROM #tCommissions AS com
WHERE SalesRepID = #SalesRepID
AND SurgeonID = #SurgeonID
AND LocationID = #LocationID
),
y AS
(
SELECT s = RTRIM(CommissionPercent) + N' (' + Categories + N')'
FROM x GROUP BY CommissionPercent, Categories
)
SELECT Result = STUFF((SELECT N', ' + s FROM y
FOR XML PATH,
TYPE).value(N'./text()[1]', N'nvarchar(max)'), 1, 2, N'');
The result is slightly different than you asked for, but you could fix that by applying string formatting when pulling CommissionPercent.
Result
--------------------------------------------------------
0.05 (Shirts, Shoes, Dresses), 0.10 (Hats), 0.15 (Pants)

I bumped into similar problem before - and the only way I could resolve this (without using cursors), is by creating a CLR aggregate function. Here's an example in C# (and in VB): http://technet.microsoft.com/en-us/library/ms131056(v=SQL.90).aspx
I believe it just does what you need: concatenation.
Combining your example and the CLR, to achieve what you want - the SQL would look like:
SELECT
c.CommissionPercent
, dbo.MyAgg(cat.Category)
FROM #tCommissions AS c
JOIN #tCategories AS cat ON c.CategoryID = cat.CategoryID
group by c.CommissionPercent

Related

How to merge multiple values from multiple columns into one row?

I have this table:
I need to convert it to (with parenthesis as well):
row_nbr - row_label - default_order
10 - TOTAL ACCOUNTABLE GROSS - (1, 3)
12 - DEDUCTIBLE TERMS - (3)
20 - TOTAL DEDUCTIBLE TERMS - (3)
34 - AMOUNT DUE (UNRECOUPED) - (4)
36 - ACCOUNTABLE GROSS - (2)
41 - TOTAL CONTINGENT COMPENSATION - (3)
I could have more than twice of the same row_nbr.
In this case the 10 is there twice, but I could have 3 10's, 4 12's, etc.
I kind of started the pivot table but honestly, even by looking at the Microsoft site, I cannot for the life of me figure this out.
select row_nbr, row_label, default_order
from #temp
pivot
(
max(row_nbr)
for default_order in (default_order)
) piv;
Anyone care to help?
Thanks.
As #Vinit says, you can use the string_agg function in 2017, but if you're at least on 2005 you can use a horrible, torturous XML generator:
SELECT row_nbr
,row_label
,default_order = '(' +
STUFF(
(SELECT ', ' + CAST(default_order AS VARCHAR(10))
FROM #temp
WHERE row_nbr = t.row_nbr
ORDER BY default_order
FOR XML PATH('') ,
ROOT('MyString'),
TYPE ).value('/MyString[1]', 'varchar(max)'), 1, 2, '')
+ ')'
FROM #temp t;
You can read more about it in this blog post
PIVOTS are definitely wonky to get a grip on. Fortunately, in this case, while you could use one as an intermediate step, it's not necessary. PIVOT will take each value and put it into a corresponding distinct column, and what you're wanting is a single column, with them all concatenated together. Like I said, you could do the pivot, then just concatenate all the generated columns together, but that's way more work than is needed.
On 2014, the easiest way to do this is using FOR XML. Russell Fox's answer pretty much covers how that technique works (although there are a few variants on how you can do that should you so choose).
If you know definitively the values are all integers, you can save a bit of typing and omit the type and value operators, as those are only necessary when you have to escape certain XML characters in string fields
select
row_nbr,
row_label,
default_order,
stuff
(
(
select concat(',', default_order)
from #temp i
where i.row_nbr = o.row_nbr
for xml path('')
), 1, 1, ''
)
from #temp o

SUMIF greater than and Workday column

So I'm trying to convert an Excel table into SQL and I'm having difficulty coming up with the last 2 columns. Below, find my Excel table that is fully functional (in green) and a table for the code that I have in SQL so far (in yellow). I need help replicating columns C and D, I pasted the Excel formula I'm using so you can understand what I'm trying to do:
Here's the code that I have so far:
WITH
cte_DistinctScheduling AS (
SELECT DISTINCT
s.JobNo
FROM
dbo.Scheduling s
WHERE
s.WorkCntr = 'Framing')
SELECT
o.OrderNo,
o.Priority AS [P],
SUM(r.TotEstHrs)/ROUND((8*w.CapacityFactor*(w.UtilizationPct/100)),2) AS
[Work Days Left],
Cast(GetDate()+ROUND(SUM(r.TotEstHrs)/ROUND((8*w.CapacityFactor*
(w.UtilizationPct/100)),2),3) AS DATE) AS DueDate
FROM OrderDet o JOIN cte_DistinctScheduling ds ON o.JobNo = ds.JobNo
JOIN OrderRouting r ON o.JobNo = r.JobNo
JOIN WorkCntr w ON r.WorkCntr = w.ShortName
WHERE r.WorkCntr = 'Framing'
AND o.OrderNo NOT IN ('44444', '77777')
GROUP BY o.OrderNo, o.Priority, ROUND((8*w.CapacityFactor*
(w.UtilizationPct/100)),2)
ORDER BY o.Priority DESC;
My work days left column in SQL gets the right amount for that particular row, but I need it to sum itself and everything with a P value above it and then add that to today's date, while taking workdays into account. I don't see a Workday function in SQL from what I've been reading, so I'm wondering what are some creative solutions? Could perhaps a CASE statement be the answer to both of my questions? Thanks in advance
Took me a while to understand how is the Excel helpful, and I'm still having a hard time absorbing the rest, can't tell if it's a me thing or a you thing, in any case...
First, I've mocked up something to test SUM per your rationale, the idea is doing a self-JOIN and summing everything from that JOIN side, relying on the fact that NULLs will come up for anything that shouldn't be summed:
DECLARE #TABLE TABLE(P int, [Value] int)
INSERT INTO #TABLE SELECT 1, 5
INSERT INTO #TABLE SELECT 2, 6
INSERT INTO #TABLE SELECT 3, 2
INSERT INTO #TABLE SELECT 4, 4
INSERT INTO #TABLE SELECT 5, 9
SELECT T1.P, [SUM] = SUM(ISNULL(T2.[Value], 0))
FROM #TABLE AS T1
LEFT JOIN #TABLE AS T2 ON T2.P <= T1.P
GROUP BY T1.P
ORDER BY P DESC
Second, workdays is a topic that comes up regularly. In case you didn't, consider reading a little about it from previous questions, I even posted an answer on one question last week, and the thread as a whole had several references.
Thirdly, we could use table definitions and sample data loaded on SQL itself, something like I did above.
Lastly, could you please check result of UtilizationPct / 100? If that's an integer-like data type, you're probably getting a bad result on it.

Insert Calculated Row into Temp Table with Variable Number of Rows and Columns SQL Server

I have a query that returns only locations that have made a purchase within the time frame that the user specifies, so the number of rows is variable. Then, the query finds all the types of spending those locations have done, and pivots it to create a dynamic number of columns.
So what I am left with is a table that has all the locations with spending and a sum of the spending in the categories in which they spent.
What I would like to do now is to insert a new row at the top that is an average across all locations in the table for each of the columns, but I'm not sure how to do that across a variable number of rows and columns.
Do any of you know how to do this? I have not posted any code because it's about 90 lines, but if that would help, I can.
Thanks in advance!
EDIT: Here is my code. I removed as much extraneous code as possible (and anonymized table names) so that you can see succinctly what I have so far.
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#RunForText varchar(max),
#Periods varchar(max)
Set #RunForText =
(
SELECT Distinct Stuff((Select ',' + c.Location
From Table..Table1 c For Xml Path ('')), 1, 1, '')) --GET UNIQUE LOCATIONS
Set #Periods = '201703' --SET TIME PERIOD FOR REPORT
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(vw.Description) --GET UNIQUE CATEGORIES OF SPEND AS COLUMNS
FROM Table..Table vw
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)') ,1,1,'')
set #query = 'SELECT p.Number, ' + #cols + '
into ##tmp
from
(
SELECT
vw.Number,
vw.Name,
[Total] = vw.Total,
Description
FROM Table..Table vw
) p
pivot
(
SUM(p.Total)
for p.Description in (' + #cols + ')
) p '
execute sp_executesql #query;
SELECT * FROM ##tmp
Currently, the Output looks like this:
NUMBER | Category1 | Category2 | Category3
01 100.00 125.00 15.00
02 1.41 23.42 14.89
I would like to insert a row so that the table looks like this:
NUMBER | Category1 | Category2 | Category3
Average 50.70 74.21 14.94
01 100.00 125.00 15.00
02 1.40 23.42 14.88
You could use a similar method to the way you are creating the pivot query, but surround the column names with a call to Avg. Based on the code you posted, it would look like:
declare #avgcols AS NVARCHAR(MAX)
select #avgcols =
STUFF((SELECT distinct ',avg(' + QUOTENAME(vw.Description) + ')'
FROM Table..Table vw
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)') ,1,1,'')
set #query =
'INSERT into ##tmp (Number,' + #cols + ')
into ##tmp
Select ''Average'',' + #avgcols + ' FROM ##tmp'
execute sp_executesql #query;
The big road block for your current approach is the pivot operator. It only supports one aggregation function at a time (source - Microsoft Docs). That means you cannot combine sums, mins, maxs and averages.
There is a reason for this. Each column has a purpose. But in your example, Col1 has two jobs. Sometimes it contains a sum, sometimes an average. That's an anti-pattern, that I like to avoid. You cannot build on top of columns, that have inconsistent contents.
Of course in your case you are just trying to add a total/sub total row. Not exactly a crime! Still where possible this kind of operation is best performed in the presentation layer. One good reason, often it is much easier!
My example uses conditional aggregation (mixing the group by clause with a number of case expressions), instead of the pivot. But the result is the same.
At a high level, the basic approach is:
Get your data.
Sum it.
Append averages.
Sort as required.
Query
/* Returns sample data with average sub total row.
*/
WITH SampleData AS
(
-- Conditional aggregation used instead of Pivot.
SELECT
Nu,
SUM(CASE WHEN Cat = 'C1' THEN Tot ELSE 0 END) AS C1,
SUM(CASE WHEN Cat = 'C2' THEN Tot ELSE 0 END) AS C2,
SUM(CASE WHEN Cat = 'C3' THEN Tot ELSE 0 END) AS C3
FROM
(
VALUES
(1, 'C1', 100.00),
(2, 'C1', 1.41),
(1, 'C2', 125.00),
(2, 'C2', 23.42),
(1, 'C3', 15.00),
(2, 'C3', 14.89)
) AS c(Nu, Cat, Tot)
GROUP BY
nu
)
-- Returns detail rows.
SELECT
*
FROM
SampleData
UNION ALL
-- Returns average row.
SELECT
'0' AS Nu,
AVG(C1) AS C1,
AVG(C2) AS C2,
AVG(C3) AS C3
FROM
SampleData
ORDER BY
Nu
;
Returns
Nu C1 C2 C3
0 50.71 74.21 14.95
1 100.00 125.00 15.00
2 1.41 23.42 14.89
You could also achieve the same results with a temp table and a couple of insert statements.
As you can see. This is one ugly query, There is a lot going on. Converting this into dynamic sql won't be fun. This is why I would recommend you don't use it.

how to select data row from a comma separated value field

My question is not exactly but similar to this question
How to SELECT parts from a comma-separated field with a LIKE statement
but i have not seen any answer there. So I am posting my question again.
i have the following table
╔════════════╦═════════════╗
║ VacancyId ║ Media ║
╠════════════╬═════════════╣
║ 1 ║ 32,26,30 ║
║ 2 ║ 31, 25,20 ║
║ 3 ║ 21,32,23 ║
╚════════════╩═════════════╝
I want to select data who has media id=30 or media=21 or media= 40
So in this case the output will return the 1st and the third row.
How can I do that ?
I have tried media like '30' but that does not return any value. Plus i just dont need to search for one string in that field .
My database is SQL Server
Thank you
It's never good to use the comma separated values to store in database if it is feasible try to make separate tables to store them as most probably this is 1:n relationship.
If this is not feasible then there are following possible ways you can do this,
If your number of values to match are going to stay same, then you might want to do the series of Like statement along with OR/AND depending on your requirement.
Ex.-
WHERE
Media LIKE '%21%'
OR Media LIKE '%30%'
OR Media LIKE '%40%'
However above query will likely to catch all the values which contains 21 so even if columns with values like 1210,210 will also be returned. To overcome this you can do following trick which is hamper the performance as it uses functions in where clause and that goes against making Seargable queries.
But here it goes,
--Declare valueSearch variable first to value to match for you can do this for multiple values using multiple variables.
Declare #valueSearch = '21'
-- Then do the matching in where clause
WHERE
(',' + RTRIM(Media) + ',') LIKE '%,' + #valueSearch + ',%'
If the number of values to match are going to change then you might want to look into FullText Index and you should thinking about the same.
And if you decide to go with this after Fulltext Index you can do as below to get what you want,
Ex.-
WHERE
CONTAINS(Media, '"21" OR "30" OR "40"')
The best possible way i can suggest is first you have do comma separated value to table using This link and you will end up with table looks like below.
SELECT * FROM Table
WHERE Media in('30','28')
It will surely works.
You can use this, but the performance is inevitably poor. You should, as others have said, normalise this structure.
WHERE
',' + media + ',' LIKE '%,21,%'
OR ',' + media + ',' LIKE '%,30,%'
Etc, etc...
If you are certain that any Media value containing the string 30 will be one you wish to return, you just need to include wildcards in your LIKE statement:
SELECT *
FROM Table
WHERE Media LIKE '%30%'
Bear in mind though that this would also return a record with a Media value of 298,300,302 for example, so if this is problematic for you, you'll need to consider a more sophisticated method, like:
SELECT *
FROM Table
WHERE Media LIKE '%,30,%'
OR Media LIKE '30,%'
OR Media LIKE '%,30'
OR Media = '30'
If there might be spaces in the strings (as per in your question), you'll also want to strip these out:
SELECT *
FROM Table
WHERE REPLACE(Media,' ','') LIKE '%,30,%'
OR REPLACE(Media,' ','') LIKE '30,%'
OR REPLACE(Media,' ','') LIKE '%,30'
OR REPLACE(Media,' ','') = '30'
Edit: I actually prefer Coder of Code's solution to this:
SELECT *
FROM Table
WHERE ',' + LTRIM(RTRIM(REPLACE(Media,' ',''))) + ',' LIKE '%,30,%'
You mention that would wish to search for multiple strings in this field, which is also possible:
SELECT *
FROM Table
WHERE Media LIKE '%30%'
OR Media LIKE '%28%'
SELECT *
FROM Table
WHERE Media LIKE '%30%'
AND Media LIKE '%28%'
I agree not a good idea comma seperated values stored like that. Bu if you have to;
I think using inline function is will give better performance;
Select VacancyId, Media from (
Select 1 as VacancyId, '32,26,30' as Media
union all
Select 2, '31,25,20'
union all
Select 3, '21,32,23'
) asa
CROSS APPLY dbo.udf_StrToTable(Media, ',') tbl
where CAST(tbl.Result as int) in (30,21,40)
Group by VacancyId, Media
Output is;
VacancyId Media
----------- ---------
1 32,26,30
3 21,32,23
and our inline function script is;
if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[udf_StrToTable]') and xtype in (N'FN', N'IF', N'TF'))
drop function [dbo].udf_StrToTable
GO
CREATE FUNCTION udf_StrToTable (#List NVARCHAR(MAX), #Delimiter NVARCHAR(1))
RETURNS TABLE
With Encryption
AS
RETURN
( WITH Split(stpos,endpos)
AS(
SELECT 0 AS stpos, CHARINDEX(#Delimiter,#List) AS endpos
UNION ALL
SELECT CAST(endpos+1 as int), CHARINDEX(#Delimiter,#List,endpos+1)
FROM Split
WHERE endpos > 0
)
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) as inx,
SUBSTRING(#List,stpos,COALESCE(NULLIF(endpos,0),LEN(#List)+1)-stpos) Result
FROM Split
)
GO
This solution uses a RECURSIVE CTE to identify the position of each comma within the string then uses SUBSTRING to return all strings between the commas.
I've left some unnecessary code in place to help you get you head round what it's doing. You can strip it down to provide exactly what you need.
DROP TABLE #TMP
CREATE TABLE #TMP(ID INT, Vals CHAR(100))
INSERT INTO #TMP(ID,VALS)
VALUES
(1,'32,26,30')
,(2,'31, 25,20')
,(3,'21,32,23')
;WITH cte
AS
(
SELECT
ID
,VALS
,0 POS
,CHARINDEX(',',VALS,0) REM
FROM
#TMP
UNION ALL
SELECT ID,VALS,REM,CHARINDEX(',',VALS,REM+1)
FROM
cte c
WHERE CHARINDEX(',',VALS,REM+1) > 0
UNION ALL
SELECT ID,VALS,REM,LEN(VALS)
FROM
cte c
WHERE POS+1 < LEN(VALS) AND CHARINDEX(',',VALS,REM+1) = 0
)
,cte_Clean
AS
(
SELECT ID,CAST(REPLACE(LTRIM(RTRIM(SUBSTRING(VALS,POS+1,REM-POS))),',','') AS INT) AS VAL FROM cte
WHERE POS <> REM
)
SELECT
ID
FROM
cte_Clean
WHERE
VAL = 32
ORDER BY ID

Return Multi Row DataSet as Single Row CSV without Temp Table

I'm doing some reporting against a silly database and I have to do
SELECT [DESC] as 'Description'
FROM dbo.tbl_custom_code_10 a
INNER JOIN dbo.Respondent b ON CHARINDEX(',' + a.code + ',', ',' + b.CC10) > 0
WHERE recordid = 116
Which Returns Multiple Rows
Palm
Compaq
Blackberry
Edit *
Schema is
Respondent Table (At a Glance) ...
*recordid lname fname address CC10 CC11 CC12 CC13*
116 Smith John Street 1,4,5, 1,3,4, 1,2,3, NULL
Tbl_Custom_Code10
*code desc*
0 None
1 Palm
10 Samsung
11 Treo
12 HTC
13 Nokia
14 LG
15 HP
16 Dash
Result set will always be 1 row, so John Smith: | 646-465-4566 | Has a Blackberry, Palm, Compaq | Likes: Walks on the beach, Rainbows, Saxophone
However I need to be able to use this within another query ... like
Select b.Name, c.Number, d.MulitLineCrap FROM Tables
How can I go about this, Thanks in advance ...
BTW I could also do it in LINQ if any body had any ideas ...
Here is one way to make a comma-separated list based on a query (just replace the query inside the first WITH block). Now, how that joins up with your query against b and c, I have no idea. You'll need to supply a more complete question - including specifics on how many rows come back from the second query and whether "MultilineCrap" is the same for each of those rows or if it depends on data in b/c.
;WITH x([DESC]) AS
(
SELECT d FROM (VALUES('Palm'),('Compaq'),('Blackberry')) AS x(d)
)
SELECT STUFF((SELECT ',' + [DESC]
FROM x
FOR XML PATH(''), TYPE).value(N'./text()[1]', N'varchar(max)'),1,1,'');
EDIT
Given the new requirements, perhaps this is the best way:
CREATE FUNCTION dbo.GetMultiLineCrap
(
#s VARCHAR(MAX)
)
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE #x VARCHAR(MAX) = '';
SELECT #x += ',' + [desc]
FROM dbo.tbl_custom_code_10
WHERE ',' + #s LIKE '%,' + RTRIM(code) + ',%';
RETURN (SELECT STUFF(#x, 1, 1, ''));
END
GO
SELECT r.LName, r.FName, MultilineCrap = dbo.GetMultiLineCrap(r.CC10)
FROM dbo.Respondent AS r
WHERE recordid = 116;
Please use aliases that make a little bit of sense, instead of just serially applying a, b, ,c, etc. Your queries will be easier to read, I promise.

Resources