Row data from a comma-delimited field used within a select query - sql-server

I am trying to build a single select query that returns
a list of Product Type Description using Table1 that is not
in the ExcludedList of Table2.
I have the following tables and data in SQL Server 2008.
Table1(nProdType INT, sProdDesc VARCHAR)
nProdType SProdDesc
----------- --------------------
1 Pencils
2 Paper
3 Pens
4 Markers
5 Erasers
6 Crayons
7 HighLighters
8 Rulers
Table2(ClassID INT, ExcludeList VARCHAR)
ClassID ExcludedList
----------- --------------------
100 2,3,4,8
101 1,2,5,6,7,8
102 4,5,6,7
103 1,2,3,4,5,6,7,8
104 7
The query should return the following:
ClassID nProdType sProdDesc
-------- --------- --------------
100 1 Pencils
100 5 Erasers
100 6 Crayons
100 7 HighLighters
101 3 Pens
101 4 Markers
102 1 Pencils
102 2 Paper
102 3 Pens
102 8 Rulers
..and so on..
I know how to build (and of course there are plenty solutions in SO) a function to split the comma-delimited field but they return ALL the rows in the table and I want it to be per record (ClassID) so that I can query to see what's not in the ExcludedList. I am trying to not code this in C# or to use a RecordSet.

You can use a CSV Splitter for this. Here is the DelimitedSplit8K function by Jeff Moden.
;WITH CteDelimitted AS(
SELECT
t.ClassID,
nProdType = CAST(s.Item AS INT)
FROM Table2 t
CROSS APPLY dbo.DelimitedSplit8K(t.ExcludedList, ',') s
),
CteCross AS(
SELECT
t2.ClassID,
t1.nProdType,
t1.SprodDesc
FROM Table1 t1
CROSS JOIN(
SELECT DISTINCT ClassID FROM Table2
)t2
)
SELECT *
FROM CteCross c
WHERE NOT EXISTS(
SELECT 1
FROM CteDelimitted
WHERE
ClassID = c.ClassID
AND nProdType = c.nProdType
)
ORDER BY ClassID, nProdType
SQL Fiddle
Another approach using NOT IN:
WITH Cte AS(
SELECT
t2.ClassID,
t1.nProdType,
t1.SprodDesc
FROM Table1 t1
CROSS JOIN(
SELECT DISTINCT ClassID FROM Table2
)t2
)
SELECT *
FROM Cte c
WHERE c.nProdType NOT IN(
SELECT CAST(s.Item AS INT)
FROM Table2
CROSS APPLY dbo.DelimitedSplit8K(ExcludedList, ',') s
WHERE ClassID = c.ClassID
)
ORDER BY ClassID, nProdType
SQL Fiddle

Related

How do you dynamically Assing Unique Code for every row iin hierarchical table?

