98

Is there a way to make a TSQL variable constant?

TheEmirOfGroofunkistan
  • 5,346
  • 7
  • 35
  • 52

12 Answers12

63

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()
dakab
  • 4,878
  • 8
  • 40
  • 60
SQLMenace
  • 128,762
  • 24
  • 200
  • 224
  • 16
    `WITH SCHEMABINDING` **should** turn this into a 'real' constant (a requirement for a UDF to be seen as deterministic in SQL). I.e. it should land up being cached. Still, +1. – Jonathan Dickinson Jul 23 '12 at 09:13
  • this answer is good, just curious can table columns in sqlserver reference a function as a default value. i could not get this to work – Ab Bennett Sep 04 '18 at 01:50
  • 1
    @JonathanDickinson To be clear, your suggestion is to use `WITH SCHEMABINDING` in the `CREATE FUNCTION` statement (as opposed to in a stored procedure that might be calling the function) -- is that right? – Holistic Developer Jan 08 '19 at 16:00
  • 2
    Yes, in the function. WITH SCHEMABINDING allows SQL to inline "inlined table-valued functions" - so it *also* needs to be in this form: https://gist.github.com/jcdickinson/61a38dedb84b35251da301b128535ceb. The query analyzer won't inline anything without SCHEMABINDING or anything with BEGIN. – Jonathan Dickinson Jan 16 '19 at 23:45
  • 1
    Implications of using non deterministic UDFs: https://docs.microsoft.com/es-es/archive/blogs/sqlprogrammability/improving-query-plans-with-the-schemabinding-option-on-t-sql-udfs – Ochoto Jun 11 '20 at 09:40
  • I think the omission of schemabinding makes this answer deprecated. The performance hit of using scalar UDF in queries _can_ be devastating. – andowero Apr 19 '22 at 19:59
37

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)
Community
  • 1
  • 1
mbobka
  • 391
  • 3
  • 3
27

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.

neves
  • 26,235
  • 24
  • 129
  • 157
John Nilsson
  • 16,631
  • 8
  • 31
  • 42
  • 4
    It helps but it also defeats the purpose of a single definition. – MikeJRamsey56 Dec 18 '18 at 16:46
  • I have found that `OPTION(RECOMPILE)` has the same effect as `OPTION(OPTIMIZE FOR ...)` - and then you don't have to specify the value twice. See [this question](https://stackoverflow.com/q/40432794/989556). That makes this answer my preferred solution to this problem. – Richard Welsh Aug 13 '21 at 10:28
10

No, but good old naming conventions should be used.

declare @MY_VALUE as int
jason saldo
  • 9,562
  • 5
  • 32
  • 40
  • @VictorYarema because sometimes convention is all you need. And because sometimes you have no other good choice. Now, that aside, SQLMenace's answer looks better, I'll agree with you. Even so, the functions name should follow the convention for constants, IMO. It should be named `FN_CONSTANT()`. That way it's clear what it's doing. – tfrascaroli Oct 10 '16 at 06:19
  • This alone won't help when you want the performance benefit. Try Michal D. and John Nilsson's answers for the performance boost as well. – WonderWorker Jan 25 '17 at 16:27
8

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?

Daniel Cassidy
  • 23,529
  • 4
  • 38
  • 52
Sören Kuklau
  • 18,756
  • 7
  • 47
  • 84
7

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
Robert
  • 71
  • 1
  • 1
  • 4
    This is quite old, but for reference here's the result when executed on my server: | `2760ms elapsed, using function` | `2300ms elapsed, using local variable` | `2286ms elapsed, using hard coded values` | – z00l Oct 07 '15 at 12:36
  • 2
    On a dev laptop, with two additional functions without schema binding. ```5570 elapsed, using function ``` | ```406 elapsed, using local variable``` | ```383 elapsed, using hard coded values``` | ```3893 elapsed, using function without schemabinding``` – monkeyhouse Dec 28 '17 at 19:04
  • For comparison, a simple select statement took 4110ms where the select statements alternated between ```select top 1 @m = cv_val from code_values where cv_id = 'C101' ``` and same ```... 'C201' ``` where code_values is dictionary table with 250 vars, there were all on SQL-Server 2016 – monkeyhouse Dec 28 '17 at 19:16
7

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)
NDB
  • 589
  • 5
  • 16
Michal D.
  • 653
  • 6
  • 6
5

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  )
monkeyhouse
  • 2,879
  • 3
  • 24
  • 41
3

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

SQLMenace
  • 128,762
  • 24
  • 200
  • 224
1

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.

Gert-Jan
  • 262
  • 1
  • 7
0

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.

Greg Hurlman
  • 17,517
  • 6
  • 51
  • 85
  • Try Michal D. and John Nilsson's answers for a performance boost as well. – WonderWorker Jan 25 '17 at 16:28
  • Literals are constant by definition. The ascii/unicode (depending on editor) character `2` gets translated into a binary value when assigned at "compile time". The actual value encoded depends on the data type it is being assigned to (int, char, ...). – samus Apr 02 '18 at 19:06
-1

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
Tony Wall
  • 1,376
  • 20
  • 18