In a SQL Server database, one can use table variables like this:
declare #table as table (a int)
In an Azure Data Warehouse, that throws an error.
Parse error at line: 1, column: 19: Incorrect syntax near 'table'
In an Azure Data Warehouse, you can use temporary tables:
create table #table (a int)
but not inside functions.
Msg 2772, Level 16, State 1, Line 6 Cannot access temporary tables
from within a function.
This document from Microsoft says,
◦Must be declared in two steps (rather than inline): ◾CREATE TYPE
my_type AS TABLE ...; , then ◾DECLARE #mytablevariable my_type;.
But when I try this:
create type t as table (a int);
drop type t;
I get this :
Msg 103010, Level 16, State 1, Line 1 Parse error at line: 1, column:
8: Incorrect syntax near 'type'.
My objective is to have a function in an Azure Data Warehouse which uses a temporary table. Is it achievable?
Edit Start Here
Note that I am not looking for other ways to create one specific function. I have actually done that and moved on. I'm a veteran programmer but an Azure Data Warehouse rookie. I want to know if it's possible to incorporate some concept of temporary tables in an Azure Data Warehouse function.
Ok, I believe this is what you are after.
Firstly, this uses a Table Value Function, which are significantly faster than Scalar or Multi-statement Table value Functions.
Secondly, there was no use for a Table Variable, or Temporary Table, just some good odd string manipulation, a bit of maths, and a CTE. Definitely no expensive WHILE loop.
I've tested this against the examples in the link, and they all return the expected values.
USE Sandbox;
GO
CREATE FUNCTION ValidateHealthNumber (#HealthNumber varchar(10))
RETURNS TABLE
AS
RETURN
WITH Doubles AS(
SELECT CONVERT(tinyint,SUBSTRING(V.HN,O.P,1)) AS HNDigit,
CONVERT(tinyint,SUBSTRING(V.HN,O.P,1)) * CASE WHEN O.P % 2 = 0 THEN 1 ELSE 2 END ToAdd
FROM (VALUES(#HealthNumber)) V(HN)
CROSS APPLY (VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)) O(P)),
Parts AS (
SELECT CONVERT(tinyint,SUBSTRING(CONVERT(varchar(2),ToAdd),1,1)) AS FirstDigit, --We know that the highest value can be 18 (2*9)
CONVERT(tinyint,SUBSTRING(CONVERT(varchar(2),ToAdd),2,1)) AS SecondDigit --so no need for more than 2 digits.
FROM Doubles)
SELECT CASE RIGHT(#HealthNumber, 1) WHEN 10 - RIGHT(SUM(FirstDigit + SecondDigit),1) THEN 1 ELSE 0 END AS IsValid
FROM Parts;
GO
CREATE TABLE #Sample(HealthNumber varchar(10));
INSERT INTO #Sample
VALUES ('9876543217'), --Sample
('5322369835'), --Valid
('7089771195'), --Valid
('8108876957'), --Valid
('4395667779'), --Valid
('6983806917'), --Valid
('2790412845'), --not Valid
('5762696912'); --not Valid
SELECT *
FROM #Sample S
CROSS APPLY ValidateHealthNumber(HealthNumber) VHN;
GO
DROP TABLE #Sample
DROP FUNCTION ValidateHealthNumber;
If you don't understand any of this, please do ask.
No you can't. Object can't be created inside User Defined Functions (UDF). Use table variables instead.
If you want yo use user defined type, first create it outside the UDF and use it as a variable type within the UDF.
-- Create the data type
CREATE TYPE TestType AS TABLE
(
Id INT NOT NULL,
Col1 VARCHAR(20) NOT NULL)
GO
-- Create the tabled valued function
CREATE FUNCTION TestFunction
()
RETURNS
#Results TABLE
(Result1 INT, Result2 INT)
AS
BEGIN
-- Fill the table variable with the rows for your result set
DECLARE #Var1 TestType;
RETURN
END
GO
Related
This question already has an answer here:
Checking if UTD parameter has values in stored procedure
(1 answer)
Closed 3 years ago.
I have a stored proc in SQL Server that accepts a table (dataset) as a parameter like so:
CREATE PROCEDURE [dbo].[blah]
(
#someParm INT,
#someTableParm [dbo].[IntIdType] READONLY,
The type IntIdType is like this:
CREATE TYPE [dbo].[IntIdType] AS TABLE(
[ID] [int] NOT NULL
)
But I need to be able to test to see if the parameter passed is NULL or if it can even be passed as null, and if not, I need to make a special test if it contains zero items
WHERE [SomeParm] = 2 AND
(#someTableParm IS NULL OR EXISTS (SELECT something FROM #someTableParm)) AND ...
I get the error message: Msg 137, Level 16, State 1, Procedure xxxx, Line 58 [Batch Start Line 0]
Must declare the scalar variable "#someTableParm".
Any suggestions?
#someTableParm IS NULL would look for a scalar variable #someTableParm, but your variable a table, so that won't work. Therefore, stick to EXISTS:
WHERE [SomeParm] = 2
AND EXISTS (SELECT 1 FROM #someTableParm)
AND...
If there are no rows in #someTableParm, then the EXISTS will return false. As a quick sample:
CREATE TABLE test (ID int)
INSERT INTO test VALUES(1);
DECLARE #S table (I int);
SELECT *
FROM test
WHERE EXISTS (SELECT 1 FROM #S);
DROP TABLE test;
Notice that the SELECT statement returns no rows.
I would like to create a function that returns a column based on input from three other columns. As temporary tables are not allowed within functions, is it possible to create a table variable from three input columns?
CREATE FUNCTION dbo.convert_value(
#CustomerID VARCHAR(MAX),
#CustomerValue VARCHAR(MAX),
#CustomerDescription VARCHAR(MAX)
)
RETURNS FLOAT
AS BEGIN
DECLARE #CustomerTable TABLE (
UniquePatientUID VARCHAR(MAX),
ResultValue VARCHAR(MAX),
PracticeDescription VARCHAR(MAX)
);
-- How can I insert #UniquePatientUID, #ResultValue and #PracticeDescription into #CustomerTable
END
The context of this question is that I have a SQL script that uses temporary tables and many UPDATE and ALTER TABLE statements, that I need to convert into a function. That script begins with the three columns mentioned, and adds a fourth column, Converted_Value, which is calculated with several hundred lines of code and manipulating temporary tables. Is there any hope here?
A table variable insert is really not different than a regular insert. Don't use temp tables. You can alter the table as well, or just declare it initially with that fourth column and allow it to be NULL.
INSERT INTO #CustomerTable (UniquePatientUID, ResultValue, PracticeDescription)
VALUES(#CustomerID, #CustomerValue, #CustomerDescription);
Don't forget to return the FLOAT.
Table Variable is a table so, you can just use INSERT INTO ... VALUES....
INSERT INTO #CustomerTable (UniquePatientUID,ResultValue,PracticeDescription )
VALUES
(#UniquePatientUID, #ResultValue , #PracticeDescription)
Unless you need a table variable for some specific reason, why not just work with the variables as a derived table expression? i.e.
;with inputs (UniquePatientUID, ResultValue, PracticeDescription) as
(
select #UniquePatientUID, #ResultValue, #PracticeDescription
)
select *
from inputs
Table variables fall out of scope after the function call, and you can't pass table types in or out of functions either. So really all a table variable does here is serve as a means of place keeping that's more familiar to SQL developers. But they're not free, which is the only reason I'm curious what your use case is.
If you don't need to return them as a set or something similar, you can just interact with the variables directly too.
How do I pass multiple entries through an input parameter mapped from a Table Function in SAP HANA ?
I've written a Table Function with an Input Parameter say IN_FORMAT_CD.
I've mapped this parameter to the one created in my calculation view.
I'm able to retrieve the data when I'm passing only one value say 100.
But it gives no result when I'm passing more than one value.
Is there any workaround for the same ?
My table function :
FUNCTION "HADMIN"."RA.Test.Prathamesh::PH_DEMO" (IN IN_FORMAT_CD NVARCHAR(500))
RETURNS TABLE (NAME NVARCHAR(10), ID NVARCHAR(10), FORMAT_CD NVARCHAR(3))
LANGUAGE SQLSCRIPT
SQL SECURITY INVOKER AS
BEGIN
RETURN
SELECT NAME,ID,FORMAT_CD
FROM
HADMIN.PH_DEMO
WHERE FORMAT_CD IN (select :IN_FORMAT_CD as FORMAT_CD from dummy);
END;
What you are looking for is the APPLY_FILTER function of SAP HANA SQLScript.
The following example shows how your scenario could be coded:
create function get_vals (IN id_list varchar(400))
returns table (id bigint, val varchar(40))
as
begin
declare myfilter varchar(450) := ' ID in (' || :id_list || ')';
_tmp = select id, val from some_vals;
_tmp2 = APPLY_FILTER (:_tmp, :myfilter);
return :_tmp2;
end;
select *
from
get_vals ('1, 4, 23, 4, 23, 3');
This approach will push down the unique list of IDs to be used as a filter when reading the table data. However, this is still dynamic SQL so you lose benefits like plan sharing and risk SQL injection attacks. Read more on this e.g. here.
If possible, you want to handle selection lists in your application code.
This, in turn, would also give you the option to decide whether using IN-lists or inner joins against temporary tables is the best approach for your situation.
In case you want to go with the selection list as a string, you should at least make sure, that common SQL injection attacks are not used and that the "in-list" really only contains possible ID values and commas.
it is not possible to produce(!) many items from a single sql variable unless you split them
In your SQL subselect query will return only rows that FORMAT_CD column values are exactly same with IN_FORMAT_CD parameter.
If this parameter represents more than one value, then this parameter is a concatenated string representation of each sub items. So we can split them back.
Splitting will produce a table on the fly which can be used for selection.
Please create the user-defined HANA Split function fnsplit that source codes can be found at referenced document
Then you can alter your function as follows assuming that each value is seperated with "," from others
ALTER FUNCTION "HADMIN"."RA.Test.Prathamesh::PH_DEMO" (IN IN_FORMAT_CD NVARCHAR(500))
RETURNS TABLE (NAME NVARCHAR(10), ID NVARCHAR(10), FORMAT_CD NVARCHAR(3))
LANGUAGE SQLSCRIPT
SQL SECURITY INVOKER AS
BEGIN
RETURN
SELECT NAME,ID,FORMAT_CD
FROM
HADMIN.PH_DEMO
WHERE FORMAT_CD IN (
select str from fnsplit(:IN_FORMAT_CD,',')
);
END;
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
Scenario
I have a stored procedure written in T-Sql using SQL Server 2005.
"SEL_ValuesByAssetName"
It accepts a unique string "AssetName".
It returns a table of values.
Question
Instead of calling the stored procedure multiple times and having to make a database call everytime I do this, I want to create another stored procedure that accepts a list of all the "AssetNames", and calls the stored procedure "SEL_ValueByAssetName" for each assetname in the list, and then returns the ENTIRE TABLE OF VALUES.
Pseudo Code
foreach(value in #AllAssetsList)
{
#AssetName = value
SEL_ValueByAssetName(#AssetName)
UPDATE #TempTable
}
How would I go about doing this?
It will look quite crippled with using Stored Procedures. But can you use Table-Valued Functions instead?
In case of Table-Valued functions it would look something like:
SELECT al.Value AS AssetName, av.* FROM #AllAssetsList AS al
CROSS APPLY SEL_ValuesByAssetName(al.Value) AS av
Sample implementation:
First of all, we need to create a Table-Valued Parameter type:
CREATE TYPE [dbo].[tvpStringTable] AS TABLE(Value varchar(max) NOT NULL)
Then, we need a function to get a value of a specific asset:
CREATE FUNCTION [dbo].[tvfGetAssetValue]
(
#assetName varchar(max)
)
RETURNS TABLE
AS
RETURN
(
-- Add the SELECT statement with parameter references here
SELECT 0 AS AssetValue
UNION
SELECT 5 AS AssetValue
UNION
SELECT 7 AS AssetValue
)
Next, a function to return a list AssetName, AssetValue for assets list:
CREATE FUNCTION [dbo].[tvfGetAllAssets]
(
#assetsList tvpStringTable READONLY
)
RETURNS TABLE
AS
RETURN
(
-- Add the SELECT statement with parameter references here
SELECT al.Value AS AssetName, av.AssetValue FROM #assetsList al
CROSS APPLY tvfGetAssetValue(al.Value) AS av
)
Finally, we can test it:
DECLARE #names tvpStringTable
INSERT INTO #names VALUES ('name1'), ('name2'), ('name3')
SELECT * FROM [Test].[dbo].[tvfGetAllAssets] (#names)
In MSSQL 2000 I would make #allAssetsList a Varchar comma separated values list. (and keep in mind that maximum length is 8000)
I would create a temporary table in the memory, parse this string and insert into that table, then do a simple query with the condition where assetName in (select assetName from #tempTable)
I wrote about MSSQL 2000 because I am not sure whether MSSQL 2005 has some new data type like an array that can be passed as a literal to the SP.