My table.
Table1
Id ParentId Name Code
1 Null John
2 1 Harry
3 1 Mary
4 2 Emma
5 2 Kyle
6 4 Robert
7 Null Rohit
I want to assign each individual with the the following format unique hierarchy codes
Output Required
Id ParentId Name Code
1 Null John 1
2 1 Harry 1.1
3 1 Mary 1.2
4 2 Emma 1.1.1
5 2 Kyle 1.1.2
6 4 Robert 1.1.1.1
7 Null Rohit 2
and so on.
I hope I've got this correctly...
You can use a recursive CTE together with ROW_NUMBER() in order to create your codes.
DECLARE #dummy TABLE(Id INT,ParentId INT,[Name] VARCHAR(100));
INSERT INTO #dummy(Id,ParentId,[Name]) VALUES
(1,Null,'John')
,(2,1 ,'Harry')
,(3,1 ,'Mary')
,(4,2 ,'Emma')
,(5,2 ,'Kyle')
,(6,4 ,'Robert')
,(7,Null,'Rohit');
WITH recCTE AS
(
SELECT Id,ParentId,[Name]
,CONCAT(N'.',CAST(ROW_NUMBER() OVER(ORDER BY Id) AS NVARCHAR(MAX))) AS Code
FROM #dummy WHERE ParentId IS NULL
UNION ALL
SELECT d.Id,d.ParentId,d.[Name]
,CONCAT(r.Code,N'.', ROW_NUMBER() OVER(ORDER BY d.Id))
FROM #dummy d
INNER JOIN recCTE r ON d.ParentId=r.Id
)
SELECT Id,ParentId,[Name]
,STUFF(Code,1,1,'') AS Code
FROM RecCTE;
The idea in short:
We pick the rows with ParentId IS NULL and give them a running number.
Now we go iteratively through them (it's a hidden RBAR actually) and call their children, again with a running number.
This we do until there is nothing left.
the final SELECT needs a STUFF in order to to get rid of the first dot.
And with an extension like this, you can create an alphanumerically sortable code:
WITH recCTE AS
(
SELECT Id,ParentId,[Name]
,CONCAT(N'.',CAST(ROW_NUMBER() OVER(ORDER BY Id) AS NVARCHAR(MAX))) AS Code
,CONCAT(N'000',CAST(ROW_NUMBER() OVER(ORDER BY Id) AS NVARCHAR(MAX))) AS Code2
FROM #dummy WHERE ParentId IS NULL
UNION ALL
SELECT d.Id,d.ParentId,d.[Name]
,CONCAT(r.Code,N'.', ROW_NUMBER() OVER(ORDER BY d.Id))
,CONCAT(r.Code2,RIGHT(CONCAT('0000',ROW_NUMBER() OVER(ORDER BY d.Id)),4))
FROM #dummy d
INNER JOIN recCTE r ON d.ParentId=r.Id
)
SELECT Id,ParentId,[Name]
,STUFF(Code,1,1,'') AS Code
,Code2
FROM RecCTE
ORDER BY Code2;

WHERE clause on value 1 (if present), or value 2 (if not)

MS SQL Server 2014
Considering a table like this:
idkey companyid |column 1 |column 2
------------------------------------------------
1 0 apple fruit 1
2 0 orange fruit 2
3 0 grapes fruit 3
4 10 banana fruit 2
CompanyId = 0 is the default list of values; other company ids can override individual values or take the default list.
Note that for companyid = 10, 'fruit 2' has a different value specified for column 1.
How do I select companyid 0's values with the company 10 override, as in:
idkey companyid |column 1 |column 2
------------------------------------------------
1 0 apple fruit 1
4 10 banana fruit 2
3 0 grapes fruit 3
Is there a way to do this with a single SQL statement, or do I need to create a stored procedure and temp table? I've searched quite a bit, but apparently can't find a way to phrase the question simply enough for a search to return useful answers.
select coalesce(t2.idkey, t1.idkey) as idkey,
coalesce(t2.companyid, t1.companyid) as companyid,
coalesce(t2.column1, t1.column1) as column1,
coalesce(t2.column2, t1.column2) as column2
from your_table t1
left join your_table t2 on t1.column2 = t2.column2
and t2.companyid <> 0
where t1.companyid = 0
Say company ID that you are interested in is 10. Your query could be:
declare #id bigint = 10
select
coalesce(t2.idkey, t1.idkey),
coalesce(t2.companyid, t1.companyid) companyid,
coalesce(t2.column1, t1.column1) column1,
t1.column2
from table t1
left join table t2 on t2.companyid = #id and t1.column2 = t2.column2
It makes sense to create a stored procedure or table-valued function with a single #id parameter
create procedure tableWithOverrides(#id bigint)
as
begin
select
coalesce(t2.idkey, t1.idkey),
coalesce(t2.companyid, t1.companyid) companyid,
coalesce(t2.column1, t1.column1) column1,
t1.column2
from table t1
left join table t2 on t2.companyid = #id and t1.column2 = t2.column2
end

Converting rows to columns

I have a query with the columns 'Name', 'Amount', and 'ReasonId'. I want to sum the amount and put the reasons on one row to keep every name to a single line. There are about 50 distinct ReasonId's so I do not want to name the column the name of the ReasonId's. Instead, I would like to name the columns 'Reason1', 'Reason2', 'Reason3', and 'Reason4'. One single name can have up to 4 different reasons.
I have this:
Name Amount ReasonId
-------------------------
Bob $5 7
Bob $8 6
John $2 8
John $5 9
John $3 9
John $8 4
I want to produce the following:
Name Amount Reason1 Reason2 Reason3 Reason4
-----------------------------------------------------
Bob $13 7 6 NULL NULL
John $18 8 9 4 NULL
One way to do this is to use the dense_rank window function to number the rows, and then use conditional aggregation to put the reason in the correct columns.
I can't see anything that would give the specific order of the reason columns though, maybe there is some column missing that provides the order?
with cte as (
select
name,
reasonid,
amount,
dense_rank() over (partition by name order by reasonid) rn
from your_table
)
select
name,
sum(amount) amount,
max(case when rn = 1 then reasonid end) reason1,
max(case when rn = 2 then reasonid end) reason2,
max(case when rn = 3 then reasonid end) reason3,
max(case when rn = 4 then reasonid end) reason4
from cte
group by name
If you have some column that gives the order you want then change the order by clause used in the dense_rank function.
Sample SQL Fiddle (using PG as MSSQL seems to be offline).
The output from the query above would be:
| name | amount | reason1 | reason2 | reason3 | reason4 |
|------|--------|---------|---------|---------|---------|
| Bob | 13 | 6 | 7 | (null) | (null) |
| John | 18 | 4 | 8 | 9 | (null) |
You could also use a pivot to achieve this; if you know the columns you can enter them in the script, but if not, you can use dynamic sql (there are reasons why you might want to avoid the dynamic solution).
The advantage of this route is that you can enter the column list in a table and then changes to that table will result in changes to your output with change to the script involved. The disadvantages are all those associated with dynamic SQL.
In the interests of variation, here is a dynamic SQL solution using temp tables to hold your data, since a different possibility has been provided:
-- set up your data
CREATE TABLE #MyTab (Name VARCHAR(4), Amount INT, ReasonId INT)
CREATE TABLE #AllPossibleReasons (Id INT,Label VARCHAR(10))
INSERT #AllPossibleReasons
VALUES
(1,'Reason1')
,(2,'Reason2')
,(3,'Reason3')
,(4,'Reason4')
,(5,'Reason5')
,(6,'Reason6')
,(7,'Reason7')
,(8,'Reason8')
,(9,'Reason9')
INSERT #MyTab
VALUES
('Bob',7,7)
,('Bob',8,6)
,('John',2,8)
,('John',5,9)
,('John',3,9)
,('John',8,4)
-----------------------------------------------------------------------------
-- The actual query
DECLARE #ReasonList VARCHAR(MAX) = ''
DECLARE #SQL VARCHAR(MAX)
SELECT #ReasonList = #ReasonList + ',' + QUOTENAME(Label)
FROM #AllPossibleReasons
SET #ReasonList = SUBSTRING(#ReasonList,2,LEN(#ReasonList))
SET #SQL =
'SELECT Name,Value,' + #ReasonList + ' FROM
(SELECT
M.Name,SUM(Amount) AS This, Label, SUM(Total.Value) AS Value
FROM
#MyTab AS M
INNER JOIN #AllPossibleReasons AS Reason ON M.ReasonId = Reason.Id
INNER JOIN(SELECT T.Name, SUM(Amount)Value
FROM #MyTab T GROUP BY T.Name) AS Total ON M.Name = Total.Name
GROUP BY M.Name, Reason.Label) AS Up
PIVOT (SUM(THis) FOR Label IN (' + #ReasonList + ')) AS Pvt'
EXEC (#SQL)
DROP TABLE #AllPossibleReasons
DROP TABLE #MyTab
Working from the information in ListAGG in SQLSERVER, I came up with this somewhat ugly example:
with tbl1 as (
-- Set up initial data set
select 'Bob' name, 5 amount, 7 ReasonId
union all select 'Bob' , 3, 4
union all select 'Bob', 2, 1
union all select 'Brian', 8, 2
union all select 'Bob', 6, 4
union all select 'Brian', 1, 3
union all select 'Tim', 2, 2)
, TBL2 AS ( -- Add a blank to separate the concatenation
SELECT NAME
, AMOUNT
, CAST(ReasonId as varchar) + ' ' ReasonId from tbl1
)
select ta.name
, Total
, ReasonIds from (
(select distinct name, stuff((select distinct '' + t2.ReasonId from tbl2 t2
where t1.name = t2.name
for xml path(''), type).value('.','NVARCHAR(MAX)'),1,0,' ') ReasonIds from tbl2 t1) ta
inner join ( select name, sum(amount) Total from tbl1 group by name) tb on ta.name = tb.name) ;
This converts TBL1 to the following:
name Total ReasonIds
Bob 16 1 4 7
Brian 9 2 3
Tim 2 2

SQL Server 2008 how to select top [column value] and random record?

I'm using SQL Server 2008, I want select random row record, and the total number of record is depend on another table's column value, how to do this?
My SQL statement is something like this, but wrong..
select top b.number a.name, a.link_id
from A a
left join B b on b.link_id = a.link_id
order by newid()
Here are my tables and the expected result.
Table A:
name link_id
james 100
albert 100
susan 100
simon 101
tom 101
fion 101
Table B:
link_id number
100 2
101 1
Expected result:
when run 1st time, result may be:
name link_id
james 100
susan 100
fion 101
2nd time result may be:
albert 100
susan 100
simon 101
3rd time could be:
james 100
albert 100
fion 101
Explaination
Refer to table B, link_id: 100, number: 2
meaning that Table A should select out 2 random record for link_id = 100
and need to select 1 random record for link_id=101
You can use the ROW_NUMBER() function:
SELECT A.name, A.link_id
FROM(
SELECT name,link_id, ROW_NUMBER()OVER(PARTITION BY link_id ORDER BY NEWID()) rn
FROM dbo.tblA
) AS A
JOIN dbo.tblB AS B
ON A.link_id = B.link_id
WHERE A.rn <= B.number;
Here is a SqlFiddle to show this in action: http://sqlfiddle.com/#!3/92eac/2
Try this:
SELECT a.*
FROM b
CROSS APPLY
(
SELECT TOP (b.number) a.*
FROM a
WHERE a.link_id = b.link_id
ORDER BY
NEWID()
) a
Also see: SQLFiddle

t-sql most efficient row to column? crosstab for xml path, pivot

I am looking for the most performant way to turn rows into columns. I have a requirement to output the contents of the db (not actual schema below, but concept is similar) in both fixed width and delimited formats. The below FOR XML PATH query gives me the result I want, but when dealing with anything other than small amounts of data, can take awhile.
select orderid
,REPLACE(( SELECT ' ' + CAST(ProductId as varchar)
FROM _details d
WHERE d.OrderId = o.OrderId
ORDER BY d.OrderId,d.DetailId
FOR XML PATH('')
),' ','') as Products
from _orders o
I've looked at pivot but most of the examples I have found are aggregating information. I just want to combine the child rows and tack them onto the parent.
I should also point out I don't need to deal with the column names either since the output of the child rows will either be a fixed width string or a delimited string.
For example, given the following tables:
OrderId CustomerId
----------- -----------
1 1
2 2
3 3
DetailId OrderId ProductId
----------- ----------- -----------
1 1 100
2 1 158
3 1 234
4 2 125
5 3 101
6 3 105
7 3 212
8 3 250
for an order I need to output:
orderid Products
----------- -----------------------
1 100 158 234
2 125
3 101 105 212 250
or
orderid Products
----------- -----------------------
1 100|158|234
2 125
3 101|105|212|250
Thoughts or suggestions? I am using SQL Server 2k5.
Example Setup:
create table _orders (
OrderId int identity(1,1) primary key nonclustered
,CustomerId int
)
create table _details (
DetailId int identity(1,1) primary key nonclustered
,OrderId int
,ProductId int
)
insert into _orders (CustomerId)
select 1
union select 2
union select 3
insert into _details (OrderId,ProductId)
select 1,100
union select 1,158
union select 1,234
union select 2,125
union select 3,105
union select 3,101
union select 3,212
union select 3,250
CREATE CLUSTERED INDEX IX_CL__orders on _orders(OrderId)
CREATE NONCLUSTERED INDEX IX_NCL__orders on _orders(OrderId)
INCLUDE (CustomerId)
CREATE CLUSTERED INDEX IX_CL_details on _details(OrderId)
CREATE NONCLUSTERED INDEX IX_NCL_details on _details(OrderId)
INCLUDE (DetailId,ProductId)
using FOR XML PATH:
select orderid
,REPLACE(( SELECT ' ' + CAST(ProductId as varchar)
FROM _details d
WHERE d.OrderId = o.OrderId
ORDER BY d.OrderId,d.DetailId
FOR XML PATH('')
),' ','') as Products
from _orders o
which outputs what I want, however is very slow for large amounts of data. One of the child tables is over 2 million rows, pushing the processing time out to ~ 4 hours.
orderid Products
----------- -----------------------
1 100 158 234
2 125
3 101 105 212 250
By definition a PIVOT is going to HAVE to aggregate in some way, because you can have multiple rows with the same pivot key columns. If you don't have multiple rows, that's fine - but you still need to choose an aggregate operator (MIN, MAX, SUM).
But the FOR XML PATH construct is better for the multiple-row-values to single-string-column "pivot" operation.
I'm not sure why yours is not performing well. What indexes do you have on the tables? What does your execution plan look like?

Resources