SQL Server, CONTEXT_INFO(), and varchar size - sql-server

In table users I have a column username of datatype varchar(50). The table has no records. I insert a new record with A for the username. The following returns what I would expect:
SELECT username, LEN(username)
FROM users
WHERE id = 1 -- returns: A, 1
So far so good.
Now I update table users from a trigger on another table, using the value from CONTEXT_INFO():
set #context = cast('B' as varbinary(128))
set CONTEXT_INFO #context
update some_other_table
set x = 'y'
where id = 97
In the trigger for some_other_table I do:
DECLARE #context VARCHAR(128)
SELECT
#context = CAST(CONTEXT_INFO() AS VARCHAR(128))
FROM
master.dbo.SYSPROCESSES
WHERE
SPID = ##SPID
DECLARE #user VARCHAR(50) = LEFT(#context, 50)
UPDATE users
SET username = LTRIM(RTRIM(#user))
WHERE id = 1
The username is correctly set to "B", but the following now returns 50:
SELECT
username, LEN(username)
FROM
users
WHERE
id = 1 -- returns: B, 50
The solution, when populating the context, is to do:
set #context = cast('B' + replicate(' ', 126) as varbinary(128))
But why do I need to do this?
When I don't pad the CONTEXT_INFO with spaces what is happening that updating using its value will cause the resulting length to be 50 (even if I ltrim and rtrim the single character value before updating)?
And why must I pad my CONTEXT_INFO to 127 bytes total, not 128? For every character over 127, 1 character is truncated from the value originally set on CONTEXT_INFO
Note: ANSI_PADDING is enabled

In your code:
declare #user varchar(50) = left(#context, 50)
UPDATE users set username = ltrim(rtrim(#user)) WHERE id = 1
you defined #user to be 50 characters. Since the CONTEXT_INFO itself is 128 bytes, the contents of #user will be the letter B padded by 49 null CHAR(0) characters. LTRIM() and RTRIM() will not remove null characters, which are not whitespace, so they have no effect on #user.
If you want to remove the NULL character you can try this (assuming you are using SQL Server 2005 or later):
UPDATE users SET username = REPLACE(#user, char(0), '')

I know this question is old, but there are two ways of retreiving CONTEXT_INFO: either directly with CONTEXT_INFO(), or from sys.dm_exec_sessions. The latter is not padded with '0' characters.
declare #c varbinary(128)
select #c = convert(varbinary(128), 'Test context')
set CONTEXT_INFO #c
go
-- option 1: padded with '0'
declare #value varchar(128)
select #value = convert(varchar(128), CONTEXT_INFO())
select #value context, len(#value) length
go
-- option 2: NOT padded with '0'
declare #value varchar(128)
select #value = convert(varchar(128), context_info)
from sys.dm_exec_sessions
where session_id = ##SPID
select #value context, len(#value) length
go
Query result.
context length
--------------- -----------
Test context 128
(1 row affected)
context length
--------------- -----------
Test context 12
(1 row affected)
Alternatively, as of SQL Server 2016 you can also use SESSION_CONTEXT(), which allows you to specify key-value pairs, instead of a single binary blob.

Related

Remove extra spaces in sp_send_dbmail

I have the following table:
CREATE TABLE [dbo].[MyTable](
[Day_ID] [nvarchar](50) NULL,
[SAS] [nvarchar](50) NULL,
[STAMP] [datetime] NULL DEFAULT (getdate())
)
which contains this data:
'2017_12_06_01','Red'
'2017_12_06_02','Yellow'
'2017_12_06_03','Green'
'2017_12_06_04','Blue'
I also have a SP which read all the data from MyTable and send it in the body of an e-mail message.
SET NOCOUNT ON;
EXEC msdb.dbo.sp_send_dbmail
#profile_name = 'MyMailProfile'
,#recipients = 'me#account.com'
,#subject = 'This is the object'
,#body = 'This is the body:'
,#body_format = 'TEXT'
,#query = 'SET NOCOUNT ON select [Day_ID],[SAS] from MyTable SET NOCOUNT OFF'
,#attach_query_result_as_file = 0
,#query_result_separator = '|'
,#exclude_query_output = 1
,#query_result_no_padding = 0
,#query_result_header = 0
,#append_query_error=1
Everything works but my problem is that in the body the results appear like these:
2017_12_06_01 |Red
2017_12_06_02 |Yellow
2017_12_06_03 |Green
2017_12_06_04 |Blue
In other words SQL Server know that the columns could be 50 characters long so they fill the space that doesn't contain characters with spaces. This is very boring if you consider that happen also if you write numeric values into a column, for example, NUMERIC.
I've tried with LTRIM and RTRIM but nothing changed.
I am using SQL Server 2005.
The parameter #query_result_no_padding should allow you to do this.
Change it from:
#query_result_no_padding = 0
to
#query_result_no_padding = 1
[ #query_result_no_padding ] #query_result_no_padding The type is bit.
The default is 0. When you set to 1, the query results are not padded,
possibly reducing the file size.If you set #query_result_no_padding to
1 and you set the #query_result_width parameter, the
#query_result_no_padding parameter overwrites the #query_result_width
parameter. + In this case no error occurs. If you set the
#query_result_no_padding to 1 and you set the #query_no_truncate
parameter, an error is raised.
Source: https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-send-dbmail-transact-sql
You need to run two queries - first to compute the actual lengths, the second to retrieve the data for the email. Obviously, this can have a high cost.
Something like:
declare #maxDay int
declare #maxSAS int
select #maxDay = MAX(LEN(RTRIM(Day_ID))),#maxSAS = MAX(LEN(RTRIM(SAS))) from MyTable
declare #sql varchar(max)
set #sql = 'SET NOCOUNT ON select CONVERT(nvarchar(' + CONVERT(varchar(10),#maxDay) +
'),[Day_ID]) as Day_ID,CONVERT(nvarchar(' + CONVERT(varchar(10),#maxSAS) +
'),[SAS]) as SAS from MyTable SET NOCOUNT OFF'
And then use #sql in you sp_send_dbmail call.
I've tried with LTRIM and RTRIM but nothing changed.
It wouldn't. Each column in a result set has a single fixed type. The input to your e.g. RTRIM() calls is an nvarchar(50). Without knowing the contents of all rows, what possible data type can SQL Server derive for the output from that RTRIM() using expression? It can only still be nvarchar(50) and so that's still the type of the column in the result set. For all the server nows, there could be a row containing 50 non-whitespace characters and the result still needs to show that.

I need to do a look up on a table based on each word in a string

I have a table of countries and their abbreviations with a column called code with the abbreviations and a column called name that contains the country names.
I need to iterate through a foreign address and look up each word until I find a match in the table (i.e. the country) and retrieve the abbreviation for a case statement.
This won't be done on all records, only certain ones that would be larger than a field of a 60 chars in a file I'm building.
So What I need to do is something to affect of:
SELECT
CASE WHEN address2 & foreign_address > 60
THEN split and iterate through '12345 MY SUPER LONG ADDRESS IN THE PHILIPPINES' and look up
each string until PHILIPPINES is matched in the country_codes table and 'PH' is returned
END
This is the best way I can think to handle this situation short of truncating the address which I don't want to do for obvious reasons. This also needs to be dynamic based on different addresses and countries.
My biggest challenge at this point is breaking up the string and doing a look up on each string fragment.
SELECT Addresses.foreign_address, Countries.Code
FROM Addresses, Countries
WHERE LEN(foreign_address) > 60
AND foreign_address LIKE '%' Countries.Name '%'
[Code] will contain the abbreviated code of the country that was matched.
Here's the relevant SQLFiddle (and code below in full):
CREATE TABLE Countries (Name varchar(128), Code varchar(2));
CREATE TABLE Addresses (foreign_address varchar(512));
INSERT INTO Countries(Name,Code) VALUES('PHILIPPINES', 'PH');
INSERT INTO Addresses(foreign_address)
VALUES('12345 MY SUPER LONG ADDRESS IN THE PHILIPPINES UNTIL PHILIPPINES IS MATCHED AND PH IS RETURNED');
SELECT Addresses.foreign_address, Countries.Code
FROM Addresses, Countries
WHERE LEN(foreign_address) > 60
AND foreign_address LIKE '%' + Countries.Name + '%'
Something like this, may be?
select l.abbr, a.id
from
lookup_table l, address_table a
where
charindex(l.country_name, a.address2 + a.foreign_address) > 0
and len( a.address2 + a.foreign_address) > 60
Not tested.
Edited for the SQL Server 2005 string concatenation operator.
You can use LIKE instead of breaking up by word:
declare #address2 varchar(128)
declare #foreign_address varchar(128)
set #address2 = '12345 MY SUPER LONG ADDRESS IN THE PHILIPPINES'
set #foreign_address = '123456789012345678901234567890123456789012345678901234567890'
SELECT
CASE
WHEN len(#address2 + #foreign_address) > 60
AND (' ' + #address2 + ' ' + #foreign_address + ' ') like '% PHILIPPINES %'
THEN 'PH'
ELSE NULL
END
I am not advocating this style of programming in SQL Server, but this is how I would do what you asked to do. First, tokenize the string into single words in a table variable. Then opena a cursor on the table variable and loop through the words, calling break if we find a result from dbo.countries. Please note, loops are very inefficient in SQL Server. The UDF table function came from here: How to split a string in T-SQL?
-- Create the UDF
CREATE FUNCTION Splitfn(#String varchar(8000), #Delimiter char(1))
returns #temptable TABLE (items varchar(8000))
as
begin
declare #idx int
declare #slice varchar(8000)
select #idx = 1
if len(#String)<1 or #String is null return
while #idx!= 0
begin
set #idx = charindex(#Delimiter,#String)
if #idx!=0
set #slice = left(#String,#idx - 1)
else
set #slice = #String
if(len(#slice)>0)
insert into #temptable(Items) values(#slice)
set #String = right(#String,len(#String) - #idx)
if len(#String) = 0 break
end
return
end
go
-- Create the dbo.countries table so we can test our code later
create table dbo.countries (code char(2), name varchar(100))
go
-- Insert one record in dbo.countries so we can test our code later
insert into dbo.countries (code, name)
select 'PH', 'PHILIPPINES'
go
-- for one #String input, this is what I would do
declare #String varchar(1000) = '12345 MY SUPER LONG ADDRESS IN THE PHILIPPINES'
declare #CountryCode char(2) = ''
declare #done bit = 0x0
declare #word varchar(1000)
declare #words table (word varchar(250) primary key)
-- Break apart your #String into a table of records, only returning the DISTINCT values.
-- Join on the domain list so we can only process the ones that will return data in the CURSOR (eliminating excess looping)
insert into #words (word)
--
select distinct items as word
from dbo.Splitfn(#String, ' ') s
join dbo.countries c
on lower(c.name) = lower(s.items)
declare word_cursor CURSOR for
select word
from #words w
open word_cursor
fetch next from word_cursor into #word
while ##FETCH_STATUS = 0
begin
select #CountryCode = code
from dbo.countries
where name = #word
if ##trancount > 0
begin
break
end
fetch next from word_cursor into #word
end
-- clean up the cursor
close word_cursor
deallocate word_cursor
-- return the found CountryCode
select #CountryCode

Can I send array of parameter to store procedure?

I have User table, it has UserId uniqueidentifier, Name varchar and IsActive bit.
I want to create store procedure to set IsActive to false for many user, for example, if I want to deactive 2 users, I want to send Guid of those users to store procedure (prefer as array). I want to know how can I do it?
P.S. I'm working on Microsoft SQL Azure
Along the same lines than Elian, take a look at XML parameters. Generally speaking you should have a cleaner/safer implementation using xml than parsing a list of strings. Click here for a code example
Here is a solution I used a while ago and that was working fine.
Send the list of guid you want to deactive merged into a comma separated string to the sp.
Then in the sp, you first convert this string into a table thanks to a table-valued function.
Here is a sample with bigint, but you can easily modify it so that it works with guid
Step 1 : the table-valued function
CREATE FUNCTION [dbo].[BigIntListToTable] (
#list VARCHAR(max)
)
RETURNS
#tbl TABLE
(
nval BIGINT NOT NULL
) AS
BEGIN
DECLARE #nPos INT
DECLARE #nNextPos INT
DECLARE #nLen INT
SELECT #nPos = 0, #nNextPos = 1
WHILE #nNextPos > 0
BEGIN
SELECT #nNextPos = CHARINDEX(',', #list, #nPos + 1)
SELECT #nLen = CASE WHEN #nNextPos > 0
THEN #nNextPos
ELSE LEN(#list) + 1
END - #nPos - 1
INSERT #tbl (nval)
VALUES (CONVERT(BIGINT, SUBSTRING(#list, #nPos + 1, #nLen)))
SELECT #nPos = #nNextPos
END
RETURN
END
Step 2 : the stored proc
CREATE PROCEDURE [dbo].[spMySP]
#IdList VARCHAR(max)
AS
BEGIN
SET NOCOUNT ON;
SET ROWCOUNT 0
UPDATE dbo.YourTable
SET isActive = 0
FROM dbo.YourTable
INNER JOIN dbo.BigIntListToTable(#IdList) l
ON dbo.YourTable.id = l.nval
END

Search an replace text in column for Column Type Text SQL Server

What I need is to search for a string in a specific column (datatype: text) of a table and replace it with another text.
For example
Id | Text
-----------------------------
1 this is test
2 that is testosterone
If I chose to replace test with quiz, results should be
this is quiz
that is quizosterone
What I've tried so far?
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROC [dbo].[SearchAndReplace]
(
#FindString NVARCHAR(100)
,#ReplaceString NVARCHAR(100)
)
AS
BEGIN
SET NOCOUNT ON
SELECT CONTENT_ID as id, CONTENT_TEXT, textptr(CONTENT_TEXT) as ptr, datalength(CONTENT_TEXT) as lng
INTO #newtable6 FROM HTML_CONTENTS
DECLARE #COUNTER INT = 0
DECLARE #TextPointer VARBINARY(16)
DECLARE #DeleteLength INT
DECLARE #OffSet INT
SELECT #TextPointer = TEXTPTR(CONTENT_TEXT)
FROM #newtable6
SET #DeleteLength = LEN(#FindString)
SET #OffSet = 0
SET #FindString = '%' + #FindString + '%'
WHILE (SELECT COUNT(*)
FROM #newtable6
WHERE PATINDEX(#FindString, CONTENT_TEXT) <> 0) > 0
BEGIN
SELECT #OffSet = PATINDEX(#FindString, CONTENT_TEXT) - 1
FROM #newtable6
WHERE PATINDEX(#FindString, CONTENT_TEXT) <> 0
UPDATETEXT #newtable6.CONTENT_TEXT
#TextPointer
#OffSet
#DeleteLength
#ReplaceString
SET #COUNTER = #COUNTER + 1
END
select #COUNTER,* from #newtable6
drop table #newtable6
SET NOCOUNT OFF
I get the error:
Msg 7116, Level 16, State 4, Procedure SearchAndReplace, Line 31
Offset 1900 is not in the range of available LOB data.
The statement has been terminated.
Thank you
If you can't change your column types permanently, you can cast them on the fly:
ALTER PROC [dbo].[SearchAndReplace]
(#FindString VARCHAR(100),
#ReplaceString VARCHAR(100) )
AS
BEGIN
UPDATE dbo.HTML_CONTENTS
SET CONTENT_TEXT = cast (REPLACE(cast (CONTEXT_TEXT as varchar(max)), #FindString, #ReplaceString) as TEXT)
END
The datatype TEXT is deprecated and should not be used anymore - exactly because it's clunky and doesn't support all the usual string manipulation methods.
From the MSDN docs on text, ntext, image:
ntext, text, and image data types will
be removed in a future version of
MicrosoftSQL Server. Avoid using these
data types in new development work,
and plan to modify applications that
currently use them. Use nvarchar(max),
varchar(max), and varbinary(max)
instead.
My recommendation: convert that column to VARCHAR(MAX) and you should be fine after that!
ALTER TABLE dbo.HTML_CONTENTS
ALTER COLUMN CONTEXT_TEXT VARCHAR(MAX)
That should do it.
When your column is VARCHAR(MAX), then your stored procedures becomes totally simple:
ALTER PROC [dbo].[SearchAndReplace]
(#FindString VARCHAR(100),
#ReplaceString VARCHAR(100) )
AS
BEGIN
UPDATE dbo.HTML_CONTENTS
SET CONTENT_TEXT = REPLACE(CONTEXT_TEXT, #FindString, #ReplaceString)
END
Two observations on the side:
it would be helpful to have a WHERE clause in your stored proc, in order not to update the whole table (unless that's what you really need to do)
you're using TEXT in your table, yet your stored procedure parameters are of type NVARCHAR - try to stick to one set - either TEXT/VARCHAR(MAX) and regular VARCHAR(100) parameters, or then use all Unicode strings: NTEXT/NVARCHAR(MAX) and NVARCHAR(100). Constantly mixing those non-Unicode and Unicode strings is a mess and causes lots of conversions and unnecessary overhead

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