generate row number by using variable in mssql - sql-server

In MYSQL, I could use variable to generate the row number like below:
SET #num=0;
SELECT #num:=#num+1 rownum,id FROM test;
How should I do this in MSSQL? I tried below code:
DECLARE #num int
SET #num=0;
SELECT #num=#num+1 ,id FROM test;
then I got the error message:
A SELECT statement that assigns a value to a variable must not be
combined with data-retrieval operations.
I know there're some built-in functions in mssql to generate the row number, but I'd like to know how to use variables in this case to solve the issue.
Appreciate if someone could help on this. :)

As the error message says, you can't do variable assignment in a select statement that also does data retrieval.
You can assign a variable using a select statement...
declare #myInt int;
select #myvariable = 1; -- works just like "set" in this case
And you can do data retrieval...
select someColumn from MyTable
But not both at the same time.
You can assign a value to a variable when selecting from a table, as long as you don't also select columns that don't get assigned to variables.
declare #myInt int;
select #myInt = myIntColumn from MyTable
If MyTable has a lot of rows in it, it's not clear which row will be used to populate the value of #myInt. SQL is "allowed" to pick any row it wants.
Of course, in your case that doesn't matter, since you're assigning a literal value to the variable. You can do this, where by "can", I mean the syntax won't be rejected:
declare #myInt int = 0;
select #myInt = #myInt + 1
from MyTable
But don't do this. It's an interesting quirk, which I am showing it to you with the good faith assumption that you won't use it. It is not supported and should not be relied upon.
Jeff Moden wrote an article about this where he goes over the dangers.
But don't do it even if you follow his rules.

If you want to create a rownum based on the variable value, you can do that by updating the value in the table like below:
drop table if exists #t
create table #t (id int, rownum int)
insert #t(id) select 100
insert #t(id) select 200
insert #t(id) select 300
insert #t(id) select 400
declare #i int
set #i = 0
update #t set rownum = #i, #i = #i + 1
select * from #t
order by rownum

Related

Issue with SQL INSERT Trigger

