String Expression to be evaluated to number - sql-server

I need to write a TSQL user defined function which will accept a string and return a number.
I will call the function like dbo.EvaluateExpression('10*4.5*0.5') should return the number 22.5
Can any one help me to write this function EvaluateExpression.
Currently I am using CLR function which I need to avoid.
Edit1
I know this can be done using stored procedure, but I want to call this function in some statements ex: select 10* dbo.EvaluateExpression('10*4.5*0.5')
Also I have around 400,000 formulas like this to be evaluated.
Edit2
I know we can do it using osql.exe inside function as explained here. But due to permission settings, I can not use this also.

I don't think that is possible in a user defined function.
You could do it in a stored procedure, like:
declare #calc varchar(max)
set #calc = '10*4.5*0.5'
declare #sql nvarchar(max)
declare #result float
set #sql = N'set #result = ' + #calc
exec sp_executesql #sql, N'#result float output', #result out
select #result
But dynamic SQL, like exec or sp_executesql, is not allowed in user defined functions.

Disclaimer: I'm the owner of the project Eval SQL.NET
For SQL 2012+, you can use Eval SQL.NET which can be run with SAFE Permission.
The performance is great (better than UDF) and honors operator precedence and parenthesis. In fact, almost all the C# language is supported.
You can also specify parameters to your formula.
-- SELECT 225.00
SELECT 10 * CAST(SQLNET::New('10*4.5*0.5').Eval() AS DECIMAL(18, 2))
-- SELECT 70
DECLARE #formula VARCHAR(50) = 'a+b*c'
SELECT 10 * SQLNET::New(#formula)
.Val('a', 1)
.Val('b', 2)
.Val('c', 3)
.EvalInt()

Use this Function, It will absolutely working.
CREATE FUNCTION dbo.EvaluateExpression(#list nvarchar(MAX))
RETURNS Decimal(10,2)
AS
BEGIN
Declare #Result Decimal(10,2)
set #Result=1
DECLARE #pos int,
#nextpos int,
#valuelen int
SELECT #pos = 0, #nextpos = 1
WHILE #nextpos > 0
BEGIN
SELECT #nextpos = charindex('*', #list, #pos + 1)
SELECT #valuelen = CASE WHEN #nextpos > 0
THEN #nextpos
ELSE len(#list) + 1
END - #pos - 1
Set #Result=#Result*convert(decimal(10,2),substring(#list, #pos + 1, #valuelen))
SELECT #pos = #nextpos
END
RETURN #Result
END
You Can use this
Select 10* dbo.EvaluateExpression('10*4.5*0.5')

You can use the SQL Stored procedure below to calculate the result of any formula with any number of variables:
I wrote in 2012 a solution which can evaluate any type of Mathematical formula using SQL SERVER. The solution can handle any formula with N variables :
I was asked to find a way to evaluate the value given by a Formula which is filled by the user.
The Formula contains mathematical operations (addition, multiplication, division and subtractions)
The parameters used to calculate the formula are stored in the SQL server DATA BASE.
The solution I found by myself was as follows:
Suppose I have n parameters used to calculate the formula, each of these parameters is stored in one row in one data table.
The data table containing the n rows to use in the formula is called tab_value
I have to store the n values found in n rows (in tab_values) in one single row in one new Table, using SQL cursor,
for that I create a new table called tab_formula
In the cursor, I will add a new column for each value, the column name will be Id1,Id2,Id3 etc.
Then I construct a SQL script containing the formula to evaluate the formula
Here after the complete script, I hope you find it useful, you are welcome to ask me about it.
The procedure uses as input:
-The formula
-A table containing the values used to calculate the formula
if exists(select 1 from sysobjects where name='usp_evaluate_formula' and xtype='p')
drop proc usp_evaluate_formula
go
create type type_tab as table(id int identity(1,1),val decimal(10,2))
go
create proc usp_evaluate_formula(#formula as nvarchar(100),#values as type_tab readonly)
as begin
declare #tab_values table (id int, val decimal(10,2))
insert into #tab_values(id,val) select * from #values
declare #id as int declare #val as decimal(10,2)
if not exists(select 1 from sysobjects where name ='tab_formula')
create table tab_formula(id int identity(1,1), formula nvarchar(1000))
if not exists(select 1 from tab_formula where formula=#formula)
insert into tab_formula(formula) values(#formula)
declare c cursor for select id,val from #tab_values
declare #script as nvarchar(4000)
open c
fetch c into #id,#val
while ##fetch_status=0
begin
set #script = 'if not exists(select 1 from syscolumns c inner join sysobjects o on c.id=o.id where o.name=''tab_formula'' and c.name=''id'+
convert(nvarchar(3),#id)+ ''')
alter table tab_formula add id'+convert(nvarchar(3),#id)+ ' decimal(10,2)'
print #script
exec(#script)
set #script='update tab_formula set id'+convert(nvarchar(3),#id)+'='+convert(nvarchar(10),#val)+' where formula='''+#formula+'''' print #script exec(#script) fetch c into #id,#val end close c deallocate c
set #script='select *,convert(decimal(10,2),'+#formula+') "Result" from tab_formula where formula='''+#formula+''''
print #script
exec(#script)
end
go
declare #mytab as type_tab
insert into #mytab(val) values(1.56),(1.5) ,(2.5) ,(32),(1.7) ,(3.3) ,(3.9)
exec usp_evaluate_formula'2*cos(id1)+cos(id2)+cos(id3)+3*cos(id4)+cos(id5)+cos(id6)+cos(id7)/2*cos(Id6)',#mytab
go
drop proc usp_evaluate_formula
drop type type_tab
drop table tab_formula

Related

SQL Server while loop not printing last value

I have below code, why i am not trying to get the last value in the temp table for #waitlistitmeID
10011 is not getting stored, though its len is 19 and it should be inserted.
Need help in the query.
DECLARE #pos INT
DECLARE #len INT
DECLARE #value VARCHAR(MAX)
declare #WaitlistItemID varchar(max)='10008,10009,10010,10011'
declare #MovingWaitListItems table ( WaitlistItemID int, WaitlistItemGUID numeric(16,0))
set #pos = 0
set #len = 0
while CHARINDEX(',', #WaitlistItemID, #pos+1)>0
BEGIN
set #len = CHARINDEX(',', #WaitlistItemID, #pos+1) - #pos
set #value = SUBSTRING(#WaitlistItemID, #pos, #len)
print #pos
print #len
print #value
INSERT INTO #MovingWaitListItems
SELECT WaitlistItemID, WaitlistItemGUID
FROM SXAAMWLWaitList
Where WaitlistItemID = #value
select * from #MovingWaitListItems
set #pos = CHARINDEX(',', #WaitlistItemID, #pos+#len) +1
print #pos
END
For above, i would like to use of xml node method instead of use of loops with substring(), CHARINDEX() function
declare #WaitlistItemID varchar(max)='10008,10009,10010,10011'
declare #MovingWaitListItems table ( WaitlistItemID int, WaitlistItemGUID numeric(16,0))
;with cte as
(
select
a.value('.', 'varchar(max)') [WaitlistItemID] from
(
select CAST('<m>'+REPLACE(#WaitlistItemID, ',', '</m><m>')+'</m>' as xml) as waitlist
) w cross apply waitlist.nodes ('/m') as split(a)
)
INSERT INTO #MovingWaitListItems
select S.WaitlistItemID, S.WaitlistItemGUID from SXAAMWLWaitList S
JOIN CTE C ON C.[WaitlistItemID] = S.WaitlistItemID
select * from #MovingWaitListItems
For the moment I'll give you the benefit of the doubt and assume that there is some good reason for #WaitlistItemID to be a comma-delimited string. Your while loop condition says that processing should continue as long as a comma exists somewhere in the part of the string you haven't parsed yet. Of course, as Juan Carlos Oropeza points out in his answer, this will always be false for the last item in your list, because you haven't placed a comma after the last value (10011).
However, I would not recommend repeating the insert statement after the loop as in Juan's answer, because there's an easy way to avoid the duplication: just reformulate the loop so its condition instead asks whether there is any part of the string you haven't processed yet. For instance:
declare #cursorPosition int = 1;
declare #commaPosition int;
declare #valueLength int;
declare #value varchar(max);
declare #WaitlistItemID varchar(max)='10008,10009,10010,10011';
declare #MovingWaitListItems table (WaitlistItemID int, WaitlistItemGUID numeric(16,0));
while #cursorPosition < len(#WaitlistItemID)
begin
set #commaPosition = charindex(',', #WaitlistItemID, #cursorPosition);
set #valueLength = case
when #commaPosition = 0 then len(#WaitlistItemID) - #cursorPosition + 1
else #commaPosition - #cursorPosition
end;
set #value = substring(#WaitlistItemID, #cursorPosition, #valueLength);
set #cursorPosition = #cursorPosition + #valueLength + 1;
insert #MovingWaitListItems
select WaitlistItemID, WaitlistItemGUID
from SXAAMWLWaitList
where WaitlistItemID = #value;
end;
select * from #MovingWaitListItems;
Hopefully this answers the question of why your loop isn't performing as you expect. The question I have for you is why you're resorting to parsing a delimited string in SQL in the first place. If #WaitlistItemID is a table of identifiers instead of a delimited string, then the equivalent logic is:
declare #WaitlistItemID table (ID int);
insert #WaitlistItemID values (10008),(10009),(10010),(10011);
declare #MovingWaitListItems table (WaitlistItemID int, WaitlistItemGUID numeric(16,0));
insert #MovingWaitListItems
select W.WaitlistItemID, W.WaitlistItemGUID
from
SXAAMWLWaitList W
inner join #WaitlistItemID ID on W.WaitlistItemID = ID.ID;
select * from #MovingWaitListItems;
Aside from being much easier to understand, this is also going to have vastly better performance than the equivalent iterative solution; set-based logic like this is SQL's forte. while loops in SQL are something you should fall back on only when it's absolutely necessary.
If, in your actual application, #MovingWaitListItems needs to be a parameter to a stored procedure (or similar), you can enable that functionality by creating a custom type. For instance:
create type dbo.IdentifierList as table (ID int);
You can use this type for parameters to stored procedures and functions like so:
create procedure DoSomething(#WaitlistItemID dbo.IdentifierList readonly)
as
select W.WaitlistItemID, W.WaitlistItemGUID
from
SXAAMWLWaitList W
inner join #WaitlistItemID ID on W.WaitlistItemID = ID.ID;
And invoke it like this:
declare #WaitlistItemID dbo.IdentifierList;
insert #WaitlistItemID values (10008),(10009),(10010),(10011);
exec DoSomething #WaitlistItemID;
Perhaps you're already aware of all this and there is a good reason for #WaitlistItemID to be a delimited string. For instance, maybe you're maintaining a stored procedure that was written to accept a delimited string as a parameter and you're not able to refactor the caller(s) to use a table-type parameter instead. In that case, I would still recommend a change to your original design: instead of writing the parsing logic directly into this specific application, consider writing yourself a generic function that takes a delimited string and outputs a table, as described in questions like this one, so you can reuse it the next time a similar scenario comes up instead of having to struggle with the parsing logic again.
Imagine if you have a #WaitlistItemID with a single element 10100 you will also skip it because your loop condition will be false.
while CHARINDEX(',', #WaitlistItemID, #pos+1)>0
Same is true for the last element. So you have to add another interaction at the end of the loop
while ...
begin
end
set #len = LEN(#WaitlistItemID)
if #len > 0
BEGIN
set #value = SUBSTRING(#WaitlistItemID, #pos, #len)
INSERT INTO #MovingWaitListItems
SELECT WaitlistItemID, WaitlistItemGUID
FROM SXAAMWLWaitList
Where WaitlistItemID = #value
END
Problem is related to the number of commas i think.
You can add one more item as dummy, you will see 10011 when you add a comma like this:
declare #WaitlistItemID varchar(max)='10008,10009,10010,10011,'
or
declare #WaitlistItemID varchar(max)='10008,10009,10010,10011,0'

how to use declare variable in select query in stored procedure using sql server

Hello I want to concate two things one is string and other is int variable. Now, these thing I want to store in one variable and use that variable in select query as a into type to create a temptable in stored procedure using sql server.
Here is my query
USE [FlightExamSoftware]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- For Storing Question in Temp table
-- EXEC [GetQuestionListPerSubjectRatioWise] 1,11
ALTER PROCEDURE [dbo].[GetQuestionListPerSubjectRatioWise]
#SubjectID INT,
#NumberOfQue INT,
#UserID int
AS
BEGIN
DECLARE #strQuery VARCHAR(MAX);
DECLARE #PerChapQue INT;
DECLARE #tempTable VARCHAR(MAX) = 'tempTestUser' + #UserID;
SELECT #PerChapQue = COUNT(appQueID)/#NumberOfQue FROM tblQuestion WHERE appQueSubID=#SubjectID
SELECT COUNT(appQueID)/#PerChapQue ChapwiseQue
,CASE WHEN COUNT(appQueID)>=#PerChapQue THEN COUNT(appQueID)/#PerChapQue ELSE 1 END ChapWiseQuePlusOne
,appQueChapID into #tempTable
FROM tblQuestion
WHERE appQueSubID=#SubjectID
GROUP BY appQueChapID
END
Now, I am talking about these line
DECLARE #tempTable VARCHAR(MAX) = 'tempTestUser' + #UserID;
In these line two things are concate one is string and other is int. And store in varchar variable.
And use in following select query i.e.
SELECT COUNT(appQueID)/#PerChapQue ChapwiseQue
,CASE WHEN COUNT(appQueID)>=#PerChapQue THEN COUNT(appQueID)/#PerChapQue ELSE 1 END ChapWiseQuePlusOne
,appQueChapID into #tempTable
FROM tblQuestion
WHERE appQueSubID=#SubjectID
GROUP BY appQueChapID
END
Now, in these query I want to create a temptable named #tempTable.
But, in these line it showing error i.e. Incorrect syntax near '#tempTable'.
Confuse that where is the syntax is wrong.
Thank You.
There are a number of things wrong with your code.
When concatenating an int to a string, you must first cast the int to varchar. Otherwise, SQL Server will try to implicitly convert the string to int, that will result with an error.
So this: DECLARE #tempTable VARCHAR(MAX) = 'tempTestUser' + #UserID; should become this:
DECLARE #tempTable VARCHAR(MAX) = 'tempTestUser' + CAST(#UserID AS VARCHAR(11)); (you need 11 chars to be able to fit the minimum value of int: -2,147,483,648)
You can't use select...into with a table variable.
You can only use it for actual tables (temporary or regular).
your #tempTable isn't even a table variable (not that it will help with a select...into).
Even if you would use select...into the correct way, unless you are going to use a global temporary table (and that doesn't come without it's risks), Unless your stored procedure uses this temporary table later on, it will be useless, since temporary tables are bound to scope.
Taking all of that into consideration I'm not sure what output you are actually looking for. If you could edit your question to include the desired output of your stored procedure as well as some sample data as DDL+DML, it would be easier to help you write better code.
Hope this Dynamic Query helps you:
Try like this:
USE [FlightExamSoftware]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- For Storing Question in Temp table
-- EXEC [GetQuestionListPerSubjectRatioWise] 1,11
ALTER PROCEDURE [dbo].[GetQuestionListPerSubjectRatioWise]
#SubjectID INT,
#NumberOfQue INT,
#UserID int
AS
BEGIN
DECLARE #strQuery VARCHAR(MAX);
DECLARE #PerChapQue INT;
DECLARE #tempTable VARCHAR(MAX) = 'tempTestUser' + CAST(#UserID AS VARCHAR);
SELECT #PerChapQue = COUNT(appQueID)/#NumberOfQue FROM tblQuestion WHERE appQueSubID=#SubjectID
SET #strQuery='
SELECT COUNT(appQueID)/'+CAST(#PerChapQue AS VARCHAR)+' ChapwiseQue
,CASE WHEN COUNT(appQueID)>='+CAST(#PerChapQue AS VARCHAR)+' THEN COUNT(appQueID)/'+CAST(#PerChapQue AS VARCHAR)+' ELSE 1 END ChapWiseQuePlusOne
,appQueChapID
INTO '+#tempTable+'
FROM tblQuestion
WHERE appQueSubID='+CAST(#SubjectID AS VARCHAR)+'
GROUP BY appQueChapID
/*.................................
And you have to use the temp table inside the String only
.................................*/
'
EXEC (#strQuery)
END

TSQL Procedure returning multiple rows of data

I'm busy trying to rewrite some PostgreSQL stored procedures/functions for SQL Server 2014s TSQL.
I am struggling to return my values from this stored procedure though, this one is just a test but I am trying to return multiple rows of data in this case the for the two variables si_code and co_desc.
I have my procedure as follows (as a test)
if (object_id('p_get_serial')) is not null
drop procedure p_get_serial
go
create procedure p_get_serial(#par01 char(20), #par02 integer)
as
declare
#co_num integer,
#co_desc char(20),
#si_code char(20),
#log char(40)
declare mycur cursor for
select co_num, co_desc
from colours
where co_num <= #par02
open mycur
fetch next from mycur into #co_num,
#co_desc
while ##FETCH_STATUS = 0
begin
set #si_code = ''
select #si_code = si_code
from sitems
where si_co_num = #co_num
set #log = #co_desc + ' ' + #si_code
raiserror(#log,0,1) with nowait
fetch next from mycur into #co_num, #co_desc
end
close mycur deallocate mycur
go
exec p_get_serial #par01 = 'paramater01', #par02 = 10
what is the best way to return my results knowing that there will be several rows?
In T-SQL you do not need to declare a cursor. Just select what you need and it will be available to the client app.
Cursor is Oracle/DB2/PostgreSQL etc way of returning data. SQL Server does not need it.
create procedure p
as
select 1 as a
returns a recordset containing one record with one column.
create procedure p
as
select 1 as a, 'a' as b
union select 2, 'b'
returns two rows each with two columns.
Example of a more complex processing before returning a result set:
create procedure p
as
begin
declare #a int, #b varchar(10)
select #a = 1
select #b = convert(varchar(10), #a)
select #a = #a + 1
select #a as a, #b as b -- this will be the resultset returned to the client
end
All you need to do is, just save the data for each row in a temp table or table variable and just write a SELECT statement at the the end of the Stored Procedure.
Your question is not clear what you need exactly, you have a cursor and while loop, they seem to be redundant

SQL variable to hold list of integers

I'm trying to debug someone else's SQL reports and have placed the underlying reports query into a query windows of SQL 2012.
One of the parameters the report asks for is a list of integers. This is achieved on the report through a multi-select drop down box. The report's underlying query uses this integer list in the where clause e.g.
select *
from TabA
where TabA.ID in (#listOfIDs)
I don't want to modify the query I'm debugging but I can't figure out how to create a variable on the SQL Server that can hold this type of data to test it.
e.g.
declare #listOfIDs int
set listOfIDs = 1,2,3,4
There is no datatype that can hold a list of integers, so how can I run the report query on my SQL Server with the same values as the report?
Table variable
declare #listOfIDs table (id int);
insert #listOfIDs(id) values(1),(2),(3);
select *
from TabA
where TabA.ID in (select id from #listOfIDs)
or
declare #listOfIDs varchar(1000);
SET #listOfIDs = ',1,2,3,'; --in this solution need put coma on begin and end
select *
from TabA
where charindex(',' + CAST(TabA.ID as nvarchar(20)) + ',', #listOfIDs) > 0
Assuming the variable is something akin to:
CREATE TYPE [dbo].[IntList] AS TABLE(
[Value] [int] NOT NULL
)
And the Stored Procedure is using it in this form:
ALTER Procedure [dbo].[GetFooByIds]
#Ids [IntList] ReadOnly
As
You can create the IntList and call the procedure like so:
Declare #IDs IntList;
Insert Into #IDs Select Id From dbo.{TableThatHasIds}
Where Id In (111, 222, 333, 444)
Exec [dbo].[GetFooByIds] #IDs
Or if you are providing the IntList yourself
DECLARE #listOfIDs dbo.IntList
INSERT INTO #listofIDs VALUES (1),(35),(118);
You are right, there is no datatype in SQL-Server which can hold a list of integers. But what you can do is store a list of integers as a string.
DECLARE #listOfIDs varchar(8000);
SET #listOfIDs = '1,2,3,4';
You can then split the string into separate integer values and put them into a table. Your procedure might already do this.
You can also use a dynamic query to achieve the same outcome:
DECLARE #SQL nvarchar(8000);
SET #SQL = 'SELECT * FROM TabA WHERE TabA.ID IN (' + #listOfIDs + ')';
EXECUTE (#SQL);
Note: I haven't done any sanitation on this query, please be aware that it's vulnerable to SQL injection. Clean as required.
For SQL Server 2016+ and Azure SQL Database, the STRING_SPLIT function was added that would be a perfect solution for this problem. Here is the documentation:
https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql
Here is an example:
/*List of ids in a comma delimited string
Note: the ') WAITFOR DELAY ''00:00:02''' is a way to verify that your script
doesn't allow for SQL injection*/
DECLARE #listOfIds VARCHAR(MAX) = '1,3,a,10.1,) WAITFOR DELAY ''00:00:02''';
--Make sure the temp table was dropped before trying to create it
IF OBJECT_ID('tempdb..#MyTable') IS NOT NULL DROP TABLE #MyTable;
--Create example reference table
CREATE TABLE #MyTable
([Id] INT NOT NULL);
--Populate the reference table
DECLARE #i INT = 1;
WHILE(#i <= 10)
BEGIN
INSERT INTO #MyTable
SELECT #i;
SET #i = #i + 1;
END
/*Find all the values
Note: I silently ignore the values that are not integers*/
SELECT t.[Id]
FROM #MyTable as t
INNER JOIN
(SELECT value as [Id]
FROM STRING_SPLIT(#listOfIds, ',')
WHERE ISNUMERIC(value) = 1 /*Make sure it is numeric*/
AND ROUND(value,0) = value /*Make sure it is an integer*/) as ids
ON t.[Id] = ids.[Id];
--Clean-up
DROP TABLE #MyTable;
The result of the query is 1,3
In the end i came to the conclusion that without modifying how the query works i could not store the values in variables. I used SQL profiler to catch the values and then hard coded them into the query to see how it worked. There were 18 of these integer arrays and some had over 30 elements in them.
I think that there is a need for MS/SQL to introduce some aditional datatypes into the language. Arrays are quite common and i don't see why you couldn't use them in a stored proc.
There is a new function in SQL called string_split if you are using list of string.
Ref Link STRING_SPLIT (Transact-SQL)
DECLARE #tags NVARCHAR(400) = 'clothing,road,,touring,bike'
SELECT value
FROM STRING_SPLIT(#tags, ',')
WHERE RTRIM(value) <> '';
you can pass this query with in as follows:
SELECT *
FROM [dbo].[yourTable]
WHERE (strval IN (SELECT value FROM STRING_SPLIT(#tags, ',') WHERE RTRIM(value) <> ''))
I use this :
1-Declare a temp table variable in the script your building:
DECLARE #ShiftPeriodList TABLE(id INT NOT NULL);
2-Allocate to temp table:
IF (SOME CONDITION)
BEGIN
INSERT INTO #ShiftPeriodList SELECT ShiftId FROM [hr].[tbl_WorkShift]
END
IF (SOME CONDITION2)
BEGIN
INSERT INTO #ShiftPeriodList
SELECT ws.ShiftId
FROM [hr].[tbl_WorkShift] ws
WHERE ws.WorkShift = 'Weekend(VSD)' OR ws.WorkShift = 'Weekend(SDL)'
END
3-Reference the table when you need it in a WHERE statement :
INSERT INTO SomeTable WHERE ShiftPeriod IN (SELECT * FROM #ShiftPeriodList)
You can't do it like this, but you can execute the entire query storing it in a variable.
For example:
DECLARE #listOfIDs NVARCHAR(MAX) =
'1,2,3'
DECLARE #query NVARCHAR(MAX) =
'Select *
From TabA
Where TabA.ID in (' + #listOfIDs + ')'
Exec (#query)

Is there a way to make a TSQL variable constant?

Is there a way to make a TSQL variable constant?
No, but you can create a function and hardcode it in there and use that.
Here is an example:
CREATE FUNCTION fnConstant()
RETURNS INT
AS
BEGIN
RETURN 2
END
GO
SELECT dbo.fnConstant()
One solution, offered by Jared Ko is to use pseudo-constants.
As explained in SQL Server: Variables, Parameters or Literals? Or… Constants?:
Pseudo-Constants are not variables or parameters. Instead, they're simply views with one row, and enough columns to support your constants. With these simple rules, the SQL Engine completely ignores the value of the view but still builds an execution plan based on its value. The execution plan doesn't even show a join to the view!
Create like this:
CREATE SCHEMA ShipMethod
GO
-- Each view can only have one row.
-- Create one column for each desired constant.
-- Each column is restricted to a single value.
CREATE VIEW ShipMethod.ShipMethodID AS
SELECT CAST(1 AS INT) AS [XRQ - TRUCK GROUND]
,CAST(2 AS INT) AS [ZY - EXPRESS]
,CAST(3 AS INT) AS [OVERSEAS - DELUXE]
,CAST(4 AS INT) AS [OVERNIGHT J-FAST]
,CAST(5 AS INT) AS [CARGO TRANSPORT 5]
Then use like this:
SELECT h.*
FROM Sales.SalesOrderHeader h
JOIN ShipMethod.ShipMethodID const
ON h.ShipMethodID = const.[OVERNIGHT J-FAST]
Or like this:
SELECT h.*
FROM Sales.SalesOrderHeader h
WHERE h.ShipMethodID = (SELECT TOP 1 [OVERNIGHT J-FAST] FROM ShipMethod.ShipMethodID)
My workaround to missing constans is to give hints about the value to the optimizer.
DECLARE #Constant INT = 123;
SELECT *
FROM [some_relation]
WHERE [some_attribute] = #Constant
OPTION( OPTIMIZE FOR (#Constant = 123))
This tells the query compiler to treat the variable as if it was a constant when creating the execution plan. The down side is that you have to define the value twice.
No, but good old naming conventions should be used.
declare #MY_VALUE as int
There is no built-in support for constants in T-SQL. You could use SQLMenace's approach to simulate it (though you can never be sure whether someone else has overwritten the function to return something else…), or possibly write a table containing constants, as suggested over here. Perhaps write a trigger that rolls back any changes to the ConstantValue column?
Prior to using a SQL function run the following script to see the differences in performance:
IF OBJECT_ID('fnFalse') IS NOT NULL
DROP FUNCTION fnFalse
GO
IF OBJECT_ID('fnTrue') IS NOT NULL
DROP FUNCTION fnTrue
GO
CREATE FUNCTION fnTrue() RETURNS INT WITH SCHEMABINDING
AS
BEGIN
RETURN 1
END
GO
CREATE FUNCTION fnFalse() RETURNS INT WITH SCHEMABINDING
AS
BEGIN
RETURN ~ dbo.fnTrue()
END
GO
DECLARE #TimeStart DATETIME = GETDATE()
DECLARE #Count INT = 100000
WHILE #Count > 0 BEGIN
SET #Count -= 1
DECLARE #Value BIT
SELECT #Value = dbo.fnTrue()
IF #Value = 1
SELECT #Value = dbo.fnFalse()
END
DECLARE #TimeEnd DATETIME = GETDATE()
PRINT CAST(DATEDIFF(ms, #TimeStart, #TimeEnd) AS VARCHAR) + ' elapsed, using function'
GO
DECLARE #TimeStart DATETIME = GETDATE()
DECLARE #Count INT = 100000
DECLARE #FALSE AS BIT = 0
DECLARE #TRUE AS BIT = ~ #FALSE
WHILE #Count > 0 BEGIN
SET #Count -= 1
DECLARE #Value BIT
SELECT #Value = #TRUE
IF #Value = 1
SELECT #Value = #FALSE
END
DECLARE #TimeEnd DATETIME = GETDATE()
PRINT CAST(DATEDIFF(ms, #TimeStart, #TimeEnd) AS VARCHAR) + ' elapsed, using local variable'
GO
DECLARE #TimeStart DATETIME = GETDATE()
DECLARE #Count INT = 100000
WHILE #Count > 0 BEGIN
SET #Count -= 1
DECLARE #Value BIT
SELECT #Value = 1
IF #Value = 1
SELECT #Value = 0
END
DECLARE #TimeEnd DATETIME = GETDATE()
PRINT CAST(DATEDIFF(ms, #TimeStart, #TimeEnd) AS VARCHAR) + ' elapsed, using hard coded values'
GO
If you are interested in getting optimal execution plan for a value in the variable you can use a dynamic sql code. It makes the variable constant.
DECLARE #var varchar(100) = 'some text'
DECLARE #sql varchar(MAX)
SET #sql = 'SELECT * FROM table WHERE col = '''+#var+''''
EXEC (#sql)
For enums or simple constants, a view with a single row has great performance and compile time checking / dependency tracking ( cause its a column name )
See Jared Ko's blog post https://blogs.msdn.microsoft.com/sql_server_appendix_z/2013/09/16/sql-server-variables-parameters-or-literals-or-constants/
create the view
CREATE VIEW ShipMethods AS
SELECT CAST(1 AS INT) AS [XRQ - TRUCK GROUND]
,CAST(2 AS INT) AS [ZY - EXPRESS]
,CAST(3 AS INT) AS [OVERSEAS - DELUXE]
, CAST(4 AS INT) AS [OVERNIGHT J-FAST]
,CAST(5 AS INT) AS [CARGO TRANSPORT 5]
use the view
SELECT h.*
FROM Sales.SalesOrderHeader
WHERE ShipMethodID = ( select [OVERNIGHT J-FAST] from ShipMethods )
Okay, lets see
Constants are immutable values which are known at compile time and do not change for the life of the program
that means you can never have a constant in SQL Server
declare #myvalue as int
set #myvalue = 5
set #myvalue = 10--oops we just changed it
the value just changed
Since there is no build in support for constants, my solution is very simple.
Since this is not supported:
Declare Constant #supplement int = 240
SELECT price + #supplement
FROM what_does_it_cost
I would simply convert it to
SELECT price + 240/*CONSTANT:supplement*/
FROM what_does_it_cost
Obviously, this relies on the whole thing (the value without trailing space and the comment) to be unique. Changing it is possible with a global search and replace.
There are no such thing as "creating a constant" in database literature. Constants exist as they are and often called values. One can declare a variable and assign a value (constant) to it. From a scholastic view:
DECLARE #two INT
SET #two = 2
Here #two is a variable and 2 is a value/constant.
SQLServer 2022 (currently only as Preview available) is now able to Inline the function proposed by SQLMenace, this should prevent the performance hit described by some comments.
CREATE FUNCTION fnConstant() RETURNS INT AS BEGIN RETURN 2 END GO
SELECT is_inlineable FROM sys.sql_modules WHERE [object_id]=OBJECT_ID('dbo.fnConstant');
is_inlineable
1
SELECT dbo.fnConstant()
ExecutionPlan
To test if it also uses the value coming from the Function, I added a second function returning value "1"
CREATE FUNCTION fnConstant1()
RETURNS INT
AS
BEGIN
RETURN 1
END
GO
Create Temp Table with about 500k rows with Value 1 and 4 rows with Value 2:
DROP TABLE IF EXISTS #temp ;
create table #temp (value_int INT)
DECLARE #counter INT;
SET #counter = 0
WHILE #counter <= 500000
BEGIN
INSERT INTO #temp VALUES (1);
SET #counter = #counter +1
END
SET #counter = 0
WHILE #counter <= 3
BEGIN
INSERT INTO #temp VALUES (2);
SET #counter = #counter +1
END
create index i_temp on #temp (value_int);
Using the describe plan we can see that the Optimizer expects 500k values for
select * from #temp where value_int = dbo.fnConstant1(); --Returns 500001 rows
Constant 1
and 4 rows for
select * from #temp where value_int = dbo.fnConstant(); --Returns 4rows
Constant 2
Robert's performance test is interesting. And even in late 2022, the scalar functions are much slower (by an order of magnitude) than variables or literals. A view (as suggested mbobka) is somewhere in-between when used for this same test.
That said, using a loop like that in SQL Server is not something I'd ever do, because I'd normally be operating on a whole set.
In SQL 2019, if you use schema-bound functions in a set operation, the difference is much less noticeable.
I created and populated a test table:
create table #testTable (id int identity(1, 1) primary key, value tinyint);
And changed the test so that instead of looping and changing a variable, it queries the test table and returns true or false depending on the value in the test table, e.g.:
insert #testTable(value)
select case when value > 127
then #FALSE
else #TRUE
end
from #testTable with(nolock)
I tested 5 scenarios:
hard-coded values
local variables
scalar functions
a view
a table-valued function
running the test 10 times, yielded the following results:
scenario
min
max
avg
scalar functions
233
259
240
hard-coded values
236
265
243
local variables
235
278
245
table-valued function
243
272
253
view
244
267
254
Suggesting to me, that for set-based work in (at least) 2019 and better, there's not much in it.
set nocount on;
go
-- create test data table
drop table if exists #testTable;
create table #testTable (id int identity(1, 1) primary key, value tinyint);
-- populate test data
insert #testTable (value)
select top (1000000) convert(binary (1), newid())
from sys.all_objects a
, sys.all_objects b
go
-- scalar function for True
drop function if exists fnTrue;
go
create function dbo.fnTrue() returns bit with schemabinding as
begin
return 1
end
go
-- scalar function for False
drop function if exists fnFalse;
go
create function dbo.fnFalse () returns bit with schemabinding as
begin
return 0
end
go
-- table-valued function for booleans
drop function if exists dbo.tvfBoolean;
go
create function tvfBoolean() returns table with schemabinding as
return
select convert(bit, 1) as true, convert(bit, 0) as false
go
-- view for booleans
drop view if exists dbo.viewBoolean;
go
create view dbo.viewBoolean with schemabinding as
select convert(bit, 1) as true, convert(bit, 0) as false
go
-- create table for results
drop table if exists #testResults
create table #testResults (id int identity(1,1), test int, elapsed bigint, message varchar(1000));
-- define tests
declare #tests table(testNumber int, description nvarchar(100), sql nvarchar(max))
insert #tests values
(1, N'hard-coded values', N'
declare #testTable table (id int, value bit);
insert #testTable(id, value)
select id, case when t.value > 127
then 0
else 1
end
from #testTable t')
, (2, N'local variables', N'
declare #FALSE as bit = 0
declare #TRUE as bit = 1
declare #testTable table (id int, value bit);
insert #testTable(id, value)
select id, case when t.value > 127
then #FALSE
else #TRUE
end
from #testTable t'),
(3, N'scalar functions', N'
declare #testTable table (id int, value bit);
insert #testTable(id, value)
select id, case when t.value > 127
then dbo.fnFalse()
else dbo.fnTrue()
end
from #testTable t'),
(4, N'view', N'
declare #testTable table (id int, value bit);
insert #testTable(id, value)
select id, case when value > 127
then b.false
else b.true
end
from #testTable t with(nolock), viewBoolean b'),
(5, N'table-valued function', N'
declare #testTable table (id int, value bit);
insert #testTable(id, value)
select id, case when value > 127
then b.false
else b.true
end
from #testTable with(nolock), dbo.tvfBoolean() b')
;
declare #testNumber int, #description varchar(100), #sql nvarchar(max)
declare #testRuns int = 10;
-- execute tests
while #testRuns > 0 begin
set #testRuns -= 1
declare testCursor cursor for select testNumber, description, sql from #tests;
open testCursor
fetch next from testCursor into #testNumber, #description, #sql
while ##FETCH_STATUS = 0 begin
declare #TimeStart datetime2(7) = sysdatetime();
execute sp_executesql #sql;
declare #TimeEnd datetime2(7) = sysdatetime()
insert #testResults(test, elapsed, message)
select #testNumber, datediff_big(ms, #TimeStart, #TimeEnd), #description
fetch next from testCursor into #testNumber, #description, #sql
end
close testCursor
deallocate testCursor
end
-- display results
select test, message, count(*) runs, min(elapsed) as min, max(elapsed) as max, avg(elapsed) as avg
from #testResults
group by test, message
order by avg(elapsed);
The best answer is from SQLMenace according to the requirement if that is to create a temporary constant for use within scripts, i.e. across multiple GO statements/batches.
Just create the procedure in the tempdb then you have no impact on the target database.
One practical example of this is a database create script which writes a control value at the end of the script containing the logical schema version. At the top of the file are some comments with change history etc... But in practice most developers will forget to scroll down and update the schema version at the bottom of the file.
Using the above code allows a visible schema version constant to be defined at the top before the database script (copied from the generate scripts feature of SSMS) creates the database but used at the end. This is right in the face of the developer next to the change history and other comments, so they are very likely to update it.
For example:
use tempdb
go
create function dbo.MySchemaVersion()
returns int
as
begin
return 123
end
go
use master
go
-- Big long database create script with multiple batches...
print 'Creating database schema version ' + CAST(tempdb.dbo.MySchemaVersion() as NVARCHAR) + '...'
go
-- ...
go
-- ...
go
use MyDatabase
go
-- Update schema version with constant at end (not normally possible as GO puts
-- local #variables out of scope)
insert MyConfigTable values ('SchemaVersion', tempdb.dbo.MySchemaVersion())
go
-- Clean-up
use tempdb
drop function MySchemaVersion
go

Resources