Can a cursor hold variables in PL/pgSQL? - database

I know that a cursor can encapsulate a query, but can it also point to a variable value or multiple ones?
For example:
declare
my cursor refcursor;
var_x varchar;
begin
var_x := (select x from table where id = 7);
open mycursor for select(var_x);
end;
Is this possible in PL/pgSQL?

Yes, it can be done:
CREATE OR REPLACE FUNCTION f_value_cursor(_curs refcursor, _id1 int, _id2 int)
RETURNS TABLE (col1 int, col2 text) AS
$func$
DECLARE
var_x text := (SELECT t.col2 FROM tbl t WHERE t.tbl_id = _id1);
BEGIN
OPEN _curs FOR SELECT var_x;
RETURN QUERY
SELECT t.col1, t.col2
FROM tbl t
WHERE t.tbl_id >= _id2;
END
$func$ LANGUAGE plpgsql;
A cursor is good for any query that returns rows - even if it returns a single constant like in the example.
Using an unbound cursor variable as parameter, you can pass a name for the cursor to the function.
I made the function return a table at the same time, since that seems to be what you are after in those last couple of questions.
As mentioned in my previous answer, it is essential that you fetch values from the cursor in the same transaction:
BEGIN;
SELECT * FROM f_value_cursor('mycursor', 1, 2);
FETCH ALL FROM mycursor;
ROLLBACK; -- or COMMIT
SQL Fiddle.
For the record: consider a temporary table instead, which lives for the duration of the session (per default), not just for the transaction. Tables are much more versatile. Cursors make more sense for huge result sets.

Related

How to run series of values through stored procedure