I'm reaching out for some help on this trigger I'm trying to get working.
Basically this is what I'm trying to do.
We have DMS software that writes to a Database and on a particular INSERT value I want the trigger to fire.
This is an example of an INSERT statement that will get processed.
INSERT INTO DOCSADM.ACTIVITYLOG (CR_IN_USE,ACTIVITY_DESC,BILLED_ON,BILLABLE,PAGES,KEYSTROKES,
TYPE_TIME,ELAPSED_TIME,TYPIST,AUTHOR,START_DATE,ACTIVITY_TYPE,REF_DOCUMENT,REF_LIBRARY,APPLICATION,VERSION_LABEL,DOCNUMBER,SYSTEM_ID)
VALUES ('','DOCSFusion','1753-01-01','',0,0,0,0,1920,1920,'2020-08-26T10:17:56',**115**,0,-1,1173,'',75,3252)
but I only want the trigger to fire when we see a value of 115 for the bold section in the INSERT statement (the Activity_type value).
For all other values that re not 115 I don't want to do anything.
This is what I have so far:
CREATE TRIGGER BW_TRIGGER
ON DOCSADM.ACTIVITYLOG
AFTER INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
--Declare some variable and set it as a value of 115.
--Example:
DECLARE #AlogType int = (SELECT I.ACTIVITY_TYPE FROM DOCSADM.ACTIVITYLOG A, INSERTED I) --This is the value you are looking for regarding the DM client/Matter actitivty type.
DECLARE #AlogDesc varchar(32) = (Select i.ACTIVITY_DESC from docsadm.ACTIVITYLOG A, INSERTED I)
--Next, you should have a fork or path in your trigger to determine how it proceeds.
--Path 1: The #AlogType value matches the inserted value so you want to process the rest of the trigger. Example path – “ProcessTrigger:”
--Path 2: The #AlogType value does NOT match the inserted value, you want to exit the trigger. Example Path – “ExitTrigger:”
IF #AlogType <> 115
GOTO TriggerExit;
ELSE
Begin
/*Create first temp table to collect insert values*/ --This table will have the SysID Value and the corresponding docnumber for the items you want.
--You can add whatever other values you think you need.
CREATE TABLE #TempSet1
(
AlogsysID INT,
Docnum INT,
AlogDate Varchar(64),
AlogTypist INT,
AlogAuthor INT,
AlogDesc varchar(32),
ALOGVER varchar(10),
ALOG_MATTER INT
)
INSERT INTO #TempSet1 (AlogsysID,Docnum,AlogDate,AlogTypist,AlogAuthor, ALOG_MATTER)
--SELECT  You SELECT STATEMENT WILL GO HERE MODIFIED TO POPULATE THE TABLE WITH THE DOCNUMBERS YOU WANT!!
select top 1 System_id, docnumber, LAST_ACCESS_DATE, TYPIST, AUTHOR, MATTER from docsadm.PROFILE where EXISTS (SELECT CLIENT.SYSTEM_ID FROM DOCSADM.CLIENT INNER JOIN DOCSADM.MATTER ON MATTER.CLIENT_ID = CLIENT.SYSTEM_ID
WHERE MATTER.SYSTEM_ID =#AlogDesc OR INH_LUP_SEC_FROM IS NULL OR INH_LUP_SEC_FROM = 0) AND MATTER=#AlogDesc
/*Set variable #SysID as the LASTKEY value -1. This will be used to set the SysID column on the #TempSet table*/
--DECLARE #SysID INT = (SELECT LASTKEY FROM DOCSADM.SEQ_SYSTEMKEY) -1;
/*Set the SysID value for every row on the #TempSet1 table as the #SysID variable +1*/
--UPDATE #TempSet1
--SET #SysID = AlogsysID = #SysID + 1
--Your #TempSet should now be set with ALL of the System_IDs and Docnumbers necessary for your insert!!!!—
--Verify this by doing a select against the #TempSet1 Table
SELECT * FROM #TempSet1;
--Next you need to set the SystemID to the correct value for future processing. To do this, we need to get a total count from the #TempSet table.
/*Set a variable to update the NEXTKEY value on the DOCSADM.SEQ_SYSTEMKEY table. The NEXTKEY value is used for the SYSTEM_ID field*/
--DECLARE #SeqUpdateCount INT = (SELECT COUNT(*) FROM #TempSet1);
/*Update the LASTKEY Value on the SEQ_SYSTEMKEY table to the next available value for DM.*/
--UPDATE DOCSADM.SEQ_SYSTEMKEY SET LASTKEY = LASTKEY+#SeqUpdateCount
--If you have all the values you need in your temp table, you can now insert them into the ACTIVITYLOG table.
--INSERT INTO DOCSADM.ACTIVITY
--(SYSTEM_ID, DOCNUMBER, START_DATE, version, EXT,)
--SELECT
--AlogSysID,Docnum,GETUTCDATE(),BLAH, BLAH
--FROM #TableSet1
INSERT INTO DOCSADM.ACTIVITYLOG
(SYSTEM_ID,
DOCNUMBER,
START_DATE,
TYPIST,
AUTHOR,
ACTIVITY_DESC,
VERSION_LABEL,
ACTIVITY_TYPE)
SELECT
AlogsysID, Docnum,AlogDate,AlogTypist, AlogAuthor, ALOG_MATTER, '',115
FROM #TempSet1;
--Now you need to Drop the Temp Table
DROP TABLE #TempSet1
--Go to the other half of your path above to exit the trigger.
END
TriggerExit:
END
Go
but when I try to run any INSERT statement on this table I get this error message. It doesn't matter if the activity_type has a value of 115 or not
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
I know the issue is with this section of the trigger:
INSERT INTO #TempSet1 (AlogsysID,Docnum,AlogDate,AlogTypist,AlogAuthor, ALOG_MATTER)
--SELECT  You SELECT STATEMENT WILL GO HERE MODIFIED TO POPULATE THE TABLE WITH THE DOCNUMBERS YOU WANT!!
SELECT TOP 1
System_id
, docnumber
, LAST_ACCESS_DATE
, TYPIST
, AUTHOR
, MATTER
FROM docsadm.PROFILE
WHERE EXISTS (SELECT CLIENT.SYSTEM_ID
FROM DOCSADM.CLIENT
INNER JOIN DOCSADM.MATTER
ON MATTER.CLIENT_ID = CLIENT.SYSTEM_ID
WHERE MATTER.SYSTEM_ID =#AlogDesc
OR INH_LUP_SEC_FROM IS NULL
OR INH_LUP_SEC_FROM = 0)
AND MATTER=#AlogDesc
It's the SELECT statement that is causing it to fail.
I know that this statement will bring back multiple rows but I only need the value from one of them so I can use this value for my INSERT. I though having the "select top 1" would do this for me but it's not working like I think it should. What am I missing?
If I had to guess I would say your problem is here:
DECLARE #AlogType int = (SELECT I.ACTIVITY_TYPE FROM DOCSADM.ACTIVITYLOG A, INSERTED I) --This is the value you are looking for regarding the DM client/Matter actitivty type.
DECLARE #AlogDesc varchar(32) = (Select i.ACTIVITY_DESC from docsadm.ACTIVITYLOG A, INSERTED I)
How are ACTIVITYLOG and INSERTED joined in the above ? without a where it would be a CROSS JOIN. Why do you even drag ACTIVITYLOG into it, you can simply use INSERTED. Also please try to stop using implicit joins ( I can see that later down the script you use the proper, more verbose join syntax)
TRY:
DECLARE #AlogType int = (SELECT I.ACTIVITY_TYPE FROM INSERTED I) --This is the value you are looking for regarding the DM client/Matter actitivty type.
DECLARE #AlogDesc varchar(32) = (Select i.ACTIVITY_DESC from INSERTED I)
Be careful that this will work with single inserts only. When you do batched inserts the INSERTED is a table containing multiple rows and you will run into issues again.

Inserting data from one table to another using a loop

I am trying to insert data from one table into another (from table [PPRS] to table [Verify]) where the Caption in PPRS is the same as in table [Master]. Someone suggested i use a loop to insert the data instead of hard coding it, however I am confused as to how to go about it.
Here's my code so far:
Declare #counter int
declare #total int
set #counter = 0
SELECT #total = Count(*) FROM PPRS
while #counter <= #total
begin
set #counter += 1
insert into [Verify]
select [Task_ID],
[Project_StartDate] ,
[PPR_Caption],
[Date]
FROM PPRS
where [PPR_Caption] in (SELECT [Caption] from Master)
end
No data is being inserted (0 rows affected)
The sample data I'm trying to insert:
17286 01/03/2018 MP - Youth Environmental Services (12/15) 15/10/2018
I suggest that this is along the lines of what you want to do:
INSERT INTO [Verify] (some_col) -- you never told us the name of this column
SELECT TOP 10 [PPR_Caption]
FROM PPRS
WHERE [PPR_Caption] IN (SELECT [Caption] FROM MasterRecords)
ORDER BY some_column;
That is, you want to insert ten records into the Verify table, determined by some order in the source PPRS table.
Try to declare specific columns to which you want to make insert. Please provide definition of table PPRS and Verify for more precise help from community.
Anyway idea please find bellow:
Declare #counter int
set #counter = 0
while #counter <= 10
begin
set #counter += 1
insert into [Verify] (NameofColumn1_Table_Verify, NameofColumn2_Table_Verify,...)
select PPRS.NameofColumn1_Table_PPRS,
PPRS.NameofColumn2_Table_PPRS,
...
FROM PPRS
where PPRS.[PPR_Caption] in (SELECT DISTINCT [Caption] from MasterRecords)
end
Anyway I think the loop is unnecesary. One batch should make it. Please write what you want to achieve at the end.

Simple logic but not working as it should

This is what i want to achieve:
So trigger fires on opportunities table when a record with opp_type = 0 is inserted.
The next part of the code just does the calculation which is to pick up the last used number from your custom table and add 1 to it. Stores new value in a variable.
The next part is to do the insert into the user field.
Finally update the custom table to record the last used number.
I am getting the number to increment by one in the NEXTEXP1 table however the user field called O_Quote is not populating via the GUI.
is the code below doing what it should in terms of the explanation above?
by the steps in my trigger it seems the same but the user field is not populating with last number used:
alter TRIGGER [dbo].[Q2] ON [dbo].[AMGR_opportunity_Tbl] AFTER INSERT
AS
BEGIN
Declare #Opp_Type int
Select #Opp_Type = 0 from inserted
If #Opp_Type = 0
BEGIN
SET NOCOUNT ON;
DECLARE #Client_Id varchar(24)
DECLARE #Contact_Number int
DECLARE #NewNumber varchar(250)
DECLARE #NextQNo float
DECLARE #UDFName varchar(50)
DECLARE #GeneratorPrefix varchar(10)
DECLARE #GeneratorLength float
DECLARE #Opptype int
DECLARE #Type_id int
DECLARE #Oppid varchar (24)
--select top 1 nextqno = nextqno from nextexp1
SELECT #NewNumber = NextQno + 1 from dbo.NextEXP1
----insert into user field
insert into O_Quote(Client_Id, Contact_Number, Type_Id, Code_Id, [O_Quote])
values (#Client_Id,0,15,0,#NextQNo)
-------update table with last number used
UPDATE [dbo].[NextEXP1] SET NextQNo = #NewNumber
End
End
GO
#Leonidas199x is right with all points. I can also say that there are too many things that are unclear with that questions and lot's of the data is missing, however this is what I can suggest (this code handles bulk inserts also):
alter TRIGGER [dbo].[Q2] ON [dbo].[AMGR_opportunity_Tbl] AFTER INSERT
AS
BEGIN
DECLARE #NewNumber varchar(250);
SELECT #NewNumber = MAX(NextQno) FROM dbo.NextEXP1; -- I guess that's what you want
insert into O_Quote(Client_Id, Contact_Number, Type_Id, Code_Id, [O_Quote])
select Client_Id, Contact_Number, Type_Id, Code_Id, #NewNumber + row_num
FROM (
SELECT Client_Id, -- once again do not know where this value is taken from
0 Contact_Number,
15 Type_Id,
0 AS Code_Id,
ROW_NUMBER() OVER(order by client_id) row_num
FROM INSERTED WHERE Opp_Type = 0 --I guess that's the right column name
) a;
SELECT #NewNumber = MAX(O_Quote) FROM O_Quote;
UPDATE [dbo].[NextEXP1] SET NextQNo = #NewNumber;
END
Looking into this, I think your logic is a bit off:
Select #Opp_Type = 0 from inserted
This will always evaluate as 0.
You want to use:
SELECT #Opp_Type = i.Opp_Type
FROM inserted AS i;
Where i.Opp_Type is your column name.
Secondly, you declare a bunch of variables, but never set them:
DECLARE #Client_Id varchar(24)
DECLARE #Contact_Number int
DECLARE #NewNumber varchar(250)
DECLARE #NextQNo float
DECLARE #UDFName varchar(50)
DECLARE #GeneratorPrefix varchar(10)
DECLARE #GeneratorLength float
DECLARE #Opptype int
DECLARE #Type_id int
DECLARE #Oppid varchar (24)
And then go on to insert them. You need to set these, if you want to use them later. Should this be:
insert into O_Quote(Client_Id, Contact_Number, Type_Id, Code_Id, [O_Quote])
values (#Client_Id,0,15,0,#NewNumber)
Or do you need to set #NextQNo to be:
SELECT #NextQNo = NextQno from dbo.NextEXP1;
SELECT #NewNumber = #NextQNo + 1;
And lastly, the way this is written will cause you issues if you insert more than one record at a time. You would need to think about a loop, to get that MaxID, which isn't ideal. Can you look at using IDENTITY columns instead?

select TOP (all)

declare #t int
set #t = 10
if (o = 'mmm') set #t = -1
select top(#t) * from table
What if I want generally it resulted with 10 rows, but rarely all of them.
I know I can do this through "SET ROWCOUNT". But is there some variable number, like -1, that causing TOP to result all elements.
The largest possible value that can be passed to TOP is 9223372036854775807 so you could just pass that.
Below I use the binary form for max signed bigint as it is easier to remember as long as you know the basic pattern and that bigint is 8 bytes.
declare #t bigint = case when some_condition then 10 else 0x7fffffffffffffff end;
select top(#t) *
From table
If you dont have an order by clause the top 10 will just be any 10 and optimisation dependant.
If you do have an order by clause to define the top 10 and an index to support it then the plan for the query above should be fine for either possible value.
If you don't have a supporting index and the plan shows a sort you should consider splitting into two queries.
im not sure I understand your question.
But if you sometimes want TOP and other times don't just use if / else construct:
if (condition)
'send TOP
SELECT TOP 10 Blah FROM...
else
SELECT blah1, blah2 FROM...
You can use dynamic SQL (but I, personally, try to avoid dynamic SQL), where you create a string of the statement you want to run from conditions or parameters.
There's also some good information here on how to do it without dynamic SQL:
https://web.archive.org/web/20150520123828/http://sqlserver2000.databases.aspfaq.com:80/how-do-i-use-a-variable-in-a-top-clause-in-sql-server.html
declare #top bigint = NULL
declare #top_max_value bigint = 9223372036854775807
if (#top IS NULL)
begin
set #top = #top_max_value
end
select top(#top) *
from [YourTableName]
a dynamic sql version isn't that's hard to do.
CREATE PROCEDURE [dbo].[VariableTopSelect]
-- Add the parameters for the stored procedure here
#t int
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
declare #sql nvarchar(max)
if (#t=10)
begin
set #sql='select top (10) * from table'
end
else
begin
set #sql='select * from table'
end
exec sp_executesql #sql
END
with this sp, if they send 10 to the sp, it'll select the top 10, otherwise it'll select all.
The best solution I've found is to select the needed columns with all of your conditions into a temporary table, then do your conditional top:
DECLARE #TempTable TABLE(cols...)
INSERT INTO #TempTable
SELECT blah FROM ...
if (condition)
SELECT TOP 10 * FROM #tempTable
else
SELECT * FROM #tempTable
This way you follow DRY, get your conditional TOP, and are just as easy to read.
Cheers.
It is also possible with a UNION and a parameter
SELECT DISTINCT TOP 10
Column1, Column2
FROM Table
WHERE #ShowAllResults = 0
UNION
SELECT DISTINCT
Column1, Column2
FROM Table
WHERE #ShowAllResults = 1
I might be too late now, or getting too old
But I solved that by using Top(100)Percent
This goes around all complexities
Select Top(100)Percent * from tablename;
Use the statement "SET ROWCOUNT #recordCount" at the beginning of the result query.The variable "#recordCount" can be any positive integer. It should be 0 to return all records.
that means , "SET ROWCOUNT 0" will return all records and "SET ROWCOUNT 15" will return only the TOP 15 rows of result set.
Drawback can be the Performance hit when dealing with large number of records. Also the SET ROWCOUNT will be effective throughout the scope of execution of the whole 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