UDF showing weird behavior when checking if variable holds null value - sql

I am trying to write a UDF that is used by a check constraint but seem to keep running into an issue.
When having a complete empty table the following results in 1
declare #a float = (SELECT max(amount) FROM Bid WHERE auctionId = 1)
if(#a is null) select 1
else select 0
But when I try to implement similar logic into an UDF it seems to return 0 every time running it against an empty table.
CREATE FUNCTION dbo.fn_ck_newBidIsHigher(#auction int, #bid numeric(8,2))
RETURNS bit
BEGIN
DECLARE #currentHighestBid float = (SELECT max(amount) FROM Bid WHERE auctionId = #auction)
IF((#bid > #currentHighestBid) OR (#currentHighestBid is null))
BEGIN
RETURN 1
END
RETURN 0
END
GO
've been looking at it for over an hour now (maybe that's the problem), but I can't figure out where it's going wrong.
I am calling the function in the check constraint as follows:
ALTER TABLE Bid ADD CONSTRAINT ck_New_Bid_Must_Be_Higher_Than_Previous_Bid CHECK(dbo.fn_ck_newBidIsHigher(auctionId, amount) = 1)

Instead of using an IF, how about using a CASE? When using a CASE you can have more than 2 possible results. F.e. 1 or 0 or NULL, instead of only 1 or 0.
Also, it's safer to compare values of the same type.
Just to avoid any unexpected side-effects from the implicit cast/convert before the comparison.
CREATE FUNCTION dbo.fn_ck_newBidIsHigher(#auction int, #bid numeric(8,2))
RETURNS bit
BEGIN
DECLARE #currentHighestBid numeric(8,2) = (SELECT max(amount) FROM Bid WHERE auctionId = #auction);
DECLARE #result bit = (
case
when #bid > #currentHighestBid then 1
when #bid <= #currentHighestBid then 0
end);
RETURN #result;
END

Related

Finding if an item is available for booking in a given period search algorithm with SQL Server

Am asking for help with an algorithm for searching the availability of an item for a given period, this my table :
CREATE TABLE Emprunter
(
idEmp int primary key identity,
idClt int references Client,
idMat int references Materiel,
dateEmprunt date,
dateRetour date, --this is a nullable
dureeEmprunt int,
montantTotal money
)
The dateRetour column is nullable, the value will be inserted when the client return the item, so if an item has a dateRetour value the item is free and available.
Now this is what I came up with:
Create function IsMatrielDisponible
(#idMat int, #dateD date, #dateF date)
returns bit
as
Begin
Declare #Bool bit = 1
if(Exists(select *
from Emprunter
where idMat = 2 and dateRetour is null
and ((dateEmprunt >= #dateD or dateEmprunt <= #dateD) and dateEmprunt <= #dateF)))
set #Bool = 0
return #Bool
End
select dbo.IsMatrielDisponible(1,'2018-03-05','2018-06-05')
but it is not working. I have a record with id = 1 in the table with the dateEmprunt = '2018-03-03' and the dateRetour is null so the item lookup should return not available return 0, but the function returns 1
update - Table in designer:
the column is null as you can see.
for the behaver that i want is when i give the function a matériel (idMat) and a date depart (#dateD) and a date Return (#dateF) it should look for the item in the rent table (Emprunter) and if the item is available it should return 1, if not return 0,
Simple data:
for example if i looked for the item 3 is should be available because no one is renting it , but for item 1 is rented by another client because the dateRetour is null this means the client has not returned the item yet
select dbo.IsMatrielDisponible(1,'2018-03-05','2018-06-05')
so by executing this it should return 0, i hope you get the idea
I would consider rewriting this as an inline table valued function instead a scalar function. It is much better for performance and has a LOT more flexibility. I also removed a lot of extra code. Any value other 0 when being converted to a bit will be 1. So we can leverage COUNT for this quite easily. It can't return NULL and will return 1 if there are ANY rows that meet the query condition. I also removed the dateEmprunt stuff because it is not needed. There is no date that can be neither <= or >= to a given date. Maybe that should be a simple dateEmprunt IS NOT NULL but I don't know what you are trying to accomplish there.
Create function IsMatrielDisponible(#idMat int, #dateD date, #dateF date)
returns table as return
select IsDisponible = convert(bit, count(*))
from Emprunter
where idMat = #idMat
and dateRetour is null
--and ((dateEmprunt >= #dateD or dateEmprunt <= #dateD ) this is pointless as every value will return true from this
and dateEmprunt <= #dateF
GO
select *
from dbo.IsMatrielDisponible(1,'2018-03-05','2018-06-05')

SQL Creating a function which checks variables to make a total

I've made 2 views which calculates the profit per reservation.
One calculates the profit from bikes and the other one from accessories.
Now when I tried SUMing them together, I get false reports. I found out why: something a reservation doesnt contain a bike or a accessory. This field gives a NULL return, which makes my SUM function unusable, because it lacks results.
I tried this:
USE Biker
GO
CREATE FUNCTION fnMaxOmzet
(
#Fiets AS int,
#Accessoire AS int
)
RETURNS int
AS
BEGIN
DECLARE #MaxOmzet AS int
IF #Fiets = NULL
SET #MaxOmzet = #Accessoire
ELSE IF #Accessoire = NULL
SET #MaxOmzet = #Fiets
ELSE
SET #MaxOmzet = #Fiets + #Accessoire
RETURN #MaxOmzet
END
But it isn't working because it gives multiple results..
Probably cause I am using '=' while it checking a list.
Does anyone know a way to make this function?
SELECT * FROM dbo.vAccessoireOmzet
SELECT * FROM dbo.vFietsOmzet
https://ibb.co/bTmJ26
Expected result: List of ID 1 - 100 and the total of AccesoireOmzet + FietsOmzet
Personally, I would use an inline Table Function. You'll find the performance is significantly faster.
NExt, you don't test for the value NULL by using the = operator, you use IS NULL.
This should cater for the logic you have above, however it is untested due to missing DDL and sample data:
CREATE FUNCTION dbo.fnMaxOmzet (#Fiets int, #Accessoire int)
RETURNS TABLE AS
RETURN
(
SELECT CASE WHEN #Fiets IS NULL THEN #Accessoire
WHEN #Accessoire IS NULL THEN #Fiets
ELSE #Fiets + #Accessoire
END AS MaxOmzet
)
GO
OK, just seen your comment with this sql: SELECT dbo.fnMaxOmzet((SELECT FietsOmzet FROM dbo.vFietsOmzet), (SELECT AccessoireOmzet FROM dbo.vAccessoireOmzet)).
That syntax is just wrong, sorry. You need to use APPLY and a JOIN. This is pure guess work as we have no DDL and Sample data... however:
SELECT MO.MaxOmzet--, --**Your other columns go here**
FROM vFietsOmzet FO
JOIN vAccessoireOmzet AO ON --**Your JOIN criteria goes here**
CROSS APPLY dbo.fnMaxOmzet (FO.FietsOmzet, AO.AccessoireOmzet) MO;
You need to use IS NULL rather than = NULL.
But you could also use something like this:
USE Biker
GO
CREATE FUNCTION fnMaxOmzet
(
#Fiets AS int,
#Accessoire AS int
)
RETURNS int
AS
BEGIN
RETURN ISNULL(#Fiets, 0) + ISNULL(#Accessoire, 0)
END
You can read here for more information on the behaviour of = NULL.
Update:
I imagine what you might be looking for is something like:
SELECT
ISNULL(F.Huurovereenkomst_id, A.Huurovereenkomst_id) Huurovereenkomst_id
, SUM(dbo.fnMaxOmzet(F.FietsOmzet, A.AccessoireOmzet)) MaxOmzet
FROM
dbo.vFietsOmzet F
FULL JOIN dbo.vAccessoireOmzet A ON F.Huurovereenkomst_id = A.Huurovereenkomst_id
GROUP BY ISNULL(F.Huurovereenkomst_id, A.Huurovereenkomst_id)
use coalesce thats position
USE Biker
GO
CREATE FUNCTION fnMaxOmzet
(
#Fiets AS int,
#Accessoire AS int
)
RETURNS int
AS
BEGIN
RETURN coalesce(#Fiets+#Accessoire,#Fiets,#Accessoire) from MyTableVar;
END

SQL Server WHILE

I am trying to generating unique card number from following function. I put my query inside a while loop to prevent duplicate card number but still I am getting duplicate numbers.
Anyone can help me?
Create FUNCTION GetCardNumber ()
RETURNS varchar(20)
AS
BEGIN
Declare #NewID varchar(20);
Declare #NewID1 varchar(36) ;
Declare #Counter int = 0;
While(1=1)
Begin
Set #NewID1 = (SELECT [MyNewId] FROM Get_NewID);
Set #NewID = '2662464' + '823' + '001' +right(YEAR(GETUTCDATE()),2) +(left(convert(varchar,ABS(CAST(CAST(#NewID1 AS VARBINARY(5)) AS bigint))),5));
Set #Counter = (Select count(*) from ContactTBL where ContactMembershipID = #NewID);
If #Counter = 0
BEGIN
BREAK;
END
End
return #newID
END
Go
Update : I am getting MyNewID from View:
CREATE VIEW Get_NewID
AS
SELECT NEWID() AS MyNewID
GO
Many thanks in advance.
Won't this just return the same value every time you run it? I can't see anywhere where you're incrementing anything, or getting any kind of value that would give you unique values each time. You need to do something that changes the value each time, for example using the current exact date and time.
You're returning varchar(20) in line 2. To get your 'unique' NewId, you're doing this:
Set #NewId = (13 digit constant value) + (last 2 digits of current year) +
left(
convert(varchar,
ABS(CAST
(CAST(#NewID1 AS VARBINARY(5)) AS bigint)
)
)
,5)
which leaves you only 5 characters of uniqueness! This is almost certainly the issue. An easy fix may be increase the characters you return on line 2 e.g. RETURNS varchar(30)
What you're doing is unnecessarily complicated, and I think there is an element of overprotecting against potential duplicate values. This line is very suspect:
Set #NewID = '2662464' + '823' + '001' +right(YEAR(GETUTCDATE()),2) +(left(convert(varchar,ABS(CAST(CAST(#NewID1 AS VARBINARY(5)) AS bigint))),5));
The maximum for bigint is 2^63-1, so casting your 5-byte VARBINARY to a bigint could result in an overflow, which may also cause an issue.
I'm not sure exactly what you're trying to achieve, but you need to simplify things and make sure you have more scope for unique values!
Set #NewID1 = (SELECT [MyNewId] FROM Get_NewID);
always return the same result (if no other changes)
Set #NewID = '2662464' + '823' + '001' +right(YEAR(GETUTCDATE()),2) +(left(convert(varchar,ABS(CAST(CAST(#NewID1 AS VARBINARY(5)) AS bigint))),5));
as result #New_ID will be the same also

**Occasional** Arithmetic overflow error converting expression to data type int

I'm running an update script to obfuscate data and am occasionally experiencing the arithmetic overflow error message, as in the title. The table being updated has 260k records and yet the update script will need to be run several times to produce the error. Although it's so rare I can't rely on the code until it's fixed as it's a pain to debug.
Looking at other similar questions, this is often resolved by changing the data type e.g from INT to BIGINT either in the table or in a calculation. However, I can't see where this could be required. I've reduced the script to the below as I've managed to pin point it to the update of one column.
A function is being called by the update and I've included this below. I suspect that, due to the randomness of the error, the use of the NEW_ID function could be causing it but I haven't been able to re-create the error when just running this part of the function multiple times. The NEW_ID function can't be used in functions so it's being called from a view, also included below.
Update script:
UPDATE dbo.Addresses
SET HouseNumber = CASE WHEN LEN(HouseNumber) > 0
THEN dbo.fn_GenerateRandomString (LEN(HouseNumber), 1, 1, 1)
ELSE HouseNumber
END
NEW_ID view and random string function
CREATE VIEW dbo.vw_GetNewID
AS
SELECT NEWID() AS New_ID
CREATE FUNCTION dbo.fn_GenerateRandomString (
#stringLength int,
#upperCaseBit bit,
#lowerCaseBit bit,
#numberBit bit
)
RETURNS nvarchar(100)
AS
BEGIN
-- Sanitise string length values.
IF ISNULL(#stringLength, -1) < 0
SET #stringLength = 0
-- Generate a random string from the specified character sets.
DECLARE #string nvarchar(100) = ''
SELECT
#string += c2
FROM
(
SELECT TOP (#stringLength) c2 FROM (
SELECT c1 FROM
(
VALUES ('A'),('B'),('C')
) AS T1(c1)
WHERE #upperCaseBit = 1
UNION ALL
SELECT c1 FROM
(
VALUES ('a'),('b'),('c')
) AS T1(c1)
WHERE #lowerCaseBit = 1
SELECT c1 FROM
(
VALUES ('0'),('1'),('2'),('3'),('4'),('5'),('6'),('7'),('8'),('9')
) AS T1(c1)
WHERE #numberBit = 1
)
AS T2(c2)
ORDER BY (SELECT ABS(CHECKSUM(New_ID)) from vw_GetNewID)
) AS T2
RETURN #string
END
Addresses table (for testing):
CREATE TABLE dbo.Addresses(HouseNumber nchar(32) NULL)
INSERT Addresses(HouseNumber)
VALUES ('DSjkmf jkghjsh35hjk h2jkhj3h jhf'),
('SDjfksj3548 ksjk'),
(NULL),
(''),
('2a'),
('1234567890'),
('An2b')
Note: only 7k of the rows in the addresses table have a value entered i.e. LEN(HouseNumber) > 0.
An arithmetic overflow in what is otherwise string-based code is confounding. But there is one thing that could be causing the arithmetic overflow. That is your ORDER BY clause:
ORDER BY (SELECT ABS(CHECKSUM(New_ID)) from vw_GetNewID)
CHECKSUM() returns an integer, whose range is -2,147,483,648 to 2,147,483,647. Note the absolute value of the smallest number is 2,147,483,648, and that is just outside the range. You can verify that SELECT ABS(CAST('-2147483648' as int)) generates the arithmetic overflow error.
You don't need the checksum(). Alas, you do need the view because this logic is in a function and NEWID() is side-effecting. But, you can use:
ORDER BY (SELECT New_ID from vw_GetNewID)
I suspect that the reason you are seeing this every million or so rows rather than every 4 billion rows or so is because the ORDER BY value is being evaluated multiple times for each row as part of the sorting process. Eventually, it is going to hit the lower limit.
EDIT:
If you care about efficiency, it is probably faster to do this using string operations rather than tables. I might suggest this version of the function:
CREATE VIEW vw_rand AS SELECT rand() as rand;
GO
CREATE FUNCTION dbo.fn_GenerateRandomString (
#stringLength int,
#upperCaseBit bit,
#lowerCaseBit bit,
#numberBit bit
)
RETURNS nvarchar(100)
AS
BEGIN
DECLARE #string NVARCHAR(255) = '';
-- Sanitise string length values.
IF ISNULL(#stringLength, -1) < 0
SET #stringLength = 0;
DECLARE #lets VARCHAR(255) = '';
IF (#upperCaseBit = 1) SET #lets = #lets + 'ABC';
IF (#lowerCaseBit = 1) SET #lets = #lets + 'abc';
IF (#numberBit = 1) SET #lets = #lets + '0123456789';
DECLARE #len int = len(#lets);
WHILE #stringLength > 0 BEGIN
SELECT #string += SUBSTRING(#lets, 1 + CAST(rand * #len as INT), 1)
FROM vw_rand;
SET #stringLength = #stringLength - 1;
END;
RETURN #string
END;
As a note: rand() is documented as being exclusive of the end of its range, so you don't have to worry about it returning exactly 1.
Also, this version is subtly different from your version because it can pull the same letter more than once (and as a consequence can also handle longer strings). I think this is actually a benefit.

SQL Server 2008 inconsistent results

I just released some code into production that is randomly causing errors. I already fixed the problem by totally changing the way I was doing the query. However, it still bothers me that I don't know what was causing the problem in the first place so was wondering if someone might know the answer. I have the following query inside of a stored procedure. I'm not looking for comments about that's not a good practice to make queries with nested function calls and things like that :-). Just really want to find out why it doesn't work consistently. Randomly the function in the query will return a non-numeric value and cause an error on the join. However, if I immediately rerun the query it works fine.
SELECT cscsf.cloud_server_current_software_firewall_id,
dbo.fn_GetCustomerFriendlyFromRuleName(cscsf.rule_name, np.policy_name) as rule_name,
cscsf.rule_action,
cscsf.rule_direction,
cscsf.source_address,
cscsf.source_mask,
cscsf.destination_address,
cscsf.destination_mask,
cscsf.protocol,
cscsf.port_or_port_range,
cscsf.created_date_utc,
cscsf.created_by
FROM CLOUD_SERVER_CURRENT_SOFTWARE_FIREWALL cscsf
LEFT JOIN CLOUD_SERVER cs
ON cscsf.cloud_server_id = cs.cloud_server_id
LEFT JOIN CLOUD_ACCOUNT cla
ON cs.cloud_account_id = cla.cloud_account_id
LEFT JOIN CONFIGURATION co
ON cla.configuration_id = co.configuration_id
LEFT JOIN DEDICATED_ACCOUNT da
ON co.dedicated_account_id = da.dedicated_account_id
LEFT JOIN CORE_ACCOUNT ca
ON da.core_account_number = ca.core_account_id
LEFT JOIN NETWORK_POLICY np
ON np.network_policy_id = (select dbo.fn_GetIDFromRuleName(cscsf.rule_name))
WHERE cs.cloud_server_id = #cloud_server_id
AND cs.current_software_firewall_confg_guid = cscsf.config_guid
AND ca.core_account_id IS NOT NULL
ORDER BY cscsf.rule_direction, cscsf.cloud_server_current_software_firewall_id
if you notice the join
ON np.network_policy_id = (select dbo.fn_GetIDFromRuleName(cscsf.rule_name))
calls a function.
Here is that function:
ALTER FUNCTION [dbo].[fn_GetIDFromRuleName]
(
#rule_name varchar(100)
)
RETURNS varchar(12)
AS
BEGIN
DECLARE #value varchar(12)
SET #value = dbo.fn_SplitGetNthRow(#rule_name, '-', 2)
SET #value = dbo.fn_SplitGetNthRow(#value, '_', 2)
SET #value = dbo.fn_SplitGetNthRow(#value, '-', 1)
RETURN #value
END
Which then calls this function:
ALTER FUNCTION [dbo].[fn_SplitGetNthRow]
(
#sInputList varchar(MAX),
#sDelimiter varchar(10) = ',',
#sRowNumber int = 1
)
RETURNS varchar(MAX)
AS
BEGIN
DECLARE #value varchar(MAX)
SELECT #value = data_split.item
FROM
(
SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) as row_num FROM dbo.fn_Split(#sInputList, #sDelimiter)
) AS data_split
WHERE
data_split.row_num = #sRowNumber
IF #value IS NULL
SET #value = ''
RETURN #value
END
which finally calls this function:
ALTER FUNCTION [dbo].[fn_Split] (
#sInputList VARCHAR(MAX),
#sDelimiter VARCHAR(10) = ','
) RETURNS #List TABLE (item VARCHAR(MAX))
BEGIN
DECLARE #sItem VARCHAR(MAX)
WHILE CHARINDEX(#sDelimiter,#sInputList,0) <> 0
BEGIN
SELECT #sItem=RTRIM(LTRIM(SUBSTRING(#sInputList,1,CHARINDEX(#sDelimiter,#sInputList,0)-1))), #sInputList=RTRIM(LTRIM(SUBSTRING(#sInputList,CHARINDEX(#sDelimiter,#sInputList,0)+LEN(#sDelimiter),LEN(#sInputList))))
IF LEN(#sItem) > 0
INSERT INTO #List SELECT #sItem
END
IF LEN(#sInputList) > 0
INSERT INTO #List SELECT #sInputList -- Put the last item in
RETURN
END
The reason it is "randomly" returning different things has to do with how SQL Server optimizes queries, and where they get short-circuited.
One way to fix the problem is the change the return value of fn_GetIDFromRuleName:
return (case when isnumeric(#value) then #value end)
Or, change the join condition:
on np.network_policy_id = (select case when isnumeric(dbo.fn_GetIDFromRuleName(cscsf.rule_name)) = 1)
then dbo.fn_GetIDFromRuleName(cscsf.rule_name) end)
The underlying problem is order of evaluation. The reason the "case" statement fixes the problem is because it checks for a numeric value before it converts and SQL Server guarantees the order of evaluation in a case statement. As a note, you could still have problems with converting numbers like "6e07" or "1.23" which are numeric, but not integers.
Why does it work sometimes? Well, clearly the query execution plan is changing, either statically or dynamically. The failing case is probably on a row that is excluded by the WHERE condition. Why does it try to do the conversion? The question is where the conversion happens.
WHere the conversion happens depends on the query plan. This may, in turn, depend on when the table cscf in question is read. If it is already in member, then it might be read and attempted to be converted as a first step in the query. Then you would get the error. In another scenario, the another table might be filtererd, and the rows removed before they are converted.
In any case, my advice is:
NEVER have implicit conversion in queries.
Use the case statement for explicit conversions.
Do not rely on WHERE clauses to filter data to make conversions work. Use the case statement.