UDF that accepts one parameter and returns a table - sql

Question: Create a UDF that accepts the State Province and returns the associated Sales Tax Rate, StateProvinceCode and CountryRegionCode (Database is AdventureWorks2012)
Name took the place of State Province because there is no column
called 'State Province', and 'Name' contains the needed information.
This is what I did in the code below.
Is there another way to run this?
In this form Ambiguous column name keeps showing up.
CREATE FUNCTION fx_TxSpCr (#Name Nvarchar(50),
#TaxRate Smallmoney,
#StateProvinceCode Nchar(3),
#CountryRegionCode Nvarchar(3)
)
RETURNS TABLE
AS
RETURN (
SELECT Name,TaxRate,StateProvinceCode,CountryRegionCode
FROM Sales.SalesTaxRate s
JOIN Person.StateProvince t
ON s.StateProvinceID=t.StateProvinceID
JOIN Sales.SalesTerritory u
ON t.TerritoryID=u.TerritoryID
)
SELECT s.TaxRate,t.StateProvinceCode,u.CountryRegionCode
FROM fx_TxSpCr
GROUP BY Name

Thank you both for the contribution. This worked:
CREATE FUNCTION fx_TaxStCtry(#StateProvince nvarchar(50))
RETURNS #TaxStCtry TABLE
(TaxRate SmallMoney not null,
StateProvinceCode Nchar(3) not null ,
CountryRegionCode Nvarchar(3) not null)
AS
BEGIN INSERT #TaxStCtry
SELECT tr.TaxRat,sp.StateProvinceCode,sp.CountryRegionCode
FROM Person.StateProvince sp
JOIN Sales.SalesTaxRate tr
ON tr.stateprovinceid = sp.stateprovinceid
WHERE sp.Name=#StateProvince
RETURN
END

The ambiguity is coming from the lack of aliases in the select statement from your function. It seems like you are trying to use the aliases in the execution of the function, but they actually need to be contained within the function itself. (For example, all three tables have a "name" column.)
After correcting that issue you will find that you actually have not used any of the parameters you have set up for your function. Which does not have to be the full return set of the select statement contained within.
The correct path here is to start with a select statement that returns what you're needing and then turn that into a function. If the data is not being returned at a base level, then you're not ready to start creating a function.

Related

Getting error while running function using T-SQL

I have created small table value function using T-SQL which takes one input parameter phone number as and returns area code from it. Function compiles successfully but when I run it I am getting error:
Msg 4121, Level 16, State 1, Line 1776
Cannot find either column "dbo" or the user-defined function or aggregate "dbo.getareacode1", or the name is ambiguous
Refer to my code:
create or alter function dbo.getareacode1 (#phoneno as nvarchar(20))
returns table
with schemabinding
as
return
(select top 1
#phoneno as PhoneNumber, value as AreaCode
from
string_split(#phoneno,'-')
);
select dbo.getareacode1( N'323-234-2111');
First off, the order of the rows returned from STRING_SPLIT is not guaranteed, so this function broken to begin with.
But the error is caused by trying to invoke a Table-Valued Function where a scalar is expected.
A TVF cannot be treated like a scalar function. So
create or alter function dbo.getareacode1 (#phoneno as nvarchar(20))
returns table
with schemabinding
as
return
(
select top 1 #phoneno as PhoneNumber, value as AreaCode
from string_split(#phoneno,'-')
);
go
select * from dbo.getareacode1( N'323-234-2111');
And if you want to call it from another query use CROSS APPLY, eg
with phoneNumbers as
(
select N'323-234-2111' phoneno
from sys.objects
)
select ac.*
from phoneNumbers p
cross apply dbo.getareacode1(p.phoneno) ac;
On the flip-side of what David said: for your function to work like you were expecting you will need a scalar user-defined function (udf). This function:
-- Create function
create or alter function dbo.getareacode1 (#phoneno as nvarchar(20))
returns nvarchar(20) with schemabinding as
begin
return
(
select top (1) AreaCode = ss.[value]
from string_split(#phoneno,'-') ss
);
end
GO
... will allow this query to work:
select dbo.getareacode1( N'323-234-2111');
All that said, I strongly advise against using scalar udfs. I'll include a a couple good links that explain why at the bottom of this post. Leaving your function as-is and using APPLY, as David demonstrated, is the way to go. Also, string_split is not required here. If phone numbers are always coming in this format: NNN-NNN-NNNN, you could just use SUBSTRING:
SUBSTRING(N'323-234-2111', 1, 3).
For countries with varible-length area codes in the format (area code(2 or more digits))-NNN-NNNN you could do this:
declare #phoneno nvarchar(30) = N'39-234-2111'; -- Using a variable for brevity
select substring(#phoneno,1, charindex('-',#phoneno)-1);
If you, for whatever reason, really need a function, then this is how I'd write it:
-- using the variable-length requirement as an example
create or alter function dbo.getareacode1_itvf(#phoneno as nvarchar(20))
returns table with schemabinding as
return (select areacode = substring(#phoneno,1, charindex('-',#phoneno)-1));
go
Great articles about Scalar UDFs and why iTVFs are better:
How to Make Scalar UDFs Run Faster -- Jeff Moden
Scalar functions, inlining, and performance -- Adam Machanic
TSQL Scalar functions are evil – Andy Irving -- Stackoerflow post

Table-valued function that takes table as parameter and performs join

I use SQL server 2016.
I want to write a function that takes a table as a parameter and then performs a join on that table with another table.
I have declared the following type:
CREATE TYPE WorklistTable AS TABLE (WorklistId int NOT NULL)
Then I use it in a lot of functions that do selects based on certain conditions
CREATE FUNCTION [dbo].[fnGetSomeData] (
#WorklistIds WorklistTable readonly
)
RETURNS TABLE
AS RETURN
(
select WorklistId, wlu.UserId
from #WorklistIds
join [dbo].[WorklistUser] wlu on wlu.WorklistId = #WorklistIds.worklistId
-- the rest is omitted
);
I get the following error:
Must declare the scalar variable "#WorklistIds".
I tried to declare the variable, but I got an error:
The variable name '#WorklistIds' has already been declared. Variable names must be unique within a query batch or stored procedure.
You should use aliases when you are joing to table variable.
CREATE FUNCTION [dbo].[fnGetSomeData] (
#WorklistIds WorklistTable readonly
)
RETURNS TABLE
AS RETURN
(
select WorklistId, wlu.UserId
from #WorklistIds t
join [dbo].[WorklistUser] wlu on wlu.WorklistId = t.worklistId
-- the rest is omitted
);
You can't directly use the #Table name when referencing a column within a table variable. You either need to alias the table or wrap it in square brackets:
select WorklistId, wlu.UserId
from #WorklistIds As W
join [dbo].[WorklistUser] wlu on wlu.WorklistId = W.worklistId
Or
select WorklistId, wlu.UserId
from #WorklistIds
join [dbo].[WorklistUser] wlu on wlu.WorklistId = [#WorklistIds].worklistId

How to parse a VARCHAR passed to a stored procedure in SQL Server?

I have two tables tbl_Products and tbl_Brands, both are joined on BrandId.
I have a stored procedure which should return all the products belong to the brand ids passed to it as parameter.
My code is as follows.
create proc Sp_ReturnPrdoucts
#BrandIds varchar(500) = '6,7,8'
AS
BEGIN
SELECT *
FROM tbl_Products as p
JOIN tbl_Brands b ON p.ProductBrandId = b.BrandId
WHERE b.BrandId IN (#BrandIds)
END
But this is giving error as BrandId is INT and #BrandIds is VARCHAR
When I hard code it this way as follows it works fine and returns the desired data from db ..
create proc Sp_ReturnPrdoucts
#BrandIds varchar(500) = '6,7,8'
AS
BEGIN
SELECT *
FROM tbl_Products AS p
JOIN tbl_Brands b ON p.ProductBrandId = b.BrandId
WHERE b.BrandId IN (6,7,8)
END
Any help :)
If possible, don't use varchar for this kind of things, use a table valued parameter instead.
To use a tabled value parameter you should first declare a user defined table type:
CREATE TYPE IntList As Table
(
IntValue int
)
Then change your stored procedure to accept this variable instead of the nvarchar:
create proc Sp_ReturnPrdoucts
#BrandIds dbo.IntList readonly -- Note: readonly is a must!
AS
BEGIN
SELECT *
FROM tbl_Products as p
join tbl_Brands b on p.ProductBrandId=b.BrandId
join #BrandIds ON(b.BrandId = IntValue)
END
The problem is that the IN() operator expects a list of variables separated by commas, while you provide a single variable that it's value is a comma separated string.
If you can't use a table valued parameter, you can use a string spliting function in sql to convert the value of the varchar to a table of ints. there are many splitters out there, I would recommend reading this article before picking one.
Another alternative is to use 'indirection' (as I've always called it)
You can then do..
create proc Sp_ReturnPrdoucts
#BrandIds varchar(500) = '6,7,8'
AS
BEGIN
if (isnumeric(replace(#BrandIds,',',''))=1)
begin
exec('SELECT * FROM tbl_Products as p join tbl_Brands b on p.ProductBrandId=b.BrandId WHERE b.BrandId IN ('+#BrandIds+')')
end
END
This way the select statement is built as a string, then executed.
I've now added validation to ensure that the string being passed in is purely numeric (after removing all the commas)

SQL query help - declaration of variables within a function

I'm trying to write a SQL function but an having problems with declaring the variables I need for use in the WHERE clause.
Here's the code:
CREATE FUNCTION fn_getEmployeePolicies(#employeeid smallint)
RETURNS TABLE
AS
DECLARE #empLoc varchar
DECLARE #empBusA varchar
DECLARE #empType varchar
#empLoc = SELECT Location FROM fn_getEmployeeDetails(#employeeid)
#empBusA = SELECT BusinessArea FROM fn_getEmployeeDetails(#employeeid)
#empType = SELECT Type FROM fn_getEmployeeDetails(#employeeid)
RETURN select PolicyId, PolicyGroupBusinessArea.BusinessArea, policysignoff.PolicyGroupLocation.Location, policysignoff.PolicyGroupEmployeeType.EmployeeType
from policysignoff.PolicyGroupPolicy
LEFT JOIN policysignoff.PolicyGroupBusinessArea on policysignoff.PolicyGroupBusinessArea.PolicyGroupId=policysignoff.PolicyGroupPolicy.PolicyGroupId
LEFT JOIN policysignoff.PolicyGroupLocation on policysignoff.PolicyGroupLocation.PolicyGroupId=policysignoff.PolicyGroupPolicy.PolicyGroupId
LEFT JOIN policysignoff.PolicyGroupEmployeeType on policysignoff.PolicyGroupEmployeeType.PolicyGroupId=policysignoff.PolicyGroupPolicy.PolicyGroupId
where BusinessArea = #empBusA
AND EmployeeType = #empType
AND Location = #empLoc
GO
The logic I am trying to build in is:
'given an employeeId, return all "applicable" policies'
An "Applicable" policy is one where the Business Area, Location and EmployeeType match that of the user.
I am trying to use another function (fn_getEmployeeDetails) to return the BusArea, Loc & EmpType for the given user.
Then with the results of that (stored as variables) I can run my select statement to return the policies.
The problem i am having is trying to get the variables declared correctly within the function.
Any help or tips would be appreciated.
Thanks in advance!
Without knowing what your error actually is, I can only say that you're properly not after using varchar as datatype without specifying length.
DECLARE #empLoc varchar will declare a varchar with length 1.
Chances are it should be something like varchar(255) or similar.
Second to set variables you'll either need to use SET and use paranthisis for selects or set it into the statement:
SET #empLoc = (SELECT Location FROM fn_getEmployeeDetails(#employeeid))
or
SELECT #empLoc = Location FROM fn_getEmployeeDetails(#employeeid)
There are subtle differences between these two methods, but for your purpose right now I don't think it's important.
EDIT:
Based on your comment you lack a BEGIN after AS, and an END before GO.
Basically - your function syntax is mixing up "inline" table function with "multi-statement" function.
Such a function "template" should look something like this:
CREATE FUNCTION <Table_Function_Name, sysname, FunctionName>
(
-- Add the parameters for the function here
<#param1, sysname, #p1> <data_type_for_param1, , int>,
<#param2, sysname, #p2> <data_type_for_param2, , char>
)
RETURNS
<#Table_Variable_Name, sysname, #Table_Var> TABLE
(
-- Add the column definitions for the TABLE variable here
<Column_1, sysname, c1> <Data_Type_For_Column1, , int>,
<Column_2, sysname, c2> <Data_Type_For_Column2, , int>
)
AS
BEGIN
-- Fill the table variable with the rows for your result set
RETURN
END
GO
(script taken from sql server management studio)

How do I make a procedure that sums values stored in nested tables?

I have the following tables:
CREATE TABLE bodega ( --winery
id_bod INTEGER NOT NULL,
prod_an_bod nt_tipo_valor , --annual production
)
CREATE TABLE marca ( --wine
id_marca INTEGER NOT NULL,
prod_an_marca nt_tipo_valor , --annual production
)
CREATE TABLE presentacion ( --n:m relation table
id_pres INTEGER NOT NULL,
bodega_fk INTEGER NOT NULL,
marca_fk INTEGER NOT NULL
)
Both prod_an_marca in the table marca and prod_an_bod in the table bodega are nested tables of the following type:
CREATE OR REPLACE TYPE tipo_valor AS OBJECT (
ano DATE, --year
cantidad INTEGER --ammount of wine produced
)
I've made the following procedure which is supposed to retrieve the ammount of wine produced by any given winery in any given year, the purpose of this is to later insert that ammount into the nested table for production values in the winery, the way this works is through the n:m relationship table (presentacion), which stores a foreign key for (bodega) and a foreign key for (marca) wine.
I'm using a cursor that retrieves the production ammounts for a given year and sums them using SUM in the select, the problem is it sums every single production value that meets the search criteria, this means, it does retrieve the production values for all of the wines belonging to a winery but sums values from every year and not the specified year.
I've tried using GROUP BY to group the sum by year, which stores the proper value for each year stored in the cursor, this could work but I need a way to insert these into the nested table for the wineries' production figures, I'm not sure how I can do this, any help would be appreciated.
Create or replace procedure prueba(idbod INTEGER, ano DATE)
CURSOR prodbod IS
SELECT
sum(nt.cantidad)
FROM bodega b,
presentacion p,
marca m,
TABLE(m.prod_an_marca) nt
WHERE m.id_marca = p.marca_fk
AND b.id_bod = p.bodega_fk
AND b.id_bod = idbod
AND nt.ano = ano;
tempvar INTEGER;
BEGIN
OPEN prodbod;
LOOP
FETCH prodbod INTO tempvar;
EXIT WHEN prodbod%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('tempvar:'||to_char(tempvar));
END LOOP;
CLOSE prodbod;
END;
The issue is actually much simpler than it might appear from your description. Your query has:
AND nt.ano = ano
As stated in the documentation:
If a SQL statement references a name that belongs to both a column and either a local variable or formal parameter, then the column name takes precedence.
so you are effectively doing:
AND nt.ano = nt.ano
which is more obviously always true. You seem to have avoided the same issue by using slightly different names for id_bod and idbod, possibly accidentally. You can either explicitly state the second reference is the PL/SQL variable by prefixing it with the procedure name:
AND nt.ano = prueba.ano
or change your formal argument name. It's common to use prefixes to avoid this sort of confusion, e.g.:
Create or replace procedure prueba(p_id_bod INTEGER, p_ano DATE) as
...
AND b.id_bod = p_id_bod
AND nt.ano = p_ano;
...
As mentioned in comments, you should really be using explicit join syntax, and your example doesn't really need an explicit cursor, or a loop (or even any PL/SQL really - though I understand you will be expanding it); you can get the total with just:
Create or replace procedure prueba(p_idbod INTEGER, p_ano DATE) as
tempvar INTEGER;
BEGIN
SELECT sum(nt.cantidad)
INTO tempvar
FROM bodega b
JOIN presentacion p on p.bodega_fk = b.id_bod
JOIN marca m on m.id_marca = p.marca_fk
CROSS JOIN TABLE(m.prod_an_marca) nt
WHERE b.id_bod = p_id_bod
AND nt.ano = p_ano;
DBMS_OUTPUT.PUT_LINE('tempvar:'||to_char(tempvar));
END;
/