While Loop with Table Update - sql-server

I have a problem with a While Loop in SQL Server.
What I want to do is to split something like:
'#Name=John;#Surname=Kowalsky;#DATA=data;'
Into table with 2 columns where one will be '#Name' and second one 'John'
After every split I wrote an update to delete the split string and start from new one but that doesn't work.
CREATE TABLE test2 (x varchar(max), y varchar(max))
INSERT INTO test2 VALUES (1, '#Name=John;#Surname=Kowalsky;#DATA=data;')
CREATE TABLE test (x varchar(max), y varchar(max))
DECLARE #y varchar(max) = (SELECT y FROM test2)
WHILE (SELECT LEN(y) FROM test2) > 0
BEGIN
INSERT INTO test (x,y)
VALUES ((SUBSTRING(#y, CHARINDEX('#', #y)+1, CHARINDEX('=',#y) -CHARINDEX('#',#y)-1)),
(SUBSTRING(#y, CHARINDEX('=', #y)+1, CHARINDEX(';',#y) -CHARINDEX('=',#y)-1)))
UPDATE test2
SET y = REPLACE(y, SUBSTRING(y, 1, CHARINDEX(';', y)), '')
FROM test2
END
In return I'm getting table test with 3 same rows: NAME / JOHN
So this loop does this job ALMOST correctly... What am I doing wrong?

The only mistake you made was not updating the variable #y for each iteration of the loop. The following solves that, and for clarity tidies up the variable usage, i.e. uses a variable instead of a temp table, and only uses a single variable rather than two for the data being processed:
-- Input data
DECLARE #Test2 VARCHAR(MAX) = '#Name=John;#Surname=Kowalsky;#DATA=data;';
-- Output table
DECLARE #Test1 TABLE (x VARCHAR(MAX), y VARCHAR(MAX))
-- While we still have data to process
WHILE LEN(#Test2) > 0 BEGIN
-- Extract one pair from the input string and put into the output table
INSERT INTO #Test1 (x,y)
VALUES ((SUBSTRING(#Test2, CHARINDEX('#', #Test2)+1, CHARINDEX('=',#Test2) -CHARINDEX('#',#Test2)-1)),
(SUBSTRING(#Test2, CHARINDEX('=', #Test2)+1, CHARINDEX(';',#Test2) -CHARINDEX('=',#Test2)-1)))
-- Update the variable #Test2 each iteration, removing the processed data
set #Test2 = REPLACE(#Test2, SUBSTRING(#Test2, 1, CHARINDEX(';', #Test2)), '');
END
SELECT * FROM #Test1
Returns:
x | y
--------| --------
Name | John
Surname | Kowalsky
DATA | data

Related

increment a column in the table based on user input values

I am trying to write a stored procedure that would increment one of the column in the table by 1, everytime either of the country code (AXX,XPP,CGG) is given as user input, depending on the already existing country code value
ID CountryCode
1 AXX
2 AXX
1 XPP
3 AXX
4 AXX
2 XPP
My code below fetches a value that reads the ID like
ID
1
2
3
4
...
...
Create procedure [dbo].[sp_Incremental]
#code varchar(50)
as
begin
declare #IDbigint
set #ID= 1 ;
begin try
Select #ID=count(ID)+1 from [Incremental_tbl]
Begin
Insert into [Incremental_tbl] values (#ID,#code)
End
end
go
You can use this SP :
CREATE PROCEDURE p_Incremental #code VARCHAR(50)
AS
SET NOCOUNT ON;
INSERT INTO Incremental_tbl (ID, code)
SELECT COALESCE(MAX(ID), 0) + 1, #code
FROM Incremental_tbl
GROUP BY #code;
GO
But, do not forget that, in concurrency acesses, same values can occur for the ID !

How to find how many times repeat number in comma separated value in particular column in SQL Server

I have a SQL Server table as shown below, in that one column contains comma-separated integer values. What I want is to get particular number with count as below expected result
Edited:
I know how to split the comma separated value. but my problem is output that I want. see the expected output in that count column shows in all table howmnay times particular number repeat.
For example :
Id values
---------------
1 2,3
2 1,2,3
3 1,3
4 2,3
Expected result :
number repeat count
--------------
1 2
2 3
3 4
Can anyone help me with this? How to write the query to get this desired output?
Thanks in advance
It looks like the question is how to aggregate the results of a SPLIT function, not how to split the values.
SQL Server 2016 provides the built-in STRING_SPLIT function to split a delimited string and return the values as a table. Individual values are returned in the value field. The following query groups the value field and returns the count:
declare #table table (id int, somevalues nvarchar(200))
insert into #table
values
(1,N'2,3'),
(2,N'1,2,3'),
(3,N'1,3'),
(4,N'2,3')
select value,count(* )
from #table
cross apply string_split(somevalues,',')
group by value
The same query can be used in previous versions as long as a split function is available. Almost all of the available techniques are described in Aaron Bertrand's articles like this one and this follow up. The fastest methods use CLR and XML.
The queries are the same, the only things that change are the names of the columns returned by the split function, eg:
select item,count(* )
from #table
cross apply dbo.SplitStrings_XML(somevalues,',')
group by item
In both cases the result is :
value (No column name)
1 2
2 3
3 4
First create split function like this
CREATE FUNCTION SplitString (
#Input NVARCHAR(MAX)
, #Character CHAR(1)
)
RETURNS #Output TABLE (Item NVARCHAR(1000))
AS
BEGIN
DECLARE #StartIndex INT
, #EndIndex INT
SET #StartIndex = 1
IF SUBSTRING(#Input, LEN(#Input) - 1, LEN(#Input)) <> #Character
BEGIN
SET #Input = #Input + #Character
END
WHILE CHARINDEX(#Character, #Input) > 0
BEGIN
SET #EndIndex = CHARINDEX(#Character, #Input)
INSERT INTO #Output (Item)
SELECT SUBSTRING(#Input, #StartIndex, #EndIndex - 1)
SET #Input = SUBSTRING(#Input, #EndIndex + 1, LEN(#Input))
END
RETURN
END
GO
then you can adapt the query as follows
create table #temp(
id int,
test varchar(20)
)
insert into #temp (id,test) values (1,'1,2,3')
SELECT t.id, count(sf.Item)
FROM #temp AS t
CROSS APPLY dbo.SplitString(t.test,',') AS sf
group by t.id;
drop table #temp

SQL Server compare charaters and update column

I have two columns in a table.
x | y
========|=========
AMDN | AMDNOXYZ
AMDNO | AMNOXYZ
I want to compare the two columns and update column y based on certain conditions on column x. If there are characters in column y which are not present in column x then just delete those characters not the whole string. The idea is to compare character by character in both columns. At any point in time there cannot be any character in column y which is not present in column x or in other words column y can only be a subset of column x.
So the result will be
x | y
========|=========
AMDN | AMDN
AMDNO | AMNO
Please help.
This is a function created in sql server which takes x and y columns as input and returns the desired output
create function remove_char(#x_string varchar(500),#y_string varchar(500))
returns varchar(500)
as
begin
declare #Y table(autoid int identity,c char(1))
declare #i int=1;declare #c char(1);
while(LEN(#y_string)>=#i)
begin
select #c=substring(#y_string,#i,1);
Insert into #Y values(#c)
select #i=#i+1
end
declare #X table(autoid int identity,c char(1))
set #i=1;
while(LEN(#x_string)>=#i)
begin
select #c=substring(#x_string,#i,1);
Insert into #X values(#c)
select #i=#i+1
end
return( select STUFF(( select ''+c from #Y
where c in (select c from #X) order by autoid
FOR XML PATH('')),1,0,'') )
end
example
select dbo.remove_char('AMDNO','AMNOXYZ')
you can use this function to update your table
update <table> set y=dbo.remove_char(x,y)

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

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