How would I extend INTERSECT for an arbitrary number of intersections in a stored procedure? - plsql

I have a table like StockItem { StoreId, ItemId } (where both fields are foreign keys), which details all items stocked in each store.
To answer the question "which stores stock X and Y" I can use INTERSECT like:
CREATE OR REPLACE PROCEDURE FindStoresStockingBothItems (
X IN NUMBER, Y IN NUMBER
) AS
BEGIN
select StoreId from StockItem where ItemId = X
INTERSECT
select StoreId from StockItem where ItemId = Y;
END;
My use case is to call from C# in a method like List<int> FindStoresStockingBothItems(int x,int y)
But what if I want to extend this to a variable number of items? My C# method signature might now be List<int> FindStoresSellingAllItems(List<int> items) but I've no idea how to do this in PL/SQL - either how to do the multiple intersections or how to pass in a variable number of input item Ids.
What might a PL/SQL stored procedure look like?

A very simple solution, albeit not beautiful, would be to pass a string with the values:
CREATE OR REPLACE PROCEDURE FindStoresStockingBothItems (p_ids VARCHAR2) AS
v_storeid INTEGER;
BEGIN
SELECT storeid
INTO v_storeid
FROM stockitem
WHERE ',' || p_ids || ',' LIKE '%,' || ItemId || ',%'
GROUP BY storeid
HAVING COUNT(*) = REGEXP_COUNT(p_ids, ',') + 1;
END;

Related

Using a variable in PL/SQL

I am using PL/SQL in Toad for Oracle.
I would like to use define a variable in my code and then using this variable multiple times in the query.
Please note that I'm not asking for the pop-up window in which input the value of the variable; I need something like this:
DEFINE min_salary = 100
SELECT Surname FROM employees
WHERE salary < min_salary
I.e. min_salary in the WHERE statement assumes the value defined above.
Surfing the net, someone suggests to add an & before the variable in the where statement, i.e.
DEFINE min_salary = 100
SELECT Surname FROM employees
WHERE salary < &min_salary
But this is not useful in my case, since the & calls the pop-up window.
Instead, I would insert the value directly in the code.
Anyone could help?
A Select-Statement is not PL/SQL it's SQL. You need to create PL/SQL-Code:
DECLARE
min_salary employees.salary%TYPE := 100;
BEGIN
FOR i IN (SELECT Surname
FROM employees
WHERE salary < min_salary)
LOOP
DBMS_OUTPUT.put_line ('Surname: ' || i.Surname);
END LOOP;
END;
I don't know what you want to do, but you have to choose where to get the output. A PL/SQL-Script doesn't output the data-grid. You only run it.
You also could build a function to validate. Example:
CREATE OR REPLACE FUNCTION IsMinSalary (salary NUMBER)
RETURN NUMBER
IS
defaultMinSalary employees.salary%TYPE := 100;
BEGIN
IF (defaultMinSalary < salary)
THEN
RETURN 0;
ELSE
RETURN 1;
END IF;
END IsMinSalary;
/
SELECT surname
FROM (SELECT 10 AS Salary, 'ten' AS Surname FROM DUAL
UNION ALL
SELECT 100 AS Salary, 'hundred' AS Surname FROM DUAL
UNION ALL
SELECT 200 AS Salary, 'two-hundred' AS Surname FROM DUAL) t -- fake-table
WHERE IsMinSalary (t.salary) = 1

Using a CASE with the IN clause in T-SQL

