First post - I am trying to pull ten different pieces of information from a single field. Let me start with this is not my table, just what I was given to work with. This is a varchar max field.
'3350|#|1234567|~|3351|#|8/1/2017|~|3352|#|Acme|~|3353|~|10000.00|~|3354|#||~|3355|#||~3356|#|Yes|~|3357|#|Doe,John|~|3358|#|CA|~|3359|#|5551212'
I know that the numbers that start with 33 are keys telling me what information is in that section. 3350 has the invoice #1234567. 3351 has the invoice date of 8/1/17. etc. 3354 and 3355 were left null. The keys are unchanging and will be the same for every record in the table.
I need to pull the data from between 3350|#| and |~|3351 to get my invoice# and between 3351|#| and |~|3352 to get my date, etc, but I am struggling with how to word this. Any help would be appreciated and any critiques on my first post will be taken constructively.
The #YourTable is just a table variable used for demonstration / illustration
For Rows - Example
Declare #YourTable table (ID int,SomeCol varchar(max))
Insert Into #YourTable values
(1,'3350|#|1234567|~|3351|#|8/1/2017|~|3352|#|Acme|~|3353|#|10000.00|~|3354|#||~|3355|#||~|3356|#|Yes|~|3357|#|Doe,John|~|3358|#|CA|~|3359|#|5551212')
Select A.ID
,Item = left(RetVal,charindex('|#|',RetVal+'|#|')-1)
,Value = right(RetVal,len(RetVal)-charindex('|#|',RetVal+'|#|')-2)
From #YourTable A
Cross Apply [dbo].[udf-Str-Parse](A.SomeCol,'|~|') B
Returns
For Columns - Example
Declare #YourTable table (ID int,SomeCol varchar(max))
Insert Into #YourTable values
(1,'3350|#|1234567|~|3351|#|8/1/2017|~|3352|#|Acme|~|3353|#|10000.00|~|3354|#||~|3355|#||~|3356|#|Yes|~|3357|#|Doe,John|~|3358|#|CA|~|3359|#|5551212')
Select *
From (
Select A.ID
,Item = left(RetVal,charindex('|#|',RetVal+'|#|')-1)
,Value = right(RetVal,len(RetVal)-charindex('|#|',RetVal+'|#|')-2)
From #YourTable A
Cross Apply [dbo].[udf-Str-Parse](A.SomeCol,'|~|') B
) A
Pivot (max([Value]) For [Item] in ([3350],[3351],[3352],[3353],[3354],[3355],[3356],[3357],[3358],[3359]) ) p
Returns
The UDF if Interested
CREATE FUNCTION [dbo].[udf-Str-Parse] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(#String,#Delimiter,'§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);
--Thanks Shnugo for making this XML safe
--Select * from [dbo].[udf-Str-Parse]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse]('John Cappelletti was here',' ')
You can try a tally based splitter like below
declare #t table ( id int, col nvarchar(max));
insert into #t values
(1, '3350|#|1234567|~|3351|#|8/1/2017|~|3352|#|Acme|~|3353|~|10000.00|~|3354|#||~|3355|#||~3356|#|Yes|~|3357|#|Doe,John|~|3358|#|CA|~|3359|#|5551212')
,(2, '3350|#|123334567|~|3351|#|8/2/2017|~|3352|#|Acme|~|3353|~|10000.00|~|3354|#||~|3355|#||~3356|#|Yes|~|3357|#|Doe,John|~|3358|#|CA|~|3359|#|5551212');
select
id,
case
when split_values like '3350|#|%' then 'id'
when split_values like '3351|#|%' then 'date'
end as fieldname,
SUBSTRING(split_values,8,LEN(split_values)-7) as value
from
(
select
--t.col as col,
row_number() over (partition by t.col order by t1.N asc) as row_num,
t.id,
SUBSTRING( t.col, t1.N, ISNULL(NULLIF(CHARINDEX('|~|',t.col,t1.N),0)-t1.N,8000)) as split_values
from #t t
join
(
select
t.col,
1 as N
from #t t
UNION ALL
select
t.col,
t1.N + 3 as N
from #t t
join
(
select
top 8000
row_number() over(order by (select NULL)) as N
from
sys.objects s1
cross join
sys.objects s2
) t1
on SUBSTRING(t.col,t1.N,3) = '|~|'
) t1
on t1.col=t.col
)a
where
split_values like '3350|#|%' or
split_values like '3351|#|%'
Live demo
I want to return integers from rather complex strings which combined unicode characters such as - and . with characters and integers.
I've come a long way in achieving this, but I still have troubles with some strings of a more complex structure. For instance:
DECLARE #Tabl as table
(
dats nvarchar(15)
)
INSERT INTO #Tabl VALUES
('103-P705hh'),
('115-xxx-44'),
('103-705.13'),
('525-hheef4')
select LEFT(SUBSTRING(REPLACE(REPLACE(dats, '.',''),'-',''), PATINDEX('%[0-9.-]%', REPLACE(REPLACE(dats, '.',''),'-','')), 8000),
PATINDEX('%[^0-9.-]%', SUBSTRING(REPLACE(REPLACE(dats, '.',''),'-',''), PATINDEX('%[0-9.-]%', REPLACE(REPLACE(dats, '.',''),'-','')), 8000) + 'X')-1)
from #tabl
Gives
Raw Input Actual return: Desired return:
103-P705hh 103 103705
115-xxx-44 115 11544
103-705.13 10370513 10370513
525-hheef4 525 5254
I had a topic regarding this yesterday to cover the case when multiple - or . are present, but as seen in the return this is actually taken care of now. However, expanding the databases I work with I encountered much more complex string such as those I presented here.
Does anyone have any idea what to do when characters and integers are "mixed up" in the string?
Regards,
Cenderze
I have seen loads of solutions that use a scalar udf with a loop, but I don't like either of these things, so throwing my hat into the ring with a different approach.
With the help of a numbers table you can deconstruct each value into its individual characters, remove non-numeric characters, then reconstruct it using FOR XML to concatenate rows, e.g.
WITH Numbers (Number) AS
( SELECT ROW_NUMBER() OVER(ORDER BY N1.N)
FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N1 (N) -- 100
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N2 (N) -- 100
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N3 (N) -- 1,000
--CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N4 (N) -- 10,000
--CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N5 (N) -- 100,000
--COMMENT OR UNCOMMENT ROWS AS NECESSARY DEPENDING ON YOU MAX STRING LENGTH
)
SELECT t.dats,
Stripped = x.data.value('.', 'INT')
FROM #tabl AS t
CROSS APPLY
( SELECT SUBSTRING(t.dats, n.Number, 1)
FROM Numbers n
WHERE n.Number <= LEN(t.dats)
AND SUBSTRING(t.dats, n.Number, 1) LIKE '[0-9]'
ORDER BY n.Number
FOR XML PATH(''), TYPE
) x (data);
Gives:
dats Stripped
----------------------
103-P705hh 103705
115-xxx-44 11544
103-705.13 10370513
525-hheef4 5254
I haven't done any testing so it could be that the added overhead of expanding each string into individual characters and reconstructing it is actually a lot more overhead than than a UDF with a loop.
I decided to bench mark this
1. Set up functions
CREATE FUNCTION dbo.ExtractNumeric_TVF (#Input VARCHAR(8000))
RETURNS TABLE
AS
RETURN
( WITH Numbers (Number) AS
( SELECT TOP (LEN(#Input)) ROW_NUMBER() OVER(ORDER BY N1.N)
FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N1 (N) -- 100
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N2 (N) -- 100
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N3 (N) -- 1,000
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N4 (N) -- 10,000
)
SELECT Stripped = x.data.value('.', 'VARCHAR(MAX)')
FROM ( SELECT SUBSTRING(#Input, n.Number, 1)
FROM Numbers n
WHERE n.Number <= LEN(#Input)
AND SUBSTRING(#Input, n.Number, 1) LIKE '[0-9]'
ORDER BY n.Number
FOR XML PATH(''), TYPE
) x (data)
);
GO
create function dbo.ExtractNumeric_UDF(#s varchar(8000))
returns varchar(8000)
as
begin
declare #out varchar(max) = ''
declare #c char(1)
while len(#s) > 0 begin
set #c = left(#s,1)
if #c like '[0123456789]' set #out += #c
set #s = substring(#s, 2, len(#s) -1)
end
return #out
end
GO
2. Create first set of sample data and log table
CREATE TABLE dbo.T (Value VARCHAR(8000) NOT NULL);
INSERT dbo.T (Value)
SELECT TOP 1000 LEFT(NEWID(), CEILING(RAND(CHECKSUM(NEWID())) * 36))
FROM sys.all_objects a
CROSS JOIN sys.all_objects b;
CREATE TABLE dbo.TestLog (Fx VARCHAR(255), NumberOfRows INT, TimeStart DATETIME2(7), TimeEnd DATETIME2(7))
3. Run Tests
GO
DECLARE #T TABLE (Val VARCHAR(8000));
INSERT dbo.TestLog (fx, NumberOfRows, TimeStart)
VALUES ('dbo.ExtractNumeric_UDF', 1000, SYSDATETIME());
INSERT #T (Val)
SELECT dbo.ExtractNumeric_UDF(Value)
FROM dbo.T;
UPDATE dbo.TestLog
SET TimeEnd = SYSDATETIME()
WHERE TimeEnd IS NULL;
GO 100
DECLARE #T TABLE (Val VARCHAR(8000));
INSERT dbo.TestLog (fx, NumberOfRows, TimeStart)
VALUES ('dbo.ExtractNumeric_TVF', 1000, SYSDATETIME());
INSERT #T (Val)
SELECT f.Stripped
FROM dbo.T
CROSS APPLY dbo.ExtractNumeric_TVF(Value) f;
UPDATE dbo.TestLog
SET TimeEnd = SYSDATETIME()
WHERE TimeEnd IS NULL;
GO 100
4. Get Results
SELECT Fx,
NumberOfRows,
RunTime = AVG(DATEDIFF(MILLISECOND, TimeStart, TimeEnd))
FROM dbo.TestLog
GROUP BY fx, NumberOfRows;
I did the following (using just NEWID() so only a maximum of 36 characters) over 1,000 and 10,000 rows, the results were:
Fx NumberOfRows RunTime
--------------------------------------------------------
dbo.ExtractNumeric_TVF 1000 31
dbo.ExtractNumeric_UDF 1000 56
dbo.ExtractNumeric_TVF 10000 280
dbo.ExtractNumeric_UDF 10000 510
So the TVF coming in at just under half the time of the UDF.
I wanted to test edge cases so put 1,000 rows of longer strings (5,400 characters)
TRUNCATE TABLE dbo.T;
INSERT dbo.T (Value)
SELECT TOP 1000
REPLICATE(CONCAT(NEWID(), NEWID(), NEWID(), NEWID(), NEWID()), 30)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b;
And this is where the TVF came into its own, running over 5x faster:
Fx NumberOfRows RunTime
------------------------------------------------
dbo.ExtractNumeric_TVF 1000 2485
dbo.ExtractNumeric_UDF 1000 12955
I also really don't like the looping solutions so I decided to try my hand at one. This is using a predefined tally table but is quite similar to others posted here already.
This is my tally table. I keep this as a view on my system.
create View [dbo].[cteTally] as
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select N from cteTally
GO
Because I don't like looping I decided to use the table valued function approach which let me reuse this functionality in other queries with little to no effort. Here is one way to write such a function.
create function GetOnlyNumbers
(
#SearchVal varchar(8000)
) returns table as return
with MyValues as
(
select substring(#SearchVal, N, 1) as number
, t.N
from cteTally t
where N <= len(#SearchVal)
and substring(#SearchVal, N, 1) like '[0-9]'
)
select distinct NumValue = STUFF((select number + ''
from MyValues mv2
order by mv2.N
for xml path('')), 1, 0, '')
from MyValues mv
That looks good but the proof is in the pudding. Let's take this out with our sample data and kick the tires a few times.
DECLARE #Tabl as table
(
dats nvarchar(15)
)
INSERT INTO #Tabl VALUES
('103-P705hh'),
('115-xxx-44'),
('103-705.13'),
('525-hheef4')
select *
from #Tabl t
cross apply dbo.GetOnlyNumbers(t.dats) x
Sure looks nice and tidy. I tested against several of the other solutions posted here and without going into deep testing this appears to be significantly faster than the other approaches posted at this time.
DECLARE #Tabl as table
(
ID INT,
dats nvarchar(15)
)
INSERT INTO #Tabl VALUES
(1, '103-P705hh'),
(2, '115-xxx-44'),
(3, '103-705.13'),
(4, '525-hheef4')
SELECT T.ID, t.dats
,(
SELECT SUBSTRING(tt.dats,V.number,1)
FROM #Tabl tt
JOIN master.dbo.spt_values V ON V.type='P' AND V.number BETWEEN 1 AND LEN(tt.dats)
WHERE tt.ID=T.ID AND SUBSTRING(TT.dats,V.number,1) LIKE '[0-9]'
ORDER BY V.number
FOR XML PATH('')
) S
FROM #Tabl t
ORDER BY T.ID;
Can you use a udf ? If so, try this
create alter function numerals(#s varchar(max))
returns varchar(max)
as
begin
declare #out varchar(max) = ''
declare #c char(1)
while len(#s) > 0 begin
set #c = left(#s,1)
if #c like '[0123456789]' set #out += #c
set #s = substring(#s, 2, len(#s) -1)
end
return #out
end
to use it on your temp table...
select dbo.numerals(dats) from #Tabl
another solution, that does not use a UDF, but will work only if your table has a primary key, uses a recursive CTE. It is:
DECLARE #Tabl as table
(pk int identity not null, -- <=== added a primary key
dats nvarchar(max) )
INSERT INTO #Tabl VALUES
('103-P705hh'),
('115-xxx-44'),
('103-705.13'),
('525-hheef4');
with newVals(pk, pos, newD) as
(select pk, 1,
case when left(Dats,1) like '[0123456789]'
then left(Dats,1) else '' end
from #tabl
Union All
Select t.pk, pos + 1, n.newD +
case when substring(dats, pos+1, 1) like '[0123456789]'
then substring(dats, pos+1, 1) else '' end
from #tabl t join newVals n on n.pk = t.pk
where pos+1 <= len(dats) )
Select newD from newVals x
where pos = (Select Max(pos)
from newVals
where pk = x.pk)
I'd like to insert 300 rows where the Username consists of a prefix followed by an incrementing number from 001-300 (Or just 1-300).
For example: PRC001, PRC002, PRC003, PRC004
How would I do this in a single statement?
EDIT: I'm using SSMS 2016 and Microsoft Azure Database
Insert Into YourTable (SomeID)
Select Top 300 Format(Row_Number() Over (Order By Number),'PRC000') From master..spt_values
Another Option would be to create an ad-hoc tally table
;with cte0(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cteN(N) As (Select Row_Number() over (Order By (Select NULL)) From cte0 N1, cte0 N2, cte0 N3)
Select Top 300 Format(N,'PRC000') From cteN
You can use any table (which has more than 300 records) as well
Select Top 300 Format(Row_Number() Over (Order By (Select null)),'PRC000') From AnyLargerTable
The Insert would be
You want to generate those rows and insert them into a table?
Then you can try something like this:
DECLARE #i INTEGER
SET #i = 1
WHILE #i <= 300
BEGIN
PRINT 'PRC' + right('00' + cast(#i AS VARCHAR), 3)
/* add your insert here... */
SET #i = #i + 1
END
Output:
PRC001
PRC002
PRC003
PRC004
PRC005
PRC006
...
PRC298
PRC299
PRC300
I would like to Improve running time in T-sql code.
The Insert query is taking a long time. Is there any chance to use a different method ?
Here is my Code
Create PROCEDURE [dbo].[CreateTableDemog]
#Age int,
#RetiredAge int
as
begin
declare #t as int=1;
declare #tlast as int=#RetiredAge -#Age
declare #Period as int=0;
BEGIN
while #t<=#tlast
begin
while #Period <=#t-1
begin
INSERT INTO dbo.Table (t, Age,Period,Prob)
VALUES (#t,#Age+#t,#Period,1);
set #Period=#Period+1
end
set #Period=0
set #t=#t+1
end
end
Here is how you could do this using a tally table instead of the nested while loops. This is a strange requirement but easy enough to follow.
This article by Jeff Moden does a great job of explaining what a tally is and how you can use it. http://www.sqlservercentral.com/articles/T-SQL/62867/
Create PROCEDURE [dbo].[CreateTableDemog]
(
#Age int
, #RetiredAge int
) as
set nocount on;
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
INSERT INTO dbo.Table
(
t
, Age
, Period
, Prob
)
select t1.N as T
, #Age + t1.N as AgePlusT
, abs(t2.N - t1.N) as Period
, 1
from cteTally t1
join cteTally t2 on t2.N <= t1.N
where t1.N <= #RetiredAge - #Age
order by t1.N
, #Age + t1.N
, abs(t2.N - t1.N);
GO