First unused number in a varchar(5) field padded with zeros - sql

I have a varchar(5) field. I need find the first unused number across all records such that they begin with leading zeros. like if there is a 00001 and a 00003 I'd like the query to return 00002. Also, many of the records contain letters and look like 'G0542'. These can be ignored.
I know I'm close. This seems to work in SQL Server 2005 but not in 2008 or 2012
http://sqlfiddle.com/#!3/4016a0/1
create table b_addr ( inst_no varchar(5) Unique );
insert into b_addr (inst_no) values ('00001');
insert into b_addr (inst_no) values ('00002');
insert into b_addr (inst_no) values ('00004');
--this is the problem line
insert into b_addr (inst_no) values ('A0045');
With usedNos as(
select CAST(b_addr.inst_no AS INT) as inst
from b_addr
where b_addr.inst_no LIKE '[0-9][0-9][0-9][0-9][0-9]')
SELECT
RIGHT('00000' + CONVERT(VARCHAR(5), COALESCE(min(inst)+1, 0)),5) AS next_inst_no
from usedNos where not exists (select null from usedNos usn where usn.inst = usedNos.inst +1)
How can I structure this so it will work in sql server 2008+ as well?

Here is a version of your query that works:
With usedNos as (
select CAST(case when isnumeric(b_addr.inst_no) = 1
then b_addr.inst_no
end AS INT) as inst
from b_addr
)
SELECT RIGHT('00000' + CONVERT(VARCHAR(5), COALESCE(min(inst)+1, 0)),5) AS next_inst_no
from usedNos
where inst is not null and
not exists (select 1
from usedNos usn
where usn.inst = usedNos.inst +1
);
The key is the use of isnumeric() inside of case. This guarantees that the cast() is not attempted unless the value looks like a number. If it doesn't look like a number, then the result is NULL, which is filtered out in the outer where clause.
Your where clause:
where b_addr.inst_no LIKE '[0-9][0-9][0-9][0-9][0-9]'
attempts to do the same thing. However, SQL Server doesn't guarantee that the where clause is processed before the select -- which is why you are getting an unexpected error.

You can check whether the number you're trying to cast is a real number:
Instead of
CAST(b_addr.inst_no AS INT) as inst
Do something like
CAST(CASE WHEN ISNUMERIC(b_addr.inst_no) = 1 THEN b_addr.inst_no ELSE 0 END AS INT) as inst
It's a bit convoluted, but it works.
Demo: http://sqlfiddle.com/#!3/1aee5/6

here's another one using patindex instead of like.
http://sqlfiddle.com/#!6/1aee5/12
With usedNos as(
select b_addr.inst_no as inst
from b_addr
where patindex('[0-9][0-9][0-9][0-9][0-9]',b_addr.inst_no) > 0
)
SELECT
RIGHT('00000' + COALESCE(cast((min(cast(inst as int)) + 1) as varchar), '00000'),5) AS next_inst_no
from usedNos
where not exists (select null from usedNos usn where cast(usn.inst as int) = cast(usedNos.inst as int) + 1 )
--should return 00003, trying to find the first unused inst_no and padd it with zeros to make it 5 chars.