I have a stored procedure that I need to run a list of values through and output into a temp table.
This is the SP: EXEC [SP_ReturnHTML] #zoneid, 1
The first value, I assume, will be a variable and the second value will be hard-coded. I am not able to modify this SP, as it is used in other processes, so I need to run these values through the SP via a cursor or WHILE loop. The values only need to be run through once, so a FAST_FORWARD cursor type may be more ideal, based on some preliminary reading on cursors (of which my experience in is extremely limited). This is what I attempted:
declare #zoneid int = (select zoneid from #values)
declare list cursor fast_forward
for EXEC [SP_ReturnHTML] #zoneid,1
open list
fetch next from list
But when I try to do this, I get the error Incorrect syntax near the keyword 'EXEC'.
The output of this SP, when using #zoneid=14105 (and the hard-coded 1 relates to the fieldgroupid) looks something like the shot below. For clarity, despite using #zoneid=14105, the reason a value of 4054 shows up is due to the way the SP is written, and is intended. The two values relate to a state and county relationship, noted by the first 2 columns, ParentHeaderId and HeaderId. I opted to use 14105 for the example, because the 3 examples in the #values table only retrieve their secondary value and I wanted to avoid confusion here.
The values that I need to run through the SP for the #zoneid are in a table (which has about 3100 rows), which can be exemplified with the following:
create table #values (zoneid int)
insert into #values
values
(13346),
(13347),
(13348)
So very simply put, I need something like the following as a final product (pseudo code):
declare #zoneid INT = (select zoneid from #values)
select * into #results from
(
EXEC [SP_ReturnHTML] #zoneid, 1
)
Something like this:
drop table if exists #results
drop table if exists #Data
go
create or alter procedure [SP_ReturnHTML] #value int, #s varchar(20)
as
begin
select concat(' value=',#value, '; s = ', #s)
end
go
create table #Data (value int, county varchar(30))
insert into #Data
values
(100, 'Baker'),
(101,'Baldwin'),
(102,'Baldwin'),
(103,'Ballard'),
(104,'Baltimore City'),
(105,'Baltimore'),
(106,'Bamberg'),
(107,'Bandera'),
(108,'Banders'),
(109,'Banks'),
(110,'Banner'),
(111,'Bannock'),
(112,'Baraga')
go
create table #results(value nvarchar(200))
declare c cursor local for select value from #Data
declare #value int
open c
fetch next from c into #value
while ##fetch_status = 0
begin
insert into #results(value)
EXEC [SP_ReturnHTML] #value, '1'
fetch next from c into #value
end
go
select *
from #results

FOR DO in SQL Server

Just curios can I do this in SQL Server
FOR
SELECT columns
FROM table_name
DO
---do some logic
--proc call
ENDFOR;
which means for every record from first select do something in DO block.
This perfectly works for Ingres DB, but not sure if it will work with SQL Server, or I should use only cursor?
This Syntax is not supported in SQL Server's T-SQL. But - as you mention yourself in your question - there is CURSOR:
--Some *mockup* data
DECLARE #tbl TABLE(ID INT IDENTITY, SomeData VARCHAR(100));
INSERT INTO #tbl VALUES('Row 1'),('Row 2'),('Row 3');
--Declare variables to puffer all row's values
DECLARE #WorkingVariable VARCHAR(100);
--never forget the `ORDER BY` if sort order matters!
DECLARE cur CURSOR FOR SELECT SomeData FROM #tbl ORDER BY ID;
OPEN cur;
--a first fetch outside of the loop
FETCH NEXT FROM cur INTO #WorkingVariable
--loop until nothing more to read
WHILE ##FETCH_STATUS=0
BEGIN
--Do whatever you like with the value(s) read into your variable(s).
SELECT #WorkingVariable;
--Pick the next value
FETCH NEXT FROM cur INTO #WorkingVariable
END
--Don't forget to get rid of the used resources
CLOSE cur;
DEALLOCATE cur;
But please keep in mind, that using a loop (however it is coded) is procedural thinking and against the principles of set-based thinking. There are very rare situations! where a CURSOR (or any other kind of loop) is the right choice...
SQL Server doesn't do any behind-the-scenes work for building WHILE loops.
One way to do something like this in SQL Server would look like:
declare #indexTable table (fieldIndex bigint identity(1,1), field (whatever your type of field is))
insert into #indexTable(field)
select field
from table_name
declare #pointer bigint = 1
declare #maxIndexValue bigint = (select max(fieldIndex) from #indexTable)
declare #fieldValue (fieldtype)
while #pointer <= #maxIndexValue
BEGIN
select #fieldValue = field from #indexTable where fieldIndex = #pointer
---do some logic
--proc call
set #pointer = #pointer + 1
END
This is an alternative to using a cursor to loop over your rowset.

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

Why does my cursor stop in the middle of a loop?

The code posted here is 'example' code, it's not production code. I've done this to make the problem I'm explaining readable / concise.
Using code similar to that below, we're coming across a strange bug. After every INSERT the WHILE loop is stopped.
table containst 100 rows, when the insert is done after 50 rows then the cursor stops, having only touched the first 50 rows. When the insert is done after 55 it stops after 55, and so on.
-- This code is an hypothetical example written to express
-- an problem seen in production
DECLARE #v1 int
DECLARE #v2 int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO #v1, #v2
WHILE(##FETCH_STATUS=0)
BEGIN
IF(#v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (#v2)
END
FETCH NEXT FROM MyCursor INTO #v1, #v2
END
CLOSE MyCursor
DEALLOCATE MyCursor
There is an AFTER INSERT trigger on table2 which is used to log mutaties on table2 into an third table, aptly named mutations. This contains an cursor which inserts to handle the insert (mutations are logged per-column in an very specific manner, which requires the cursor).
A bit of background: this exists on an set of small support tables. It is an requirement for the project that every change made to the source data is logged, for auditing purposes. The tables with the logging contain things such as bank account numbers, into which vast sums of money will be deposited. There are maximum a few thousand records, and they should only be modified very rarely. The auditing functionality is there to discourage fraud: as we log 'what changed' with 'who did it'.
The obvious, fast and logical way to implement this would be to store the entire row each time an update is made. Then we wouldn't need the cursor, and it would perform an factor better. However the politics of the situation means my hands are tied.
Phew. Now back to the question.
Simplified version of the trigger (real version does an insert per column, and it also inserts the old value):
--This cursor is an hypothetical cursor written to express
--an problem seen in production.
--On UPDATE a new record must be added to table Mutaties for
--every row in every column in the database. This is required
--for auditing purposes.
--An set-based approach which stores the previous state of the row
--is expressly forbidden by the customer
DECLARE #col1 int
DECLARE #col2 int
DECLARE #col1_old int
DECLARE #col2_old int
--Loop through old values next to new values
DECLARE MyTriggerCursor CURSOR FAST_FORWARD FOR
SELECT i.col1, i.col2, d.col1 as col1_old, d.col2 as col2_old
FROM Inserted i
INNER JOIN Deleted d ON i.id=d.id
OPEN MyTriggerCursor
FETCH NEXT FROM MyTriggerCursor INTO #col1, #col2, #col1_old, #col2_old
--Loop through all rows which were updated
WHILE(##FETCH_STATUS=0)
BEGIN
--In production code a few more details are logged, such as userid, times etc etc
--First column
INSERT Mutaties (tablename, columnname, newvalue, oldvalue)
VALUES ('table2', 'col1', #col1, #col1_old)
--Second column
INSERT Mutaties (tablename, columnname, newvalue, oldvalue)
VALUES ('table2', 'col2', #col2, #col1_old)
FETCH NEXT FROM MyTriggerCursor INTO #col1, #col2, #col1_old, #col2_old
END
CLOSE MyTriggerCursor
DEALLOCATE MyTriggerCursor
Why is the code exiting in the middle of the loop?
Your problem is that you should NOT be using a cursor for this at all! This is the code for the example given above.
INSERT INTO table2(col1)
SELECT Col1 FROM table
where col1>10
You also should never ever use a cursor in a trigger, that will kill performance. If someone added 100,000 rows in an insert this could take minutes (or even hours) instead of millseconds or seconds. We replaced one here (that predated my coming to this job) and reduced an import to that table from 40 minites to 45 seconds.
Any production code that uses a cursor should be examined to replace it with correct set-based code. in my experience 90+% of all cursors can be reqwritten in a set-based fashion.
Ryan, your problem is that ##FETCH_STATUS is global to all cursors in an connection.
So the cursor within the trigger ends with an ##FETCH_STATUS of -1. When control returns to the code above, the last ##FETCH_STATUS was -1 so the cursor ends.
That's explained in the documentation, which can be found on MSDN here.
What you can do is use an local variable to store the ##FETCH_STATUS, and put that local variable in the loop. So you get something like this:
DECLARE #v1 int
DECLARE #v2 int
DECLARE #FetchStatus int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO #v1, #v2
SET #FetchStatus = ##FETCH_STATUS
WHILE(#FetchStatus=0)
BEGIN
IF(#v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (#v2)
END
FETCH NEXT FROM MyCursor INTO #v1, #v2
SET #FetchStatus = ##FETCH_STATUS
END
CLOSE MyCursor
DEALLOCATE MyCursor
It's worth noting that this behaviour does not apply to nested cursors. I've made an quick example, which on SqlServer 2008 returns the expected result (50).
USE AdventureWorks
GO
DECLARE #LocationId smallint
DECLARE #ProductId smallint
DECLARE #Counter int
SET #Counter=0
DECLARE MyFirstCursor CURSOR FOR
SELECT TOP 10 LocationId
FROM Production.Location
OPEN MyFirstCursor
FETCH NEXT FROM MyFirstCursor INTO #LocationId
WHILE (##FETCH_STATUS=0)
BEGIN
DECLARE MySecondCursor CURSOR FOR
SELECT TOP 5 ProductID
FROM Production.Product
OPEN MySecondCursor
FETCH NEXT FROM MySecondCursor INTO #ProductId
WHILE(##FETCH_STATUS=0)
BEGIN
SET #Counter=#Counter+1
FETCH NEXT FROM MySecondCursor INTO #ProductId
END
CLOSE MySecondCursor
DEALLOCATE MySecondCursor
FETCH NEXT FROM MyFirstCursor INTO #LocationId
END
CLOSE MyFirstCursor
DEALLOCATE MyFirstCursor
--
--Against the initial version of AdventureWorks, counter should be 50.
--
IF(#Counter=50)
PRINT 'All is good with the world'
ELSE
PRINT 'Something''s wrong with the world today'
this is a simple misunderstanding of triggers... you don't need a cursor at all for this
if UPDATE(Col1)
begin
insert into mutaties
(
tablename,
columnname,
newvalue
)
select
'table2',
coalesce(d.Col1,''),
coalesce(i.Col1,''),
getdate()
from inserted i
join deleted d on i.ID=d.ID
and coalesce(d.Col1,-666)<>coalesce(i.Col1,-666)
end
basically what this code does is it checks to see if that column's data was updated. if it was, it compares the new and old data, and if it's different it inserts into your log table.
you're first code example could easily be replaced with something like this
insert into table2 (col1)
select Col2
from table
where Col1>10
This code does not fetch any further values from the cursor, nor does it increment any values. As it is, there is no reason to implement a cursor here.
Your entire code could be rewritten as:
DECLARE #v1 int
DECLARE #v2 int
SELECT #v1 = Col1, #v2 = Col2
FROM table
IF(#v1>10)
INSERT INTO table2(col1) VALUES (#v2)
Edit: Post has been edited to fix the problem I was referring to.
You do not have to use a cursor to insert each column as a separate row.
Here is an example:
INSERT LOG.DataChanges
SELECT
SchemaName = 'Schemaname',
TableName = 'TableName',
ColumnName = CASE ColumnID WHEN 1 THEN 'Column1' WHEN 2 THEN 'Column2' WHEN 3 THEN 'Column3' WHEN 4 THEN 'Column4' END
ID = Key1,
ID2 = Key2,
ID3 = Key3,
DataBefore = CASE ColumnID WHEN 1 THEN I.Column1 WHEN 2 THEN I.Column2 WHEN 3 THEN I.Column3 WHEN 4 THEN I.Column4 END,
DataAfter = CASE ColumnID WHEN 1 THEN D.Column1 WHEN 2 THEN D.Column2 WHEN 3 THEN D.Column3 WHEN 4 THEN D.Column4 END,
DateChange = GETDATE(),
USER = WhateverFunctionYouAreUsingForThis
FROM
Inserted I
FULL JOIN Deleted D ON I.Key1 = D.Key1 AND I.Key2 = D.Key2
CROSS JOIN (
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
) X (ColumnID)
In the X table, you could code additional behavior with a second column that specially describes how to handle just that column (let's say you wanted some to post all the time, but others only when the value changes). What's important is that this is an example of the cross join technique of splitting rows into each column, but there is a lot more that can be done. Note that the full join allows this to work on inserts and deletes as well as updates.
I also fully agree that storing each row is FAR superior. See this forum for more about this.
As ck mentioned, you are not fetching any further values. The ##FETCH_STATUS thus get's its value from your cursor contained in your AFTER INSERT trigger.
You should change your code to
DECLARE #v1 int
DECLARE #v2 int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO #v1, #v2
WHILE(##FETCH_STATUS=0)
BEGIN
IF(#v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (#v2)
END
FETCH NEXT FROM MyCursor INTO #v1, #v2
END

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