Scalar-valued function returning NULL - sql

I have the below function, and for the life of me, I cannot get it to return a value, I get NULL every time.
I am calling it via select [dbo].[getFiatProfit](600.26,'GBP', 1000.99,'BTC') as op
What am I missing?
/****** Object: UserDefinedFunction [dbo].[getFiatProfit] Script Date: 06/07/2022 11:42:26 ******/
ALTER FUNCTION [dbo].[getFiatProfit] (
#fiatInvested float,
#fiatInvestedCurrency nvarchar,
#quantity float,
#currency nvarchar
)
RETURNS float
AS
BEGIN
declare #tmp float
declare #result float
declare #usdtgbp float
IF (#fiatInvestedCurrency = 'USD')
BEGIN
select #tmp = [dbo].[usdtPairs].[Value] from [dbo].[usdtPairs] where usdtPairs.ID = #currency;
select #usdtgbp = [dbo].[usdtPairs].[Value] from [dbo].[usdtPairs] where usdtPairs.ID = 'GBP';
set #result = (((#quantity * #tmp) - #fiatInvested) / #usdtgbp);
-- set #result = #quantity * #tmp;
END
ELSE
BEGIN
select #tmp = [dbo].[usdtPairs].[Value] from [dbo].[usdtPairs] where usdtPairs.ID = #currency;
set #result = ((#quantity * #tmp) - #fiatInvested);
-- set #result = #quantity * #tmp;
END
return (#result)
END

Your issue looks it's because your parameters are declared without a length. nvarchar defaults to a length of 1 in a lot of circumstances, so it's simply the wrong value being received. A much better data type would be char(3) which is fixed length, given that all currencies have exact three-letter names.
You should also convert this function into a Table Valued Function, which is likely to perform far better.
CREATE OR ALTER FUNCTION dbo.getFiatProfit (
#fiatInvested float,
#fiatInvestedCurrency char(3),
#quantity float,
#currency char(3)
)
RETURNS TABLE
AS RETURN
SELECT
result = ((#quantity * u.Value) - #fiatInvested)
/ (CASE WHEN #fiatInvestedCurrency = 'USD'
THEN 1
ELSE
(SELECT u2.Value FROM dbo.usdtPairs u2 WHERE u2.ID = 'GBP')
END)
FROM dbo.usdtPairs u
WHERE u.ID = #currency;
You use it like this
SELECT t.*, fp.*
FROM YourTable t
CROSS APPLY dbo.getFiatProfit(t.fiatInvested, t.fiatInvestedCurrency, t.Qty, 'GBP') fp;

Related

How can I write SQL select statement inside scalar type user-defined function?

I am trying to create a function in SQL Server using the following, but I think I am missing something in either in syntax or query
CREATE FUNCTION DEMO.Get_Rate_For_Absence
(#company_id_ VARCHAR,
#emp_no_ VARCHAR,
#account_date_ DATE)
RETURN DECIMAL(10, 2) AS
BEGIN
DECLARE #RATE_ DECIMAL(10, 2)
SET #RATE_ = SELECT rate
FROM DEMO.Employee
WHERE COMPANY_ID = '#company_id_ '
AND Emp_no = '#emp_no_ '
AND ORG_CODE = '#wage_code_'
AND ACCOUNT_DATE = '#account_date_'
RETURN #RATE
END
The SQL statement that I am trying to write inside function code block is:
SELECT DISTINCT rate
FROM DEMO.Employee
WHERE Company_ID = #company_id_
AND EMP_NO = #emp_no_
AND ACCOUNT_DATE = #account_date_
Something like:
CREATE OR ALTER FUNCTION DEMO.Get_Rate_For_Absence
(#company_id VARCHAR(200),
#emp_no VARCHAR(200),
#account_date DATE)
RETURNS DECIMAL(10, 2) AS
BEGIN
DECLARE #RATE DECIMAL(10, 2)
SET #RATE = (
SELECT rate
FROM DEMO.Employee
WHERE COMPANY_ID = #company_id
AND Emp_no = #emp_no
AND ACCOUNT_DATE = #account_date
)
RETURN #RATE
END
Perhaps you actually want to return a whole resultset rather than just a single value.
Then you should use an inline Table Valued Function (of the form RETURNS TABLE AS RETURN SELECT ...) which in any case performs much better than a scalar function.
Variables don't go in quotes so you just do COMPANY_ID = #company_id_.
Always declare varchar with a length.
CREATE OR ALTER FUNCTION DEMO.Get_Rate_For_Absence (
#company_id_ VARCHAR(100),
#emp_no_ VARCHAR(100),
#wage_code_ VARCAHR(100),
#account_date_ DATE
)
RETURNS TABLE AS RETURN
SELECT e.rate
FROM DEMO.Employee e
WHERE e.COMPANY_ID = #company_id_
AND e.Emp_no = #emp_no_
AND e.ORG_CODE = #wage_code_
AND e.ACCOUNT_DATE = #account_date_;
You use it slightly differently than scalar functions, as it goes in the FROM part
SELECT r.rate
FROM DEMO.Get_Rate_For_Absence('a', 'b', 'c', GETDATE()) r;
Or
SELECT r.rate
FROM SomeTable t
CROSS APPLY DEMO.Get_Rate_For_Absence(t.a, t.b, t.c, t.date) r;

store the variable for the call inside the select sql server

how to store the variable for the call inside the select
example:
DECLARE #Qty INT, #workdays INT, #dailygoal INT, #bufferstock INT;
SELECT
#Qty = SUM(qty) [qty],
#workdays = 25 [workdays],
#dailygoal = (SUM(qty) / 25) [dailygoal],
#bufferstock = (#dailygoal * MAX(leadtime)) [bufferstock]
FROM table
Remove Alias Name ..
DECLARE #Qty INT, #workdays INT, #dailygoal INT, #bufferstock INT;
SELECT
#Qty = SUM(Bonus_amount),
#workdays = 25,
#dailygoal = (SUM(Bonus_amount) / 25),
#bufferstock = (#dailygoal * 5)
FROM [dbo].[Bonus]
select #Qty,#workdays,#dailygoal,#bufferstock
The query you have will error, due to your aliases. When you assign a variable in a SELECT you do not alias the column.
Also, although you can do syntax like you have, where you reference a variable assigned elsewhere in the statement, I would err against it; though not documented I would not be surprised if has the same antipattern as documented in Antipattern use of recursive variable assignment and that the latter variable could be derived prior to the former. This would mean that the variable #bufferstock could be assigned the value NULL, as the value of #dailygoal was NULL when the expressed was determined.
Instead, just repeat the expression:
DECLARE #V1 int,
#V2 int;
SELECT #V1 = SUM(V.I),
#V2 = SUM(V.I) * MAX(V.I)
FROM (VALUES(1),(2),(3))V(I);
SELECT #V1,
#V2;
If you don't want to do that, then use a derived table (a CTE or Subquery) to define the expression instead. I use a CTE here:
DECLARE #V1 int,
#V2 int;
WITH CTE AS(
SELECT SUM(V.I) AS S,
MAX(V.I) AS M
FROM (VALUES(1),(2),(3))V(I))
SELECT #V1 = S,
#V2 = S*M
FROM CTE;
SELECT #V1,
#V2;
Using a derived table for your query would mean something like the following:
DECLARE #Qty int,
#workdays int,
#dailygoal int,
#bufferstock int;
WITH CTE AS(
SELECT SUM(qty) AS qty,
SUM(qty) / 25 AS dailygoal,
MAX(leadtime) AS leadtime
FROM dbo.[table])
SELECT #Qty = qty,
#workdays = 25,
#dailygoal = dailygoal,
#bufferstock = dailygoal * leadtime
FROM CTE;

Insert into table within function

I would like to insert the #OBV's value into a table inside the function. What is the correct way to achieve it?
alter function CalculateOnBalanceVolume (
#operation varchar(3),
#volume money
)
returns char(4) as
begin
declare #prevOBV as money,
#OBV as money
set #prevOBV = (
select top 1 OnBalanceVolume
from OnBalanceVolume
order by EventTime desc
)
if (#operation = 'add') set #OBV = #prevOBV + #volume
if (#operation = 'sub') set #OBV = #prevOBV - #volume
insert into OBVTable values (#OBV) // error
return #OBV
end;
Functions cannot perform any actions known as side-effecting which includes inserting or updating or deleting from tables, so you cannot use a Function for this.
To use a stored procedure you might have:
create procedure CalculateOnBalanceVolume
#operation varchar(3),
#volume decimal(9,2),
#OBV decimal(9,2) output
as
select top (1) #Obv=OnBalanceVolume +
case when #Operation='add' then #volume else -#volume end
from OnBalanceVolume
order by EventTime desc
insert into OBVTable values (#OBV)
go
And then to invoke the procedure and get your output value you would do for example:
declare #OBV decimal(9,2)
exec CalculateOnBalanceVolume 'add', 100, #OBV output
select #OBV as OutputValue

Return two decimal points in create function

I have here an sql create function that will return float datatype.
I would like to know if how am I going to show the results to two decimal points.
ALTER FUNCTION [dbo].[udf_get_total]
(
-- Add the parameters for the function here
#code as int
)
RETURNS FLOAT
AS
BEGIN
-- Declare the return variable here
DECLARE #quantity as FLOAT
DECLARE #price as FLOAT
-- Add the T-SQL statements to compute the return value here
SET #quantity = (SELECT quantity FROM Requested_Item where Code_id = #code)
SET #price = (SELECT price FROM Requested_Item where Code_id = #code)
-- Return the result of the function
RETURN #quantity * #price
END
The sample results:
1632.1740985705144
6323.8092596543638
Use CAST
select cast(float_column as decimal(10,2))
from your_table
decimal(10,2) will display 10 digits before the point and 2 after it.
First of all. I'd rewrite your function. You're querying your table twice. This can be done by running a single SELECT statement. See this code:
ALTER FUNCTION [dbo].[udf_get_total]
(
#code AS INT
)
RETURNS FLOAT
AS
BEGIN
-- Declare the return variable here
DECLARE #quantity AS FLOAT
, #price AS FLOAT;
SELECT #quantity = quantity, #price = price
FROM Requested_Item
WHERE Code_id = #code;
-- Return the result of the function
RETURN #quantity * #price;
END
Now how to format your numbers properly? I used this method with CEILING and a bit of maths:
DECLARE #first FLOAT = 1632.1740985705144
, #second FLOAT = 6323.8092596543638;
SELECT FLOOR(#first * 100) / 100
, FLOOR(#second * 100) / 100;
It brings requested result:
╔═════════╦════════╗
║ 1632.17 ║ 6323.8 ║
╚═════════╩════════╝
Try this:
RETURN TRUNCATE(#quantity * #price, 2)
The 2 at the end means there will be 2 decimal points. You can really use the TRUNCATE function anywhere - the only downside is that it doesn't really round the numbers.
I only changed the return statement of my function to
RETURN CAST ((#total / #cur_amount) as decimal(10,2))
and it shows the 2 decimal points.

SQL Table Valued Function in Select Statement

SQL is not my best thing but I have been trying to optimize this stored procedure. It had multiple scalar-valued functions that I tried to change to table-valued functions because I read in many places that it's a more efficient way of doing it. And now I have them made but not real sure how to implement or if I maybe just didn't create them correctly.
This is the function I'm calling.
Alter FUNCTION [IsNotSenateActivityTableValue]
(
#ActivityCode int,
#BillId int,
#TextToDisplay varchar(max)
)
returns #T table(result varchar(max))
as
begin
DECLARE #result varchar(max);
declare #countcodes int;
declare #ishousebill int;
select #ishousebill = count(billid)
from BillMaster
where BillID = #BillID and Chamber = 'H'
If (#ishousebill = 0)
begin
SELECT #countcodes = count([ActivityCode])
FROM [HouseCoreData].[dbo].[ActivityCode]
where ActivityDescription not like '%(H)%' and ActivityType = 'S'
and [ActivityCode] = #ActivityCode
if (#countcodes = 0)
begin
set #result = 'test'
end
else
begin
set #result = 'test2'
end
end
else
begin
set #result = #TextToDisplay
end
RETURN
END
And this is how I was trying to call them like this. I would prefer just being able to put them in the top but really anything that works would be good.
SELECT distinct
ActionDates.result as ActionDate
,ActivityDescriptions.result as ActivityDescription
FROM BillWebReporting.vwBillDetailWithSubjectIndex as vw
left outer join [BillWebReporting].[HasHouseSummary] as HasSummary on vw.BillID = HasSummary.BillID
outer APPLY dbo.IsNotSenateActivityDateTableValue(ActivityCode,vw.BillID,[ActionDate]) ActionDates
OUTER APPLY dbo.IsNotSenateActivityTableValue(ActivityCode,vw.BillID,[ActivityDescription]) as ActivityDescriptions
Getting a count just to see if at least one row exists is very expensive. You should use EXISTS instead, which can potentially short circuit without materializing the entire count.
Here is a more efficient way using an inline table-valued function instead of a multi-statement table-valued function.
ALTER FUNCTION dbo.[IsNotSenateActivityTableValue] -- always use schema prefix!
(
#ActivityCode int,
#BillId int,
#TextToDisplay varchar(max)
)
RETURNS TABLE
AS
RETURN (SELECT result = CASE WHEN EXISTS
(SELECT 1 FROM dbo.BillMaster
WHERE BillID = #BillID AND Chamber = 'H'
) THEN #TextToDisplay ELSE CASE WHEN EXISTS
(SELECT 1 FROM [HouseCoreData].[dbo].[ActivityCode]
where ActivityDescription not like '%(H)%'
and ActivityType = 'S'
and [ActivityCode] = #ActivityCode
) THEN 'test2' ELSE 'test' END
END);
GO
Of course it could also just be a scalar UDF...
ALTER FUNCTION dbo.[IsNotSenateActivityScalar] -- always use schema prefix!
(
#ActivityCode int,
#BillId int,
#TextToDisplay varchar(max)
)
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE #result VARCHAR(MAX);
SELECT #result = CASE WHEN EXISTS
(SELECT 1 FROM dbo.BillMaster
WHERE BillID = #BillID AND Chamber = 'H'
) THEN #TextToDisplay ELSE CASE WHEN EXISTS
(SELECT 1 FROM [HouseCoreData].[dbo].[ActivityCode]
where ActivityDescription not like '%(H)%'
and ActivityType = 'S'
and [ActivityCode] = #ActivityCode
) THEN 'test2' ELSE 'test' END
END;
RETURN (#result);
END
GO
Table-valued functions return a table, in which, like any other table, rows have to be inserted.
Instead of doing set #result = ....., do:
INSERT INTO #T (result) VALUES ( ..... )
EDIT: As a side note, I don't really understand the reason for this function to be table-valued. You are essentially returning one value.
First of all UDFs generally are very non-performant. I am not sure about MySQL, but in Sql Server a UDF is recompiled every time (FOR EACH ROW OF OUTPUT) it is executed, except for what are called inline UDFs, which only have a single select statement, which is folded into the SQL of the outer query it is included in... and so is only compiled once.
MySQL does have inline table-valued functions, use it instead... in SQL Server, the syntax would be:
CREATE FUNCTION IsNotSenateActivityTableValue
(
#ActivityCode int,
#BillId int,
#TextToDisplay varchar(max)
)
RETURNS TABLE
AS
RETURN
(
Select case
When y.bilCnt + z.actCnt = 0 Then 'test'
when y.bilCnt = 0 then 'test2'
else #TextToDisplay end result
From (Select Count(billId) bilCnt
From BillMaster
Where BillID = #BillID
And Chamber = 'H') y
Full Join
(Select count([ActivityCode]) actCnt
From [HouseCoreData].[dbo].[ActivityCode]
Where ActivityDescription not like '%(H)%'
And ActivityType = 'S'
And [ActivityCode] = #ActivityCode) z
)
GO