create table b_addr ( inst_no varchar(5) Unique );
insert into b_addr (inst_no) values ('00001');
insert into b_addr (inst_no) values ('00002');
insert into b_addr (inst_no) values ('00004');
--this is the problem line
insert into b_addr (inst_no) values ('A0045');
--Specify how many of the "next" unused numbers you want.
DECLARE #HowMany INT = 10
--Common Table Expression construct to generate sequential numbers.
;WITH
L0 AS(SELECT 1 AS c UNION ALL SELECT 1),
L1 AS(SELECT 1 AS c FROM L0 AS A, L0 AS B),
L2 AS(SELECT 1 AS c FROM L1 AS A, L1 AS B),
L3 AS(SELECT 1 AS c FROM L2 AS A, L2 AS B),
L4 AS(SELECT 1 AS c FROM L3 AS A, L3 AS B),
L5 AS(SELECT 1 AS c FROM L4 AS A, L4 AS B),
L6 AS(SELECT 1 AS c FROM L5 AS A, L5 AS B),
SequentialNumbers AS(SELECT ROW_NUMBER() OVER(ORDER BY c) AS Num FROM L6)
SELECT TOP(#HowMany) RIGHT('00000' + CAST(sn.Num AS VARCHAR), 5) AS NextNumbers
FROM SequentialNumbers sn
WHERE sn.Num NOT IN (
SELECT CAST(b_addr.inst_no AS INT) AS inst
FROM b_addr
WHERE b_addr.inst_no LIKE '[0-9][0-9][0-9][0-9][0-9]'
)

Related

Sorting VARCHAR column which contains integers

I have this table:
IF OBJECT_ID('tempdb..#Test') IS NOT NULL
DROP TABLE #Test;
CREATE TABLE #Test (Col VARCHAR(100));
INSERT INTO #Test
VALUES ('1'), ('2'), ('10'), ('A'), ('B'), ('C1'), ('1D'), ('10HH')
SELECT * FROM #Test
I want to sort by numeric value first and then alphabetically.
Outcome of sort I want to is:
1
1D
2
10
10HH
A
B
C1
Assume structure of entries is one of those (with no dash of course)
number
number-string
string-number
string
if there is an entry like string-number-string, assume it is string-number
It's not pretty, but it works.
SELECT T.Col
FROM #Test T
CROSS APPLY (VALUES(PATINDEX('%[^0-9]%',T.Col)))PI(I)
CROSS APPLY (VALUES(TRY_CONVERT(int,NULLIF(ISNULL(LEFT(T.Col,NULLIF(PI.I,0)-1),LEN(T.Col)),''))))TC(L)
ORDER BY CASE WHEN TC.L IS NULL THEN 1 ELSE 0 END,
TC.L,
T.Col;
Honestly, I would suggest that if you want to order your data like a numerical value you actually store the numerical value in a numerical column; clearly the above should be a numerical prefix value, and then the string suffix. If you then want to then have the values you have, the use a (PERSISTED) computed column. Like this:
CREATE TABLE #Test (Prefix int NULL,
Suffix varchar(100) NULL,
Col AS CONCAT(Prefix, Suffix) PERSISTED);
INSERT INTO #Test (Prefix, Suffix)
VALUES (1,NULL), (2,NULL), (10,NULL), (NULL,'A'), (NULL,'B'), (NULL,'C1'), (1,'D'), (10,'HH');
SELECT Col
FROM #Test
ORDER BY CASE WHEN Prefix IS NULL THEN 1 ELSE 0 END,
Prefix,
Suffix;
This awful and unintuitive solution, that would be unnecessary if you stored the two pieces of data separately, brought to you by bad idea designs™:
;WITH cte AS
(
SELECT Col, rest = SUBSTRING(Col, pos, 100),
possible_int = TRY_CONVERT(bigint, CASE WHEN pos <> 1 THEN
LEFT(Col, COALESCE(NULLIF(pos,0),100)-1) END)
FROM (SELECT Col, pos = PATINDEX('%[^0-9]%', Col) FROM #Test) AS src
)
SELECT Col FROM cte
ORDER BY CASE
WHEN possible_int IS NULL THEN 2 ELSE 1 END,
possible_int,
rest;
Result:
Col
1
1D
2
10
10HH
A
B
C1
Example db<>fiddle

Insert a random string from a list into a table

I'm trying to insert a random department name into an SQL Server table. Currently I have the code below. I want any of the four department values listed (comp sci, biology, psychology,chemistry) to be inserted randomly when I populate my table with sample data. Any help would be appreciated
Declare #SID int
Set #SID = 1
/* Create temporary table to insert random department name
declare #myList table (Dept varchar(50))
insert into #myList values ('Computer Science'), ('Biology'), ('Psychology'), ('Chemistry')*/
While #SID <= 12000
Begin
Insert Into Student values ('Student', CAST(#SID as nvarchar(10)), 'Department' + CAST(#SID as nvarchar(10)), '50')
Print #SID
Set #SID = #SID + 1
End
Use a select (rather than values) to select from the list you have created and insert the top 1 ordered by newid().
--insert into dbo.Student (Name, id, Department, Position)
select top 1 'Student', CAST(#SID as nvarchar(10)), Dept, '50'
from #myList
order by newid();
Note: Its best practice to list all columns being inserted into.
Since you required 12,000 of random data, use #myList to CROSS JOIN itself. And use row_number() to generate the sequential number. The random part is handle by NEWID()
with stud as
(
select SID = row_number() over (order by newid()),
l1.Dept
from #myList l1
cross join #myList l2
cross join #myList l3
cross join #myList l4
cross join #myList l5
cross join #myList l6
cross join #myList l7
)
select *
from stud
where SID <= 12000
For TSQL, you can generate a random number, then use the first digit to determine which value to use, eg
declare #ran int = (select rand() * 10)
declare #subject varchar(100)=''
if #ran <= 3
set #subject = 'computer science'
else if #ran <= 6
set #subject = 'Biology'
else if #ran <= 8
set #subject = 'Psychology'
else
set #subject = 'Chemistry'
insert into table ...... etc etc

SQL Server stored procedure looping through a comma delimited cell

I am trying to figure out how to go about getting the values of a comma separated string that's present in one of my cells.
This is the query I current am trying to figure out in my stored procedure:
SELECT
uT.id,
uT.permissions
FROM
usersTbl AS uT
INNER JOIN
usersPermissions AS uP
/*Need to loop here I think?*/
WHERE
uT.active = 'true'
AND
uT.email = 'bbarker#thepriceisright.com'
The usersPermissions table looks like this:
And so a row in the usersTbl table looks like this for permissions:
1,3
I need to find a way to loop through that cell and get each number and place the name ****, in my returned results for the usersTbl.permissions.
So instead of returning this:
Name | id | permissions | age |
------------------------------------
Bbarker | 5987 | 1,3 | 87 |
It needs to returns this:
Name | id | permissions | age |
------------------------------------
Bbarker | 5987 | Read,Upload | 87 |
Really just replacing 1,3 with Read,Upload.
Any help would be great from a SQL GURU!
Reworked query
SELECT
*
FROM
usersTbl AS uT
INNER JOIN
usersPermissionsTbl AS uPT
ON
uPT.userId = uT.id
INNER JOIN
usersPermissions AS uP
ON
uPT.permissionId = uP.id
WHERE
uT.active='true'
AND
uT.email='bBarker#thepriceisright.com'
I agree with all of the comments... but strictly trying to do what you want, here's a way with a splitter function
declare #usersTbl table ([Name] varchar(64), id int, [permissions] varchar(64), age int)
insert into #usersTbl
values
('Bbarker',5987,'1,3',87)
declare #usersTblpermissions table (id int, [type] varchar(64))
insert into #usersTblpermissions
values
(1,'Read'),
(2,'Write'),
(3,'Upload'),
(4,'Admin')
;with cte as(
select
u.[Name]
,u.id as UID
,p.id
,p.type
,u.age
from #usersTbl u
cross apply dbo.DelimitedSplit8K([permissions],',') x
inner join #usersTblpermissions p on p.id = x.Item)
select distinct
[Name]
,UID
,age
,STUFF((
SELECT ',' + t2.type
FROM cte t2
WHERE t.UID = t2.UID
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
from cte t
Jeff Moden Splitter
CREATE FUNCTION [dbo].[DelimitedSplit8K] (#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
/* "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
enough to cover VARCHAR(8000)*/
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
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 (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
GO
First, you should read Is storing a delimited list in a database column really that bad?, where you will see a lot of reasons why the answer to this question is Absolutely yes!
Second, you should add a table for user permissions since this is clearly a many to many relationship.
Your tables might look something like this (pseudo code):
usersTbl
(
Id int primary key
-- other user related columns
)
usersPermissionsTbl
(
UserId int, -- Foreign key to usersTbl
PermissionId int, -- Foreign key to permissionsTbl
Primary key (UserId, PermissionId)
)
permissionsTbl
(
Id int primary key,
Name varchar(20)
)
Once you have your tables correct, it's quite easy to get a list of comma separated values from the permissions table.
Adapting scsimon's sample data script to a correct many to many relationship:
declare #users table ([Name] varchar(64), id int, age int)
insert into #users values
('Bbarker',5987,87)
declare #permissions table (id int, [type] varchar(64))
insert into #permissions values
(1,'Read'),
(2,'Write'),
(3,'Upload'),
(4,'Admin')
declare #usersPermissions as table (userId int, permissionId int)
insert into #usersPermissions values (5987, 1), (5987, 3)
Now the query looks like this:
SELECT u.Name,
u.Id,
STUFF(
(
SELECT ','+ [type]
FROM #permissions p
INNER JOIN #usersPermissions up ON p.id = up.permissionId
WHERE up.userId = u.Id
FOR XML PATH('')
)
, 1, 1, '') As Permissions,
u.Age
FROM #Users As u
And the results:
Name Id Permissions Age
Bbarker 5987 Read,Upload 87
You can see a live demo on rextester.
I concur with much of the advice being presented to you in the other responses. The structure you're starting with is not going to be fun to maintain and work with. However, your situation may mean you are stuck with it so maybe some of the tools below will help you.
You can parse the delimiter with charindex() as others demonstrated here- MSSQL - How to split a string using a comma as a separator
... and even better here (several functions are provided) - Split function equivalent in T-SQL?
If you still want to do it with raw inline SQL and are committed to a loop, then pair the string manipulation with a CURSOR. Cursors have their own controversies BTW. The code below will work if your permission syntax remains consistent, which it probably doesn't.
They used charindex(',',columnName) and fed the location into the left() and right() functions along with some additional string evaluation to pull values out. You should be able to piece those together with a cursor
Your query might look like this...
--creating my temp structure
declare #userPermissions table (id int, [type] varchar(16))
insert into #userPermissions (id, [type]) values (1, 'Read')
insert into #userPermissions (id, [type]) values (2, 'Write')
insert into #userPermissions (id, [type]) values (3, 'Upload')
insert into #userPermissions (id, [type]) values (4, 'Admin')
declare #usersTbl table ([Name] varchar(16), id int, [permissions] varchar(8), age int)
insert into #usersTbl ([Name], id, [permissions], age) values ('Bbarker', 5987, '1,3', 87)
insert into #usersTbl ([Name], id, [permissions], age) values ('Mmouse', 5988, '2,4', 88)
--example query
select
ut.[Name]
, (select [type] from #userPermissions where [id] = left(ut.[permissions], charindex(',', ut.[permissions])-1) )
+ ','
+ (select [type] from #userPermissions where [id] = right(ut.[permissions], len(ut.[permissions])-charindex(',', ut.[permissions])) )
from #usersTbl ut

How do I replace strings of a table from another table column

How do I update/replace the value of the first table from the list of my second table in SQL. Sorry im not so good in using replace() of SQL especially replacing from values base from different table
First table.
ID | Value
======================
1 | Fruits[Apple]
2 | Fruits[Apple,Mango]
3 | Apple[Red,Green]
Second table
Search | Replace
=========================
Apple | Orange
Green | Yellow
You will need some kind of recursive replace.
something like a loop
declare #t1 table (ID int, Value varchar(max))
declare #t2 table (Search varchar(max), ReplaceWith varchar(max))
insert #t1 values (1, 'Fruits[Apple]'),(2, 'Fruits[Apple,Mango]'), (3, 'Apple[Red,Green]')
insert #t2 values ('Apple', 'Orange'),('Green', 'Yellow')
--loop nth times for rows that have more than one match
while exists(select top 1 * from #t1 inner join #t2 on charindex(Search, Value ) > 0)
begin
update #t1
set Value = replace(Value, Search, ReplaceWith)
from #t2
inner join #t1 on charindex(Search, Value ) > 0
end
select * from #t1
results
ID Value
----- -----------------------
1 Fruits[Orange]
2 Fruits[Orange,Mango]
3 Orange[Red,Yellow]
Alternatively, you could use recursive CTE
;with CTE(ID, Value, rec_count)
as (
select distinct ID, Value, 1 as rec_count from #t1 inner join #t2 on charindex(Search, Value ) > 0
union all
select ID, Value = replace(Value, Search, ReplaceWith), rec_count +1
from CTE
inner join #t2 on charindex(Search, Value ) > 0
)
update #t1
set Value= replaced.Value
from #t1 t
inner join
( select distinct ID, Value
from CTE c
where rec_count > 1
and rec_count = (select max(rec_count) from CTE where ID = c.ID) ) replaced on replaced.ID = t.ID
Simply use following UPDATE by cross-joined select statement and enjoy it! ;)
UPDATE tFirst
SET Value = REPLACE(tFirst.Value, tSecond.Search, tSecond.Replace)
FROM
[First] tFirst
CROSS JOIN [Second] tSecond

SQL Trigger to split string during insert without a common delimiter and store it into another table

Currently I have a system that is dumping data into a table with the format:
Table1
Id#, row#, row_dump
222, 1, “set1 = aaaa set2 =aaaaaa aaaa dd set4=1111”
I want to take the row dump and transpose it into rows and insert it into another table of the format:
Table2
Id#, setting, value
222, ‘set1’,’aaa’
222, ‘set2’,’aaaaaa aaaa dd’
222, ‘set4’,’1111’
Is there a way to make a trigger in MSSQL that will parse this string on insert in Table1 and insert it into Table2 properly?
All of the examples I’ve found required a common delimiter. ‘=’ separates the setting from the value, space(s) separate a value from a setting but a value could have spaces in it (settings do not have spaces in them so the last word before the equal sign is the setting name but there could be spaces between the setting name and equal sign).
There could be 1-5 settings and values in any given row. The values can have spaces. There may or may not be space between the setting name and the ‘=’ sign.
I have no control over the original insert process or format as it is used for other purposes.
You could use 'set' as a delimiter. This is a simple sample. It obviously may have to be molded to your environment.
use tempdb
GO
IF OBJECT_ID('dbo.fn_TVF_Split') IS NOT NULL
DROP FUNCTION dbo.fn_TVF_Split;
GO
CREATE FUNCTION dbo.fn_TVF_Split(#arr AS NVARCHAR(2000), #sep AS NCHAR(3))
RETURNS TABLE
AS
RETURN
WITH
L0 AS (SELECT 1 AS C UNION ALL SELECT 1) --2 rows
,L1 AS (SELECT 1 AS C FROM L0 AS A, L0 AS B) --4 rows (2x2)
,L2 AS (SELECT 1 AS C FROM L1 AS A, L1 AS B) --16 rows (4x4)
,L3 AS (SELECT 1 AS C FROM L2 AS A, L2 AS B) --256 rows (16x16)
,L4 AS (SELECT 1 AS C FROM L3 AS A, L3 AS B) --65536 rows (256x256)
,L5 AS (SELECT 1 AS C FROM L4 AS A, L4 AS B) --4,294,967,296 rows (65536x65536)
,Nums AS (SELECT row_number() OVER (ORDER BY (SELECT 0)) AS N FROM L5)
SELECT
(n - 1) - LEN(REPLACE(LEFT(#arr, n-1), #sep, N'')) + 1 AS pos,
SUBSTRING(#arr, n, CHARINDEX(#sep, #arr + #sep, n) - n) AS element
FROM Nums
WHERE
n <= LEN(#arr) + 3
AND SUBSTRING(#sep + #arr, n, 3) = #sep
AND N<=100000
GO
declare #t table(
Id int,
row int,
row_dump varchar(Max)
);
insert into #t values(222, 1, 'set1 = aaaa set2 =aaaaaa aaaa dd set4=1111')
insert into #t values(111, 2, ' set1 =cx set2 =4444set4=124')
DECLARE #t2 TABLE(
Id int,
Setting VARCHAR(6),
[Value] VARCHAR(50)
)
insert into #t2 (Id,Setting,Value)
select
Id,
[Setting]='set' + left(LTRIM(element),1),
[Value]=RIGHT(element,charindex('=',reverse(element))-1)
from #t t
cross apply dbo.fn_TVF_Split(row_dump,'set')
where pos > 1
order by
id asc,
'set' + left(LTRIM(element),1) asc
select *
from #t2
Update
You could do something like this. It is not optimal and could probably be better handled in the transformation tool or application. Anyway here we go.
Note: You will need the split function I posted before.
declare #t table(
Id int,
row int,
row_dump varchar(Max)
);
insert into #t values(222, 1, 'set1 = aaaa set2 =aaaaaa aaaa dd set3=abc set4=1111 set5=7373')
insert into #t values(111, 2, 'set1 =cx set2 = 4444 set4=124')
DECLARE #t2 TABLE(
Id int,
Setting VARCHAR(6),
[Value] VARCHAR(50)
)
if OBJECT_ID('tempdb.dbo.#Vals') IS NOT NULL
BEGIN
DROP TABLE #Vals;
END
CREATE TABLE #Vals(
Id INT,
Row INT,
Element VARCHAR(MAX),
pos int,
value VARCHAR(MAX)
);
insert into #Vals
select
Id,
row,
element,
pos,
Value=STUFF(LEFT(element,len(element) - CHARINDEX(' ',reverse(element))),1,1,'')
from(
select
Id,
row,
row_dump = REPLACE(REPLACE(REPLACE(row_dump,'= ','='),' =','='),'=','=|')
from #t
) AS t
cross apply dbo.fn_TVF_Split(row_dump,'=')
where pos >=1 and pos < 10
insert into #t2 (Id,Setting,Value)
select
t1.Id,
Setting =
(
SELECT TOP 1
CASE WHEN t2.pos = 1
THEN LTRIM(RTRIM(t2.element))
ELSE LTRIM(RTRIM(RIGHT(t2.element,CHARINDEX(' ',REVERSE(t2.element)))))
END
FROM #Vals t2
where
t2.Id = t1.id
and t2.row = t1.row
and t2.pos < t1.pos
ORDER BY t2.pos DESC
),
t1.Value
from #Vals t1
where t1.pos > 1 and t1.pos < 10
order by t1.id,t1.row,t1.pos
select * from #t2