Function to check value and returns result - sql-server

I need to create function in SQL to check data in variables or parameters as below
#Category as varchar(50)='ABC,DEF'
#Value as varchar(50)='1,2'
And compare #Category value with Category in table then return value matching from parameter
JOB TABLE ---
JOB CATEGORY
123 ABC
234 DEF
234 SSS
Select JobNo,FUNCTION(#Category,#Value,CATEGORY) from JOB
FINAL RESULTS
JOB VALUE
123 1
234 2
234 0
If category match then return value from parameter else return 0.

If you can't use a static lookup table as mentioned in the comments (for example, perhaps the mapping needs to be dynamic based on data supplied by the application), then this looks like a job for a table valued parameter.
Right now you have two parameters, but it seems to me that the values in the parameters are related. That is to say, right now you have #category = 'ABC,DEF' and #value = '1,2', but I think you intend each "element" in the comma delimited set of "categories" to associate with the "element" in the comma delimiited set of "values" that is in the same position.
Right now the design is brittle, because what would happen if I use parameters like this: #category = 'ABC,DEF,GHI,JKL', #value = '1'?
So, you can make your code more durable, and use the sort of "join-based" lookup table solution being recommended to you in the comments, by using a function that takes a table valued parameter argument. To do this, you first have to create the table valued parameter as a type in your database. We then accept a parameter of that type, and join onto it. In the solution below I have "guessed" at datatypes for category and value that seem reasonable based on the sample data in your question.
Also, I've kept the "structure" of your solution - ie, the function is written in such a way that it can be "applied" against every row in jobs, individually. We don't have to do it this way. We could instead just do the whole query inside the function (ie, including the join to the job table), but perhaps you want to use the function against other tables that also have a cateogry column? I won't try to second guess the overall design here. But I will switch the function to an inline table valued function (which returns one row with one column) rather than a scalar function, for performance reasons.
-- schema and data to match your question
create table dbo.job (job int, category char(3));
insert dbo.job(job, category) values (123, 'ABC'), (234, 'DEF'), (234, 'SSS');
go
-- solution
create type dbo.CategoryValues as table
(
category char(3) unique,
[value] int
);
go
create or alter function dbo.MapCategory(#category char(3), #map dbo.CategoryValues readonly)
returns table as return
(
select [value] from #map where category = #category
);
go
-- to call the function we need to pass a parameter of type
-- dbo.CategoryValues with the mappings we desire
declare #map dbo.CategoryValues;
insert #map values ('ABC', 1), ('DEF', 2)
-- outer apply the function to each row in the job table.
select j.job, [value] = isnull(v.[value], 0)
from dbo.job j
outer apply dbo.MapCategory(j.category, #map) v

Related

How to overcome "iif" limitations with subqueries?

I have 2 tables (A and B). I want to implement such logic to table B:
[Bank] AS (iif( Some_Statement,
(SELECT [Money] FROM [ATM] WHERE [B].[Currency] = [A].[Currency] ),
[NoMoney]
)
)
I get an error: Subqueries are not allowed in this context. Only scalar expressions are allowed.
Is there a way to implement such logic on creation of the tables? It doesn't look hard.
After reading the discussion in comments I get the feeling, that this was going the wrong direction. Might be because of your very direct question about IIF()...
If I get this correctly you try to add a computed column to you table. Something along this:
DECLARE #tblTest TABLE (ID INT IDENTITY
,SomeValue VARCHAR(100)
,[Test] AS IIF(SomeValue IS NOT NULL, CONCAT(SomeValue,'Blah'), 'Default if null'));
INSERT INTO #tblTest(SomeValue) VALUES('Test');
INSERT INTO #tblTest(SomeValue) VALUES(NULL);
SELECT * FROM #tblTest;
The result
ID SomeValue Test
1 Test TestBlah
2 NULL Default if null
Now you want to get the value for the computed column not just from some simple scalar computation, but you want to pick it from a table.
Here I will try to simulate your issue. Next time it is up to you to do this yourself. Providing DDL, sample data and the expected output together with your own attempts is the best chance to get the answer you are waiting for.
This is the table B from where you want to get the value.
Later we will ask for 'Test' and not for 'xyz'.
CREATE TABLE tblB (ID INT IDENTITY
,SomeResultColumn VARCHAR(100)
,SomeConditionColumn VARCHAR(100));
INSERT INTO tblB(SomeResultColumn,SomeConditionColumn) VALUES('not wanted','xyz')
,('wanted','Test');
GO
--This is what you are trying to do, but I out-commented it because of the error you've got.
--A computed column does not allow for a SELECT. This is not bound to the IIF()
--CREATE TABLE tblA (ID INT IDENTITY
-- ,SomeValue VARCHAR(100)
-- ,[Test] AS IIF(SomeValue IS NOT NULL,(SELECT b.SomeResultColumn FROM tblB b WHERE b.SomeConditionColumn=SomeValue),'Default if null'));
--GO
--But what we can do - and the error message is telling so - provide a scalar expression:
--A Scalar Function is exactly this:
CREATE FUNCTION dbo.GetMyComputedColumn(#Condition VARCHAR(100))
RETURNS VARCHAR(100) AS
BEGIN
RETURN (SELECT b.SomeResultColumn --You might use `TOP 1` to ensure a scalar result
FROM tblB b
WHERE b.SomeConditionColumn=#Condition);
END
GO
--We can use this function in the IIF():
CREATE TABLE tblA (ID INT IDENTITY
,SomeValue VARCHAR(100)
,[Test] AS IIF(SomeValue IS NOT NULL,dbo.GetMyComputedColumn(SomeValue),'Default if null'));
GO
INSERT INTO tblA(SomeValue) VALUES('Test');
INSERT INTO tblA(SomeValue) VALUES(NULL);
SELECT * FROM tblA;
The result
ID SomeValue Test
1 Test wanted
2 NULL Default if null
Should you do this?
The question, if this is a good idea, is something completely different.
Besides the fact, that scalar functions are known as bad performers, the main question is: WHY?
Do you need a persistant default value? In this case a trigger will be a better choice... Or you can use an insert statement with the computed value directly. I'm afraid you are mixing the concepts of computed columns and a default constraint. The computed column cannot be changed...
If you want to compute this value whenever you fetch data from this table, it was much better to use a VIEW or an iTVF, where you simply join the needed value to your result set.
If what you are saying is that you want a value from table "B" IF it matches on a value from your main table "A" and if there is no matching value in "B", use a literal as a default, try this:
select
Bank,
coalesce( (SELECT Money FROM ATM WHERE Currency = A.Currency), 'NoMoney' ) as 'Type'
from
ATM A
where
....
If the select of [Money] returns null, the literal 'NoMoney' will be used.
If need be, you can have a select on both sides of the coalesce, the right need not be a literal.

T-SQL joining external variables into tables

I've come from an application dev and been thrust into the web dev and I'm getting my head around asymmetrical data requests/returns and how to handle them.
I need to make a number of SQL requests and though the best way to manage which ones are returned would be to insert a UUID or something similar into the return sql table.
Also, in general I'm pretty basic with my sql language, but I want to add an external value into my returned table, where #ext would be the external data added in from the original request.
SELECT *
FROM
#ext AS uuid,
dbo.Orders
WHERE ....
expected return table
uuid: 12234
customer: jack
orderNo: 774
postAddy: 123 Albert St
...
The error I'm always getting is "but declare the table variable "#ext".
Is this the right approach or am I just doing something dumb?
The error message you are getting is telling you that you haven't declared the table variable #ext. This is because you've used a variable name (with the # prefix) in the FROM clause where it's expecting a table or other table-like object (ie. table, view, table variable, TVF, etc).
The #ext variable appears to be a scalar (single-valued) variable, so it isn't recognised in the FROM clause. You should try something like this instead:
SELECT
-- scalar values and column names / aliases go here
#ext AS uuid, *
FROM
-- only tables, views, table variables, TVF's etc go here
dbo.Orders
WHERE ....
Note that if your query returns multiple rows, they will all have the same value for uuid. This may or may not be desirable, and there may be better ways to achieve what you want, in terms of managing the data that is returned from multiple queries, but this is best posed in another question once you have a working example.
Make sure you know what #ext is for you and how to properly reference it.
If it's a sacalar value you can use it on expressions:
DECLARE #ext INT = 5
SELECT
#ext AS ScalarValue,
#ext + 10 AS ScalarOperation,
#ext + S.SomeColumn AS ScalarOperationWithTableColumn
FROM
SomeTable AS S
If it's a table variable, you can reference it as table (as in your example):
DECLARE #ext TABLE (
FirstValue INT,
SecondValue VARCHAR(100))
INSERT INTO #ext (
FirstValue,
SecondValue)
VALUES
(10, 'SomeText'),
(20, 'AnotherText')
SELECT
E.FirstValue,
E.SecondValue
FROM
#ext AS E
/*
LEFT JOIN ....
WHERE
....
*/

SQL Server Sub query with multiple results

I am attempting to populate an existing bridge table from a table that exists as a template for how items are populated into the bridge table.
I am attempting to do a new insert into the bridge table (dbo.ECN_ChecklistItem) where the list of items from dbo.ECN_ChecklistItem (SELECTed by ECNID (a foreign key ID)) differs from dbo.BusinessUnit_ChecklistItem (SELECTed by BusinessUnit and InUse).
I get a SQL exception stating that "Sub query returned more than 1 value. This is not permitted when the sub query follows follows =, !=, <, <= , >, >= or when the subquery is used as an expression."
I have attempted to modify the query to include an 'in' operator rather than a '=' operator, however that was not successful either. I may have done it incorrectly.
CREATE PROCEDURE checkCurrentChecklistItemsAgainstInUseItems
--Parameters
#ECNID INT, --I need the ECNID to do the search properly.
#BU NVARCHAR(50),
#Date NVARCHAR(10), --This is the date that the checklist item was added to the ECN.
#NewStatus NVarchar(3) --This is based off of the status of the ECN, will either be '?' or 'No'
AS
BEGIN
DECLARE #System NCHAR(6)='SYSTEM'
DECLARE #Message NVARCHAR(50) = 'This value was added by the system automatically.'--Not in use.
SET NOCOUNT ON;
INSERT INTO dbo.ECN_ChecklistItem (ECNID, Required, LogBy, LogDate, Description, CheckName)
VALUES(#ECNID, #NewStatus, #System, #Date, NULL,
(Select ChecklistItem FROM dbo.BusinessUnit_ChecklistItem WHERE BusinessUnit Like #BU
EXCEPT SELECT CheckName FROM dbo.ECN_ChecklistItem WHERE ECNID Like #ECNID))
END
GO
The rest of the Insert Statement is towards a single row. However, the subquery might be returning (or has the possibility of returning) multiple rows. Whenever this situation occurs, SQL Server puts up this.
You might want to examine, by running the subquery separately as a query to find whether multiple rows are being returned even as you "assume" that there will be only row returned.
If it does indeed multiple rows, then you will need to handle that situation separately depending on your logic.
However, if you are sure only one row is being returned, you can always a MAX or MIN or TOP 1 . Although they are inconsequential when a single row is being returned, they will avoid the 'subquery returned mutliple' error.
Edit :
If you indeed want multiple rows to be inserted, replace the Values phrase, with the complete select phrase, with values for all the other fields remaining as fixed values (# ones), and the value for CheckName coming from the select statement. Select #..., #..., #...., .... CheckListItem From .... Where BusinessUnit Like ... Except... and so on.

Creating a function using PL/SQL

I have the following table:
create table movie(
movie_id integer primary key,
title varchar(500) not null,
kind varchar(30),
year integer not null
);
I want to create a function:
addMovie(title, kind, year)
The function must insert a new row into the movie table with a unique movie_id, and then return the movie_id.
This is the first time I'm using PL/SQL, so I'm kind of lost at the moment - couldn't find any (good) examples so far.
Thanks!
Your function needs to do 3 things
Generate the unique movie id
Insert into the table
Return the generated id
Let's take it one step at a time
Generate the unique movie id
The best way to do it is to use a seqeuence which will generated a id for you. Look up on sequences
Insert into the table
This is done by a straightforward insert. Since the movie id is generated by the sequence, we use sequence_name.nextval in the insert statement. Thus the insert statement looks like
INSERT INTO movie(movie_id, title, kind, year) values (movie_id_seq.nextval, title, kind, year)
Return the generated id back
You can make use of the Returning clause in a DML to return the generated id back into a variable. And then use the RETURN statement to return the value back.
So this is how your function will look like
FUNCTION addmovie(p_title,
p_kind,
p_year)
RETURN NUMBER
IS
v_id movie.id%TYPE;
BEGIN
INSERT INTO movie
(
movie_id,
title,
kind,
year
)
VALUES
(
movie_id_seq.NEXTVAL,
p_title,
p_kind,
p_year
)
returning id
INTO v_id;
RETURN v_id;
END;
Note that this is a fairly basic function, with no error checking, exception handling - I'll leave it up to you.
Note that max(movie_id)+1 isn't the best way forward, for purposes of the assignment. You'll need
SELECT max(movie_id)+1 into v_id from movies;
before the insert statement.
Also, because of the DML, you can't use this function as part of a query.

Ordering SQL Results based on Input Params

In conjunction with the fn_split function, I'm returning a list of results from a table based on comma separated values.
The Stored Procedure T-SQL is as follows:
SELECT ProductCode, ProductDesc, ImageAsset, PriceEuros, PriceGBP, PriceDollars,
replace([FileName],' ','\_') as [filename],
ID as FileID, weight
from Products
LEFT OUTER JOIN Assets on Assets.ID = Products.ImageAsset
where ProductCode COLLATE DATABASE_DEFAULT IN
(select [value] from fn\_split(#txt,','))
and showOnWeb = 1
I pass in to the #txt param the following (as an example):
ABC001,ABC009,ABC098,ABC877,ABC723
This all works fine, however the results are not returned in any particular order - I need the products returning in the 'SAME ORDER' as the input param.
Unfortunately this is a live site with a built schema, so I can't change anything on it (but I wish I could) - otherwise I would make it more sensible.
If all of the references that are passed in on the #txt param are unique you could use CharIndex to find their position within the param e.g.
order by charindex(ProductCode, #txt)
In the stored procedure, I would create a table which has a numeric key which is the PK for the temp table and set to auto-increment. Then, I would insert the results of fn_split into that table, so that you have the parameters as they are ordered in the list (the auto-increment will take care of that).
Then, join the result set to this temp table, ordering by the numeric key.
If the entries in the parameter list are not unique, then after inserting the records into the temp table, I would delete any duplicates, except for the record where the id is the minimum for that particular parameter.

Resources