Complex SQL selection query - sql

I have a stored procedure which needs a different if condition to work properly.
The procedure has 2 parameter namely, #CategoryID and #ClassID, which basically come from a UI tree view functionality. #CategoryID corresponds to the parent nodes, while #ClassID corresponds to the child nodes.
Based upon the above parameters I need to make a selection(Column Code) from a table which has CategoryID and ClassID as columns.
Now there are 2 scenarios:
Scenario 1
#CategoryID:A
#ClassID:B (which is a child node of CategoryID A)
Result needed: Codes corresponding to only ClassID B, which is basically the intersection
Scenario 2
#CategoryID:A
#ClassID: C (which is not a child node for CategoryID A)
Result needed: Codes corresponding to the CategoryID A, as well as ClassID B, basically a union
The procedure which I wrote gives me correct answer for the second scenario, but the first scenario it fails. Below is my procedure:
ALTER PROCEDURE [dbo].[uspGetCodes]
#CategoryID varchar(50),
#ClassID varchar(50)
AS
BEGIN
BEGIN TRY
DECLARE #SQLQuery NVARCHAR(MAX)
SET #SQLQuery=N'SELECT Code FROM dbo.ClassToCategoryMapping WHERE '
IF (#CategoryID IS NULL OR #CategoryID='')
BEGIN
SET #SQLQuery=#SQLQuery + 'ClassId IN ('+#ClassID+')'
PRINT(#SQLQuery)
EXEC(#SQLQuery)
END
ELSE IF (#ClassID IS NULL OR #ClassID='')
BEGIN
SET #SQLQuery=#SQLQuery+'CategoryID IN ('+#CategoryID+')'
PRINT(#SQLQuery)
EXEC(#SQLQuery)
END
ELSE
BEGIN
SET #SQLQuery=#SQLQuery+'(CategoryID IN ('+#CategoryID+') OR ClassId IN ('+#ClassID+') )'
PRINT(#SQLQuery)
EXEC(#SQLQuery)
END
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS 'ErrorNumber', ERROR_MESSAGE() AS 'ErrorMessage', ERROR_SEVERITY() AS 'ErrorSeverity', ERROR_STATE() AS 'ErrorState', ERROR_LINE() AS 'ErrorLine'
RETURN ERROR_NUMBER()
END CATCH
END
The Last Else part actually does an 'OR', which gives me the union of the Codes for CategoryID's and ClassID's irrespective whether the given ClassID is a child of the given CategoryID or not.
My question over here would be, how to write the condition to achieve both the scenarios.
Latest Sample Data:
Scenario 1
#CategoryId=2,5, #ClassID=10 (Here 10 is the child while 2 is the parent, CategoryID 2 corresponds to ClassID's 10, 11, 12)
Expected Result: 10, 26, 27 (26 and 27 correspond to the CategoryID 5)
Scenario 2
#CategoryID=2, #ClassID=13,15 (13 and 15 is the child of a different parent, CategoryID 2 corresponds to ClassID's 10, 11 ,12)
Expected Result: 10, 11, 12, 13, 15
Data in Table dbo.ClasstoCategoryMapping will be somewhat as below:
CategoryID ClassID Code
2 10 200
2 11 201
2 12 202
5 26 501
5 27 502
6 15 601
6 16 602
6 17 603
7 20 701
7 21 702
7 22 703
I guess I have made my question quite clear, if no then, folks can ask me to edit it. I would be happy to do so. I urge the experts to assist me in this problem. Any pointers too will be quite appreciated.
Regards
Anurag

If I understand the question correctly, what you require in your result set is:
(all supplied classid) + (all classid for supplied categoryid with no matching supplied classid)
That would translate to the following:
CREATE PROCEDURE [dbo].[uspGetCodes]
(
#CategoryID varchar(50),
#ClassID varchar(50)
)
AS
BEGIN
SELECT COALESCE(CM.CategoryID, CM2.CategoryID) AS CategoryID,
COALESCE(CM.ClassID, CM2.ClassID) AS ClassID,
COALESCE(CM.Code, CM2.Code) AS Code
--Matched classIDs:
FROM dbo.udfSplitCommaSeparatedIntList(#ClassID) CLAS
JOIN dbo.ClassToCategoryMapping CM
ON CM.ClassId = CLAS.Value
--Unmatched CategoryIDs:
FULL
OUTER
JOIN dbo.udfSplitCommaSeparatedIntList(#CategoryID) CAT
ON CM.CategoryID = CAT.Value
LEFT
JOIN dbo.ClassToCategoryMapping CM2
ON CM.CategoryID IS NULL
AND CM2.CategoryID = CAT.Value
END
I have included Category, Class and Code in the result since its easier to see what's going on, however I guess you only really need code
This makes use of the following function to split the supplied comma separated strings:
CREATE FUNCTION [dbo].[udfSplitCommaSeparatedIntList]
(
#Values varchar(50)
)
RETURNS #Result TABLE
(
Value int
)
AS
BEGIN
DECLARE #LengthValues int
SELECT #LengthValues = COALESCE(LEN(#Values), 0)
IF (#LengthValues = 0)
RETURN
DECLARE #StartIndex int
SELECT #StartIndex = 1
DECLARE #CommaIndex int
SELECT #CommaIndex = CHARINDEX(',', #Values, #StartIndex)
DECLARE #Value varchar(50);
WHILE (#CommaIndex > 0)
BEGIN
SELECT #Value = SUBSTRING(#Values, #StartIndex, #CommaIndex - #StartIndex)
INSERT #Result VALUES (#Value)
SELECT #StartIndex = #CommaIndex + 1
SELECT #CommaIndex = CHARINDEX(',', #Values, #StartIndex)
END
SELECT #Value = SUBSTRING(#Values, #StartIndex, LEN(#Values) - #StartIndex + 1)
INSERT #Result VALUES (#Value)
RETURN
END

this is the sample query that can achieve your goal, is this what you want?
DECLARE #SAMPLE TABLE
(
ID INT IDENTITY(1,1),
CategoryId INT,
ClassID INT
)
INSERT INTO #sample
VALUES(2,10)
INSERT INTO #sample
VALUES(2,11)
INSERT INTO #sample
VALUES(2,12)
INSERT INTO #sample
VALUES(3,13)
DECLARE #CategoryID INT
DECLARE #ClassID Int
--Play around your parameter(s) here
SET #CategoryID = 2
SET #ClassID = 13
--Snenario 1
--#CategoryId=2, #ClassID=10 (Here 10 is the child while 2 is the parent, CategoryID 2 corresponds to ClassID's 10, 11, 12)
--Expected Result: 10
IF EXISTS(SELECT * FROM #SAMPLE WHERE CategoryId = #CategoryID AND ClassID = #ClassID)
SELECT ClassID FROM #SAMPLE WHERE CategoryId = #CategoryID AND ClassID = #ClassID
--Scenario 2
--#CategoryID=2, #ClassID=13 (13 is the child of a different parent, CategoryID 2 corresponds to ClassID's 10, 11 ,12)
--Expected Result: 10, 11, 12, 13
ELSE
SELECT ClassID FROM #SAMPLE WHERE ClassID = #ClassID OR CategoryId = #CategoryID

Try this
select * from yourtable
where CategoryId = #CategoryID and ClassID = #ClassID
union
select * from
(
select * from yourtable where ClassID = #ClassID
union
select * from yourtable where CategoryId = #CategoryID
) v
where not exists (select * from yourtable where CategoryId = #CategoryID and ClassID = #ClassID)

UPDATE FOR DELIMITED STRING
If you have a comma delimited string then it is best to use a CLR function to create the table, but you could use a SQL function. Examples of how to do this are easy to find with a Google search... but for reference here is one good article on the subject -> http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings I expect at some point there will be native support on most platforms.
Given that you have a function that returns a table of one column (named ID) of type int, or an empty table on a null input. Note: You may have to have the null return a table with one row containing an invalid value (a value that will never join), say -1.
The code is as simple as this:
SELECT Code
FROM ClassToCategoryMapping
LEFT JOIN MakeTableFromCVS(#CategoryID) AS CatTable
ON CatTable.ID = CategoryID
LEFT JOIN MakeTableFromCVS(#ClassID) AS ClassTable
ON ClassTable.ID = ClassID
WHERE
CASE
WHEN #CatgoryID IS NULL THEN -1 ELSE CatTable.ID
END = ISNULL(CatTable.ID,-1)
AND
CASE
WHEN #ClassID IS NULL THEN -1 ELSE ClassTable.ID
END = ISNULL(ClassTable.ID,-1)
AND
COALESCE(CatTable.ID,ClassTable.ID,-1) != -1
The logic is the same as below. Because the join will vary the values if it is not null we have to use a different trick. Here we use a marker value (in this case -1) to signal the null value. Any value that won't appear in the comma separated list will work as this marker value, remember it must be of the same type.
You don't need dynamic SQL here and you will find SQL server is better at optimizing if you don't use dynamic SQL. In fact, you don't even need an if statement If you can be sure the input is always null you can do this:
SELECT Code
FROM ClassToCategoryMapping
WHERE
How this works
This query checks for both CategoryID and ClassID columns match the incoming parameters but "ignores" the input when they are null by checking the column against itself. This is an handy SQL trick.
Note if you do need to check for empty strings then this will be almost as fast
DECLARE #myCatID varchar(max)
DECLARE #myClassID varchar(max)
SET #myCatID = #CategoryID
SET #myClassID = #ClassID
IF LTRIM(RTRIM(#CategoryID) = '' THEN SET #myCatID = NULL
IF LTRIM(RTRIM(#ClassID) = '' THEN SET #myClassID = NULL
SELECT Code
FROM ClassToCategoryMapping
WHERE CatgoryID = ISNULL(#myCatID,CategoryID)
AND ClassID = ISNULL(#myClassID,ClassID)
You can replace ISNULL() with COALESCE() if you want... they do the same thing in this case.

Related

I am having the error with a Subquery returning more than one value. How do I reduce to one?

At the very end of the Stored procedure a SELECT statement is made to display the contents of the Table including function that will simultaneously populate fields in the table.
Here is the Select Statement:
IF #type = 'SH'
SELECT DISTINCT *
FROM #History
ORDER BY 1, 2, 3, 4, 5
ELSE
SELECT DISTINCT AmhazName
,Activity
,ServiceName
,Sarid
,PerformedDate
,UserRole
,Details
,dbo.ufn_SarHistoryActionText(sarid, status, performeddate) AS [ActionText]
,FullName
,CategoryDescription
,StatusDescription
,ActionPerformed
,Case
when Details like '%ProjManagerId%'
Then dbo.ufn_GetUserForHistoryReport (PerformedDate, SarId, '%ProjManagerId%')
Else
--when Details like '%UserId%'
dbo.ufn_GetUserForHistoryReport (PerformedDate, SarId, '%UserId%')
--(select 'no user') as [AssignedUser]
End as [AssignedUser]
--,dbo.ufn_GetPMForHistoryReport(PerformedDate, SarId) as [AssignedUser]
FROM #history
ORDER BY 1, 2, 3, 4, 5
DROP TABLE #Historyw
Here is the function I believe is causing problems:
ALTER FUNCTION [dbo].[ufn_SarHistoryActionText]
(
-- Add the parameters for the function here
#sarID int
, #status varchar(6)
, #statusDate datetime
)
RETURNS varchar(100)
AS
BEGIN
-- Declare the return variable here
DECLARE #Result varchar(100)
set #Result = (
SELECT C.ActionText
from LuStatusChange as C
WHERE C.FromStatus = dbo.ufn_SarHistoryPriorStatus(#sarID,#status,#statusDate)
AND C.ToStatus = #status
)
-- Return the result of the function
RETURN #Result
END
GO
As I debug and walk through loads of values, I haven't come across anything that resulted in multiple values. maybe I'm missing something.
Add TOP 1 in the select inside the function:
SELECT TOP 1 C.ActionText
Can you replace
set #Result = (
SELECT C.ActionText
from LuStatusChange as C
WHERE C.FromStatus = dbo.ufn_SarHistoryPriorStatus(#sarID,#status,#statusDate)
AND C.ToStatus = #status
)
as below:
#Result ***IN*** (
SELECT C.ActionText
from LuStatusChange as C
WHERE C.FromStatus = dbo.ufn_SarHistoryPriorStatus(#sarID,#status,#statusDate)
AND C.ToStatus = #status
)
If functionally your query should not written more than 1 row, something is wrong with your query.

SQL Server: how to update a column with a value that is in that column when another number in another column is >1

I have a table with the following data:
Part Comp level item_nbr
-------------------------------
abc ab 1 1
null cd 2 2
null ef 3 3
cde gh 1 4
null ij 2 5
null kl 3 6
null mn 4 7
I would like to update the nulls to the value in each level 1, so every level that is >1 is updated with the level one value.
Part Comp level
---------------------
abc ab 1
abc cd 2
abc ef 3
cde gh 1
cde ij 2
cde kl 3
cde mn 4
I am at a loss as to how to achieve this on a very large dataset. Any help would be greatly appreciated!
To explain another way,
part level
abc 1
2
3
Then the next row is populated with another part
efg 1
2
2
etc.
Further clarification:
I need the string"abc" to be filled down with the string "abc" while the column fields below are null. The next row has a string of efg and the following column fields below are null, again, those fields should be filled down with the value "efg" and so on.
The level field = 1 will always have a part number, but all the other levels report up to the level 1 part, so should be populated identically. And repeat.
Hope this makes sense.
Use an updatable CTE with window functions:
with toupdate as (
select t.*,
max(part) over (partition by itm_nbr_not_null) as new_part
from (select t.*,
max(case when part is not null then item_nbr end) over (order by item_nbr) as itm_nbr_not_null
from t
) t
)
update toupdate
set part = new_part
where part is null;
You can run the CTE to see what is happening.
well, from your question what I understand is, you need to update the null column's value until you get a not null value. and you want to continue it up to the last row of the table.
for that scenario, I created a stored procedure, where I read the value of every n-th cell if it is null I changing it with the prev. cell's value, when the cell was not null.
Approach:
create a temporary table/ table variable.
add an extra column, which is basically identity, which will help to rank the column.
iterate a loop until the maximum row is reached.
in each iteration, read the cell value for the i-th row
4.1 if it is not null put it in a temporary variable.
4.2 else, replace/update the i-th cell's value with the temporary variable
continue it, until you reached up to the last row of the table/table variable.
look at my following snippets:
create proc DemoPost
as
begin
declare #table table(serial_no int identity(1,1), name varchar(30), text varchar(30), level int)
insert #table
select Name, Text, Level from Demo
declare #max as int = (select max(serial_no) from #table)
--select #max
declare #i as int =0
declare #temp as varchar(30)
declare #text as varchar(30)
while #i < #max
begin
set #i = #i +1
set #temp = (select name from #table where serial_no = #i)
-- if #temp is not null, fetch its value, otherwise, update/replace it with
-- previously gotten not-null cell's value.
if #temp is not null
begin
set #text = (select name from #table where serial_no = #i)
end
else
begin
update #table
set name = #text where serial_no = #i
end
end
select name, text, level from #table
end
You can update it using temporary table according to the given scenario i thought item_nbr is unique in row Hope this will help
SELECT *
INTO #TEMP
FROM URTablehere
DECLARE #PRev VARCHAR(MAX)
WHILE ( SELECT COUNT(*)
FROM URTablehere
) > 0
BEGIN
DECLARE #ID INT
DECLARE #Part VARCHAR(MAX)
DECLARE #Num INT
SELECT TOP ( 1 )
#ID = level ,
#Part = Part ,
#Num = item_nbr
FROM #TEMP
IF ( #ID = 1 )
BEGIN
SELECT #PRev = #Part
END
IF ( #ID > 1
AND #Part IS NULL
)
BEGIN
UPDATE URTablehere
SET Part = #PRev
WHERE item_nbr = #Num
END
DELETE
FROM #TEMP WHERE item_nbr=#Num
END

SQL Server: efficiently search for many values on many to many columns?

I am creating a website using SQL Server. In the admin interface, I have two fields:
Subject: Math, English, History, ...
Grade: 1, 2, 3, 4, ...
Multiple values of a field can be assigned to a record.
Now in the frontend search, I would like a visitor to be able to select more than one value of a field for search. For example, someone may search for Subject being Math OR History and Grade being 1 OR 3.
What table design and SQL statement (or MS-proprietary statement) should I use to have efficient search?
Thanks and regards.
UPDATE
Thanks for all input!
I feel compelled to explain. I am technical and familiar with SQL. One thing I learned over my MANY years of programming experience is to be practical. For this question, I already have an initial design, but not sure how other folks to handle it for EFFICIENT SEARCH (there are always smarter folks out there). Here is my table design for storing a record:
Subject
type: varchar. record example: ,1,3, (each is the id of corresponding value)
Grade (this means applicable grade)
type: varchar. record example: ,1,2, (each is the id of corresponding values. this means a record is applicable to grade 1, 2)
Search example
where (subject LIKE '%,1,%' OR subject like '%,3,%') AND (grade like '%,1,%')
This design should lead to efficient search, but has drawbacks that it increases the complexity data management in the backend.
Another reason for my design is: the Subject and Grade each have a list of values that never/rarely change, and once a record is created, it never or rarely updates.
I am trying to strike a balance among simplicity, understandability, design, management, etc.
create table Subject (
SubjectId int identity(1, 1),
SubjectName nvarchar(255),
other fields.... )
create table GradingScale (
GradeId int identity(1, 1),
Grade int,
Description varchar(25),
other fields... )
create table Students (
StudentId int identity(1, 1),
StudentName nvarchar(255))
create table StudentGrades (
StudentId int,
SubjectId int,
GradeId int,
SemesterId int )
create FUNCTION [dbo].[fnArray] ( #Str varchar(max), #Delim varchar(1) = ' ', #RemoveDups bit = 0 )
returns #tmpTable table ( arrValue varchar(max))
as
begin
declare #pos integer
declare #lastpos integer
declare #arrdata varchar(8000)
declare #data varchar(max)
set #arrdata = replace(#Str,#Delim,'|')
set #arrdata = #arrdata + '|'
set #lastpos = 1
set #pos = 0
set #pos = charindex('|', #arrdata)
while #pos <= len(#arrdata) and #pos <> 0
begin
set #data = substring(#arrdata, #lastpos, (#pos - #lastpos))
if rtrim(ltrim(#data)) > ''
begin
if #RemoveDups = 0
begin
insert into #tmpTable ( arrValue ) values ( #data )
end
else
begin
if not exists( select top 1 arrValue from #tmpTable where arrValue = #data )
begin
insert into #tmpTable ( arrValue ) values ( #data )
end
end
end
set #lastpos = #pos + 1
set #pos = charindex('|', #arrdata, #lastpos)
end
return
end
select *
from Students st
inner join StudentGrades sg on sg.StudentId = st.StudentId
inner join Subject s on sg.SubjectId = s.SubjectId
inner join GradingScale gs on sg.GradeId = gs.GradeId
inner join dbo.fnArray(#subjects, ',') sArr on s.SubjectId = convert(int, sArr.arrValue)
inner join dbo.fnArray(#grades, ',') gArr on gs.GradeId = convert(int, gArr.arrValue)
obviously #subjectId and #gradeId could be passed in via some drop down selectors or however your UI is composed.
Edited to use dbo.fnArray, a little tool that can parse delimited strings into a list.
Now of course this would mean that if you had 2 subjects and 2 grades...like Show me all students that took ( Math and Science ) and scored ( 2 or 3 ) this would work. However if you wanted students who took Math and scored 2 or 3 or Students who took Science and scored a 3 you would have to rework the query.

stored procedure refuses to insert data

I have the following table:
ThisCategoryID int IDENTITY, AUTO_INCREMENT
Title text
Type text
CategoryID int ALLOW NULLS
IsActive bit
OrderIndex int ALLOW NULLS
with this data:
ThisCategoryID Title Type CategoryID IsActive OrderIndex
0 Lunch Menu Section NULL True 3
2 Dessert Menu Section NULL True 1
3 Banh Mi Food Item 0 True 4
and the following stored procedure:
ALTER PROCEDURE [dbo].[sp_new_category]
#Title text,
#Type text,
#CategoryID int = null,
#IsActive bit,
#OrderIndex int = null
AS
DECLARE #Identity int
IF (SELECT count(*) FROM Category WHERE difference(title, #title) = 4) > 0
BEGIN
INSERT INTO Category (Title, Type, CategoryID, IsActive, OrderIndex) VALUES (#Title, #Type, #CategoryID, #IsActive, #OrderIndex)
SET #Identity = scope_identity()
END
ELSE
BEGIN
SET #Identity = -1
END
SELECT #Identity AS ID
RETURN #Identity
There is no item in the table with the title "Snack", yet the sp_new_category yields -1, every time I run it with the following parameters:
#Title: Snack
#Type: Menu Section
#CategoryID: NULL
#IsActive: True
#OrderIndex: NULL
Can someone explain to me why that is?
I believe your intent for the following conditional is to check if someone is trying enter an already existing item by Title (matched with SOUNDEX):
IF (SELECT count(*) FROM Category WHERE difference(title, #title) = 4) > 0
However, the way the conditional is written, you will only add items if they are similar. Try this instead:
DECLARE #Identity int
IF (SELECT count(*) FROM Category WHERE difference(title, #title) = 4) > 0
BEGIN
SET #Identity = -1
END
ELSE
BEGIN
INSERT INTO Category (Title, Type, CategoryID, IsActive, OrderIndex) VALUES (#Title, #Type, #CategoryID, #IsActive, #OrderIndex)
SET #Identity = scope_identity()
END
SELECT #Identity AS ID
RETURN #Identity
SELECT difference('Snack', 'Lunch')
UNION SELECT difference('Snack', 'Dessert')
UNION SELECT difference('Snack', 'Banh Mi')
3
1
2
None of your differences ever equal 4, so your insert is never run, so #identity will always be -1
difference is the difference in SOUNDEX encodings for text, which is an archaic hash for English names. It is not suitable for foreign words. If you tell us what you think you are accomplishing by using it, we may be able to help you.
Am I missing something?
The IF statement is false so he goes to the ELSE and puts SET #Identity = -1. So it'll return -1.

SQL Query return values in a set sequence

I have been trying for a while now to return data from the database with the ID(int) values in the following order.
3, 6, 1, 9, 2, 5.
Is there anyway this can be done?
EDIT: Ok i made a bit of a stuff up in my post. the ID's above are just an example.
I am trying to do this dynamically, based around how many records from another table are linked to the record i want to pull out, e.g. i host 3 branches and each branch has a group of shops how would i determine which has the most?
I hope this helps.
Yes, something like this:
select ID from tablename
order by
CASE WHEN ID = 3 THEN 1
WHEN ID = 6 THEN 2
WHEN ID = 1 THEN 3
WHEN ID = 9 THEN 4
WHEN ID = 2 THEN 5
WHEN ID = 5 THEN 6
ELSE 7 END, ID ASC
This will put 3,6,1,9,2,5 and afterwords the other numbers in ascending order.
select cols from table where
order by
case ID when 3 then 0
when 6 then 1
when 1 then 2
when 9 then 3
...
end
You get the idea...
Create a table for the sorting.
CREATE TABLE SortPriority (
SourceID int NULL,
Priority int NULL)
Populate it with the ids and what order they should showup in. Join to the table. and use SortPriority.Priority in your sorting.
You can more easily change the sorting around this way. You would just need to modify the data. You can also later write scripts to populate the table to handle predictable needs in the changing of the sorting.
A split function like this one:
CREATE FUNCTION fnSplit(#str varchar(max), #dlm char(1))
RETURNS #result TABLE (id int, value varchar(50))
AS BEGIN
DECLARE
#id int, #value varchar(50),
#lastpos int, #pos int, #len int;
SET #id = 0;
SET #len = LEN(#str);
SET #lastpos = 1;
SET #pos = CHARINDEX(#dlm, #str + #dlm);
IF #pos <> 0
WHILE 1 = 1 BEGIN
SET #value = SUBSTRING(#str, #lastpos, #pos - #lastpos);
IF #value <> '' BEGIN
SET #id = #id + 1;
INSERT INTO #result VALUES (#id, #value);
END;
IF #pos > #len BREAK;
SET #lastpos = #pos + 1;
SET #pos = CHARINDEX(#dlm, #str + #dlm, #lastpos);
END;
RETURN;
END
would return a row set containing not only the values, but also their indexes within the list. You could then use the function in this way:
SELECT
…
FROM atable t
LEFT JOIN dbo.Split('3,6,1,9,2,5', ',') s ON t.Value = s.Value
ORDER BY
CASE WHEN s.id IS NULL THEN 2147483647 ELSE s.id END