SQL Server: Select Varying Number Of Values Based On Column Value - sql-server

Version: Microsoft SQL Server 2014 - 12.0.2000.8 (X64) Feb 20 2014 20:04:26 Copyright (c) Microsoft Corporation Express Edition (64-bit) on Windows NT 6.1 (Build 7601: Service Pack 1)
I need to select a varying number of values from a table where a certain column is equal to a parameter and a certain column is LIKE 'String1' or 'String2'.
I have created a stored procedure that is returning the MAX and MIN strings, but naturally this method is not dynamic.
I have tried the following query which says it completes successfully, but does not return any results.
SELECT UPC, PartNum, PartDesc
FROM dbo.table
WHERE UPC = #upc
GROUP BY UPC, PartNum, PartDesc
HAVING PartDesc in ('%RED%','%BLUE%')
ORDER BY PartDesc;
Example table:
ID UPC PartNum PartDesc
-------------------------------------------
1 123 543 Red1
2 123 345 Blue1
3 123 654 Red2
4 123 765 Blue2
I need to pass a parameter to a stored procedure as #upc from an application.
Where it will find any PartDesc that are like '%RED%' or '%BLUE%' AND where the UPC = #upc.
Then store the Part#(s) found in a new table to be queried later.
Created Table From Stored Procedure:
ID UPC Red1 Red2 Blue1 Blue2
----------------------------------------------------------
1 123 543 654 345 765
There can be any number or combination of "Red" or "Blue" per UPC number. i.e.,
Some UPC numbers may only have two "Red" parts and one "Blue" part and others may only have two "Red" parts and no "Blue" parts. Maybe five "Red" parts and ten "Blue" parts.
How do would I write the query that will store the varying number of found results to a new table in a stored procedure?
Edit
It seems as though the PIVOT function should be used, but I am unsure of how to use the required aggregate in my scenario. I don't need to pivot on the "SUM" of PartDesc or any other column for that matter.
Perhaps a dynamic Pivot?
EDIT Based on Corgi's recommendation. Also, showing my work.
DECLARE #upc As varchar(13)
DECLARE #Red1 As nvarchar(100) = CASE
WHEN
(
SELECT MIN(PartNum) FROM dbo.table
WHERE PartDesc LIKE '%RED%' AND UPC = #upc
) IS NOT NULL THEN
(
SELECT MIN(PartNum) FROM dbo.table
WHERE PartDesc LIKE '%RED%' AND UPC = #upc
)
ELSE 'Not Found'
END
DECLARE #Red2 As nvarchar(100) = CASE
WHEN
(
SELECT MAX(PartNum) FROM dbo.table
WHERE PartDesc LIKE '%RED%' AND UPC = #upc
) IS NOT NULL THEN
(
SELECT MAX(PartNum) FROM dbo.table
WHERE PartDesc LIKE '%RED%' AND UPC = #upc
)
ELSE 'Not Found'
END
DECLARE #Blue1 As nvarchar(100) = CASE
WHEN
(
SELECT MAX(PartNum) FROM dbo.table
WHERE PartDesc LIKE '%BLUE%' AND UPC = #upc
) IS NOT NULL THEN
(
SELECT MAX(PartNum) FROM dbo.table
WHERE PartDesc LIKE '%BLUE%' AND UPC = #upc
)
ELSE 'Not Found'
END
;WITH MostColumns AS
(
SELECT UPC, #Red1 As Part1, #Red2 As Part2, #Blue1 As Part3
FROM (SELECT UPC, PartNum, PartDesc
FROM dbo.table) AS source
PIVOT
(MIN(PartNum) FOR PartDesc IN ([Part1], [Part2], [Part3])) AS pvt
)
SELECT MIN(p.ID) AS ID, p.UPC, mc.Part1, mc.Part2, mc.Part3
INTO MyNewTable
FROM dbo.table p
INNER JOIN MostColumns mc ON p.UPC = mc.UPC
GROUP BY p.UPC, mc.Part1, mc.Part2, mc.Part3
Result:
ID UPC Part1 Part2 Part3
2876 123 Not Found Not Found Not Found
2758 213 Not Found Not Found Not Found
2321 312 Not Found Not Found Not Found
802 321 Not Found Not Found Not Found
868 132 Not Found Not Found Not Found
This is the correct format, but no cigar. I know for a fact, that all of my UPCs contain atleast one Red1 part. For some reason, it did not find any of the parts.
EDIT--ANSWER
#Corgi After more research on dynamic pivots I arrived at this solution. I will still need to build on it to make it operate the way I need it to. Although, those are not relevant to this question.
Thank you #bluefeet for your answer in this post.
SQL Dynamic Pivot
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ','
+ QUOTENAME('Part_' + cast(rn as varchar(10)))
from dbo.table
cross apply
(
select row_number() over(partition by UPC order by PartNum) rn
from dbo.table
) x
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT UPC, ' + #cols + ' from
(
select UPC, PartNum,
''Component_''
+ cast(row_number() over(partition by UPC order by PartNum) as varchar(10)) val
from dbo.table
) x
pivot
(
max(PartNum)
for val in (' + #cols + ')
) p '
execute(#query)

Your observation about PIVOT is on track, but you can't actually use PIVOT without specifying the output column names (that is, the values from PartDesc). It sounds like, because there can be a varying number of these PartDesc values, the closest you can get would be to find all the values:
SELECT DISTINCT t.PartDesc
FROM MyTable t
WHERE t.PartDesc LIKE '%Red%' OR t.PartDesc LIKE '%Blue%'
Then you can use the values to build your query. If you really have to have the query be dynamic, you would need to construct a query string to use with something like sp_executesql. The way you would create the table from the output is by using SELECT... INTO within your dynamic query.
The PIVOT syntax you need, combined with SELECT... INTO, might look something like:
;WITH MostColumns AS
(
SELECT UPC, Red1, Red2, Blue1, Blue2
FROM (SELECT UPC, PartNum, PartDesc
FROM dbo.table) AS source
PIVOT
(MIN(PartNum) FOR PartDesc IN ([Red1], [Red2], [Blue1], [Blue2])) AS pvt
)
SELECT MIN(p.ID) AS ID, p.UPC, mc.Red1, mc.Red2, mc.Blue1, mc.Blue2
INTO MyNewTable
FROM dbo.table p
INNER JOIN MostColumns mc ON p.UPC = mc.UPC
GROUP BY p.UPC, mc.Red1, mc.Red2, mc.Blue1, mc.Blue2
The MostColumns common table expression is there because it doesn't work well to have ID in your original query -- it's an "extra" column that is not part of the pivot.

Related

SQL combine multiple records into one row

I have a table pulling userid's and their personal and work e-mails. I'd like to have one line per id showing both types of e-mails, but I can't figure out how to do that.
declare #t table(NPI int, email varchar(50), emailtype varchar(50))
insert into #t
values(1, 'john#home', 'personal'), (1, 'john#work', 'work');
This is the query I've written so far, which puts this on 2 separate rows:
select npi, case when emailtype = 'personal' then email end as personalemail,
case when emailtype = 'work' then email end as workemail
from #t;
Current Output:
npi personalemail workemail
1 john#home NULL
1 NULL john#work
What I'd like to see is:
npi personalemail workemail
1 john#home john#work
How can I do this?
This has been asked and answered around here about a million times. It is called conditional aggregation or crosstab. It is faster to write an answer than find one. As an alternative you could use PIVOT but I find the syntax a bit obtuse for me.
select NPI
, max(case when emailtype = 'personal' then email end) as PersonalEmail
, max(case when emailtype = 'work' then email end) as WorkEmail
from #t
group by NPI
Use pivot
SELECT
*
FROM #T
PIVOT
(
MAX(email)
FOR EmailType IN
(
personal,work
)
)Q

SQL server OPENJSON with multiple where on a filed

I have a table with a json field. The json schema is the same for all records.
I want to get just 2 products with red or blue color and with brand 1.
I tried the below query but I know that's not working:
SELECT [Id], [JName], [JValue]
FROM [Product]
CROSS APPLY OPENJSON([Json])
WITH ([JName] NVARCHAR(50) '$.name', [JValue] NVARCHAR(50) '$.value')
WHERE
(CASE WHEN [JName]=N'color' AND [JValue] IN (N'red', N'red') THEN 1 ELSE 0 END) &
(CASE WHEN [JName]=N'brand' AND [JValue] IN (N'brand 1') THEN 1 ELSE 0 END) = 1
so, how should I write this query?
I am a bit unsure of what you are asking:
How to get data matching your criteria (red or blue and brand1), since your posted query will not get what you want.
OR
How to do an OFFSET for pagination purposes.
OR
Both.
Anyway, after my initial comment above (but before your reply), I wrote a query which would give you what you want (red or blue and Brand1).
After your reply I modified the query to do OFFSET as well:
;WITH prod
AS
(
SELECT [Id], [JName], [JValue]
FROM dbo.tb_Product
CROSS APPLY OPENJSON([Json])
WITH ([JName] NVARCHAR(50) '$.name', [JValue] NVARCHAR(50) '$.value')
)
SELECT p1.Id, p1.JValue AS Color, p2.JValue AS Brand
FROM prod p1
JOIN prod p2
ON p1.Id = p2.Id
WHERE p1.JName = 'Color'
AND p1.JValue IN ('red', 'blue')
AND p2.JName = 'Brand'
AND p2.JValue IN ('Brand1')
ORDER BY p1.ID
OFFSET 0 ROWS
FETCH NEXT 2 ROWS ONLY;
I hope this is somewhat what you want.

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.

Returning Field names as part of a SQL Query

I need to write a Sql Satement that gets passed any valid SQL subquery, and return the the resultset, WITH HEADERS.
Somehow i need to interrogate the resultset, get the fieldnames and return them as part of a "Union" with the origional data, then pass the result onwards for exporting.
Below my attempt: I have a Sub-Query Callled "A", wich returns a dataset and i need to query it for its fieldnames. ?ordinally maybe?
select A.fields[0].name, A.fields[1].name, A.fields[2].name from
(
Select 'xxx1' as [Complaint Mechanism] , 'xxx2' as [Actual Achievements]
union ALL
Select 'xxx3' as [Complaint Mechanism] , 'xxx4' as [Actual Achievements]
union ALL
Select 'xxx5' as [Complaint Mechanism] , 'xxx6' as [Actual Achievements] ) as A
Any pointers would be appreciated (maybe i am just missing the obvious...)
The Resultset should look like the table below:
F1 F2
--------------------- ---------------------
[Complaint Mechanism] [Actual Achievements]
xxx1 xxx2
xxx3 xxx4
xxx5 xxx6
If you have a static number of columns, you can put your data into a temp table and then query tempdb.sys.columns to get the column names, which you can then union on top of your data. If you will have a dynamic number of columns, you will need to use dynamic SQL to build your pivot statement but I'll leave that up to you to figure out.
The one caveat here is that all data under your column names will need to be converted to strings:
select 1 a, 2 b
into #a;
select [1] as FirstColumn
,[2] as SecondColumn
from (
select column_id
,name
from tempdb.sys.columns
where object_id = object_id('tempdb..#a')
) d
pivot (max(name)
for column_id in([1],[2])
) pvt
union all
select cast(a as nvarchar(100))
,cast(b as nvarchar(100))
from #a;
Query Results:
| FirstColumn | SecondColumn |
|-------------|--------------|
| a | b |
| 1 | 2 |

How can I get all items to display as columns using SQL pivot when number of items is large?

If I have a table like so:
SalesPerson Product SalesAmount
Bob Pickles $100.00
Sue Oranges $50.00
Bob Pickles $25.00
Bob Oranges $300.00
Sue Oranges $500.00
and I wanted to get
SalesPerson Oranges Pickles
Bob $300.00 $125.00
Sue $550.00
I would use:
SELECT SalesPerson, [Oranges] AS Oranges, [Pickles] AS Pickles
FROM
(SELECT SalesPerson, Product, SalesAmount
FROM ProductSales ) ps
PIVOT
(
SUM (SalesAmount)
FOR Product IN
( [Oranges], [Pickles])
) AS pvt
However this would be a bit of a pain if I had a large number of products and I wanted to get the same result. I'm guessing I could use a Select statement like so:
FOR Product IN(SELECT DISTINCT(Product) FROM ProductSales
but I would still need to define the individual column headings i.e.
SELECT SalesPerson, [Oranges] AS Oranges, [Pickles] AS Pickles, [Bagels] AS Bagels, [MonsterTrucks] AS MonsterTrucks...
Is there a more efficient way of going about achieving this?
see,i do not know you exact table design.
I am just trying to give you rough idea here,And this is just main part of whole query .
Declare #item varchar(500)
Declare #Sql varchar(max)
create table t (SalesPerson varchar(50), Product varchar(50), SalesAmount float)
insert into t values
('Bob','Pickles', 100.00)
,('Sue','Oranges', 50.00)
,('Bob','Pickles', 25.00)
,('Bob','Oranges', 300.00)
,('Sue','Oranges', 500.00)
;With CTE as
(
Select distinct product from t
)
select
#item=stuff((select ','+ '['+ product + ']' from cte for xml path('')),1,1,'')
from CTE A
print #item --debug
set #sql=' SELECT SalesPerson, '+#item+'
FROM
(SELECT SalesPerson, Product, SalesAmount
FROM t ) ps
PIVOT
(
SUM (SalesAmount)
FOR Product IN
( '+#item+')
) AS pvt '
print #sql --debug
exec (#sql)

Resources