In My WHERE Clause I am using a CASE that will return all rows if parameter is blank or null. This works fine with single valuses. However not so well when using the IN clause. For example:
This works very well, if there is not match then ALL records are returned!
AND
Name = CASE WHEN #Name_ != '' THEN #Name_ ELSE Name END
AND
This works also, but there is no way to use the CASE expression, so if i do not provide some value i will not see any records:
AND
Name IN (Select Names FROM Person Where Names like #Name_)
AND
Can I use a CASE with the 2nd example? So if there is not a match all Names will be returned?
Maybe coalesce will resolve your problem
AND
Name IN (Select Names FROM Person Where Names like coalesce(#Name,Name))
AND
As Mattfew Lake said and used, you can also use isnull function
Name IN (Select Names FROM Person Where Names like isnull(#Name,Name))
No need for a CASE statement, just use a nested OR condition.
AND ( Name IN (Select Names FROM Person Where Names like #Name_)
OR
#Name_ IS NULL
)
AND
Perhaps something like this?
AND
Name IN (Select Names FROM Person Where Names LIKE
CASE WHEN #Name_ = '' OR #Name_ IS NULL THEN '%' ELSE #Name_ END)
AND
This will use the pattern '%' (which will match everything) only when a null or blank #Name_ parameter is provided, otherwise it will use the pattern specified by #Name_.
Alternatively, something like this should work:
AND
Name IN (Select Names FROM Person Where Names LIKE
ISNULL( NULLIF( #Name_, '' ), '%' ))
AND
This works
DECLARE #Name NVARCHAR(100)
SET #Name = ''
DECLARE #Person TABLE ( NAME NVARCHAR(100) )
INSERT INTO #Person VALUES ( 'fred' )
INSERT INTO #Person VALUES ( 'jo' )
DECLARE #Temp TABLE
(
id INT ,
NAME NVARCHAR(100)
)
INSERT INTO #Temp ( id, NAME ) VALUES ( 1, N'' )
INSERT INTO #Temp ( id, NAME ) VALUES ( 5, N'jo' )
INSERT INTO #Temp ( id, NAME ) VALUES ( 2, N'fred' )
INSERT INTO #Temp ( id, NAME ) VALUES ( 3, N'bob' )
INSERT INTO #Temp ( id, NAME ) VALUES ( 4, N'' )
SELECT * FROM #Temp
WHERE name IN ( SELECT name
FROM #Person
WHERE name = CASE WHEN #name != '' THEN #Name
ELSE name
END )
You should almost definitely use an IF statement with two selects. e.g.
IF #Name IS NULL
BEGIN
SELECT *
FROM Person
END
ELSE
BEGIN
SELECT *
FROM Person
--WHERE Name LIKE '%' + #Name + '%'
WHERE Name = #Name
END
N.B. I've changed like to equals since LIKE without wildcards it is no different to equals,
, it shouldn't make any difference in terms of performance, but it stops ambiguity for the next person that will read your query. If you do want non exact
matches then use the commented out WHERE and remove wildcards as required.
The reason for the IF is that the two queries may have very different execution plans, but by combining them into one query you are forcing the optimiser to pick one plan or the other. Imagine this schema:
CREATE TABLE Person
( PersonID INT IDENTITY(1, 1) NOT NULL,
Name VARCHAR(255) NOT NULL,
DateOfBirth DATE NULL
CONSTRAINT PK_Person_PersonID PRIMARY KEY (PersonID)
);
GO
CREATE NONCLUSTERED INDEX IX_Person_Name ON Person (Name) INCLUDE (DateOfBirth);
GO
INSERT Person (Name)
SELECT DISTINCT LEFT(Name, 50)
FROM sys.all_objects;
GO
CREATE PROCEDURE GetPeopleByName1 #Name VARCHAR(50)
AS
SELECT PersonID, Name, DateOfBirth
FROM Person
WHERE Name IN (SELECT Name FROM Person WHERE Name LIKE ISNULL(#Name, Name));
GO
CREATE PROCEDURE GetPeopleByName2 #Name VARCHAR(50)
AS
IF #Name IS NULL
SELECT PersonID, Name, DateOfBirth
FROM Person
ELSE
SELECT PersonID, Name, DateOfBirth
FROM Person
WHERE Name = #Name;
GO
Now If I run the both procedures both with a value and without:
EXECUTE GetPeopleByName1 'asymmetric_keys';
EXECUTE GetPeopleByName1 NULL;
EXECUTE GetPeopleByName2 'asymmetric_keys';
EXECUTE GetPeopleByName2 NULL;
The results are the same for both procedures, however, I get the same plan each time for the first procedure, but two different plans for the second, both of which are much more efficient that the first.
If you can't use an IF (e.g if you are using an inline table valued function) then you can get a similar result by using UNION ALL:
SELECT PersonID, Name, DateOfBirth
FROM Person
WHERE #Name IS NULL
UNION ALL
SELECT PersonID, Name, DateOfBirth
FROM Person
WHERE Name = #Name;
This is not as efficient as using IF, but still more efficient than your first query. The bottom line is that less is not always more, yes using IF is more verbose and may look like it is doing more work, but it is in fact doing a lot less work, and can be much more efficient.

How to use Split function in stored procedure?

I am selecting the values like below:
select browser, firstname, lastname from details
Here the browser value will be like this:
33243#Firefox#dsfsd
34234#google#dfsd
And separately I have used split function for single value as below:
select * from dbo.split('33243#Firefox#dsfsd','#')
Result is:
items
===========
33243
firefox
dsfsd
So I have used split function like below
select split(browser, '#'), firstname, lastname from details
but its not working...
What I need is
33243#Firefox#dsfsd
instead of displaying the value like this into the grid,
have to display only Firefox into the grid.
Since you know you want the second element each time, you could write a function like this:
CREATE FUNCTION [dbo].[fn_SplitElement]
(
#inputString nvarchar(2000), --The input string
#elem int, --The 1-based element index to return,
#delimiter nvarchar(1) --The delimiter char
)
RETURNS nvarchar(2000)
AS
BEGIN
-- Declare the return variable here
DECLARE #result nvarchar(2000)
-- Add the T-SQL statements to compute the return value here
SELECT #result = value
FROM
(
SELECT *,ROW_NUMBER() OVER(ORDER By Position) as rownum FROM dbo.split(#inputString,#delimiter)
) as t
WHERE rownum=#elem
-- Return the result of the function
RETURN #result
END
GO
Then you can call:
select [dbo].[fn_SplitElement](browser,2,'#') as 'BrowserName', firstname, lastname from details
You do not state which version of SQL Server you are using or which split function. However, most split functions return a table rather than rows so you'll need to use a JOIN to join the two tables (the first is the split, the second is the rest of the fields).
For more information in SQL split functions, see Erland Sommarskog's page at http://www.sommarskog.se/arrays-in-sql.html .
You can Create a Split function and that will be use when you want.
CREATE FUNCTION [dbo].[Split]
(
#List nvarchar(max),
#SplitOn nvarchar(1)
)
RETURNS #RtnValue table (
Id int identity(1,1),
Value nvarchar(max)
)
AS
BEGIN
While (Charindex(#SplitOn,#List)>0)
Begin
Insert Into #RtnValue (value)
Select
Value = ltrim(rtrim(Substring(#List,1,Charindex(#SplitOn,#List)-1)))
Set #List = Substring(#List,Charindex(#SplitOn,#List)+len(#SplitOn),len(#List))
End
Insert Into #RtnValue (Value)
Select Value = ltrim(rtrim(#List))
Return
END
after that you can call the function in your query as below.
SELECT * FROM anotherTable WHERE user_id IN(dbo.split(#user_list,','))
I Have used below query instead of using split function.. its working perfectly..
select
SUBSTRING(SUBSTRING(browser, CHARINDEX ('#', browser)+1,LEN(browser)-charindex('#', browser)),
0,
CHARINDEX('#',SUBSTRING(browser, CHARINDEX ('#', browser)+1,LEN(browser)-CHARINDEX('#', browser)))),
firstname, lastname from details

Ordering SQL Server results by IN clause

I have a stored procedure which uses the IN clause. In my ASP.NET application, I have a multiline textbox that supplies values to the stored procedure. I want to be able to order by the values as they were entered in the textbox. I found out how to do this easily in mySQL (using FIELD function), but not a SQL Server equivalent.
So my query looks like:
Select * from myTable where item in #item
So I would be passing in values from my application like '113113','112112','114114' (in an arbitrary order). I want to order the results by that list.
Would a CASE statement be feasible? I wouldn't know how many items are coming in the textbox data.
How are you parameterising the IN clause?
As you are on SQL Server 2008 I would pass in a Table Valued Parameter with two columns item and sort_order and join on that instead. Then you can just add an ORDER BY sort_order onto the end.
From KM's comment above...
I know you didn't state it is comma seperated, but if it was a CSV or even if you have it space seperated you could do the following.
DECLARE #SomeTest varchar(100) --used to hold your values
SET #SomeTest = (SELECT '68,72,103') --just some test data
SELECT
LoginID --change to your column names
FROM
Login --change to your source table name
INNER JOIN
( SELECT
*
FROM fn_IntegerInList(#SomeTest)
) n
ON
n.InListID = Login.LoginID
ORDER BY
n.SortOrder
And then create fn_IntegerInList():
CREATE FUNCTION [dbo].[fn_IntegerInList] (#InListString ntext)
RETURNS #tblINList TABLE (InListID int, SortOrder int)
AS
BEGIN
declare #length int
declare #startpos int
declare #ctr int
declare #val nvarchar(50)
declare #subs nvarchar(50)
declare #sort int
set #sort=1
set #startpos = 1
set #ctr = 1
select #length = datalength(#InListString)
while (#ctr <= #length)
begin
select #val = substring(#InListString,#ctr,1)
if #val = N','
begin
select #subs = substring(#InListString,#startpos,#ctr-#startpos)
insert into #tblINList values (#subs, #sort)
set #startpos = #ctr+1
end
if #ctr = #length
begin
select #subs = substring(#InListString,#startpos,#ctr-#startpos)
insert into #tblINList values (#subs, #sort)
end
set #ctr = #ctr +1
set #sort = #sort + 1
end
RETURN
END
This way your function creates a table that holds a sort order namely, SortOrder and the ID or number you are passing in. You can of course modify this so that you are looking for space rather then , values. Otherwise Martin has the right idea in his answer. Please note in my example I am using one of my tables, so you will need to change the name Login to whatever you are dealing with.
the same way you concatenate ('113113','112112','114114') to pass to the sql sentence in the where clausule you can concatenate
order by
case item
when '113113' then 1
when '112112' then 2
when '114114' then 3
end
to pass to your order by clausule

Cannot implicitly convert type 'System.Data.Linq.ISingleResult<CustomeProcedureName> to 'int'

Sorry for this simple question .
I have a Stored Procedure that return an int value , I'm trying to call this sp from my asp.net linq to sql project .
int currentRating = db.sproc_GetAverageByPageId(pageId);
But i get this error :
Cannot implicitly convert type `'System.Data.Linq.ISingleResult<PsychoDataLayer.sproc_GetAverageByPageId> to 'int' .`
Edit 1
The solution that friends implied didn't work . All the time it return 0
For more information i put my stored procedure here :
ALTER procedure [dbo].[sproc_GetAverageByPageId](
#PageId int )
as
select (select sum(score) from votes where pageId = #PageId)/(select count(*) from votes where pageId=#PageId)
You should inspect the ReturnValue property.
Perhaps the following works better?
int currentRating = (int)db.sproc_GetAverageByPageId(pageId).ReturnValue;
Update: since your stored proc returns a resultset instead of using a return statement the actual data will be available as an element in the enumerable returned by db.sproc_GetAverageByPageId(pageId). If you inspect the ISingleResult<T> type, you'll see that it inherits IEnumerable<T> which indicates that you can enumerate the object to get to the data, each element being of type T.
Since the sproc does a SELECT SUM(*) ... we can count on the resultset to always contain one row. Thus, the following code will give you the first (and only) element in the collection:
var sumRow = db.sproc_GetAverageByPageId(pageId).Single();
Now, the type of sumRow will be T from the interface definition, which in your case is PsychoDataLayer.sproc_GetAverageByPageId. This type hopefully contains a property that contains the actual value you are after.
Perhaps you can share with us the layout of the PsychoDataLayer.sproc_GetAverageByPageId type?
Looks like you're actually after the ReturnValue. You may need to cast it to System.Data.Linq.ISingleResult if it isn't already, then cast ReturnValueto int.
This is actually returning an ISingleResult
int currentRating = (int) db.sproc_GetAverageByPageId(pageId).ReturnValue;
Change your sp to :
ALTER procedure [dbo].[sproc_GetAverageByPageId](
#PageId int )
as
return (select sum(score) from votes where pageId = #PageId)/(select count(*) from votes where pageId=#PageId)
one more thing you can do:
ALTER procedure [dbo].[sproc_GetAverageByPageId](#PageId int ) as
select (select sum(score) from votes where pageId = #PageId)/(SELECT * FROM votes where pageId=#PageId)
WRITE >>
"select * From"<< instead of "select Count(*)"
select (select sum(score) from votes where pageId = #PageId)/(SELECT * FROM votes where pageId=#PageId)
and after that:
int currentRating = (int)db.sproc_GetAverageByPageId(pageId).count();

Resources