I'm developing a system that manages work orders for vehicles. The ID of work orders is composed as follows: OT-001-16.
Where OT- is a string, 001 is the counter, followed by - character and finally the number 16 is the current year.
Example:
If the current year is 2018, the ID should be OT-001-18.
The problem is when the year changes, the counter must restart from 001. I have a stored procedure to do that, but i think i'm doing a lot more work.
This is my stored procedure code:
CREATE PROCEDURE ot (#name varchar(100), #area varchar(100), #idate varchar(100), #edate varchar(100))
AS
BEGIN
SET NOCOUNT ON;
DECLARE #aux varchar(100);
DECLARE #aux2 varchar(100);
DECLARE #aux3 int;
DECLARE #aux4 varchar(100);
SELECT #aux = id_workorder FROM idaux;
IF (#aux IS NULL)
SET #aux = CONCAT('OT-000-', RIGHT(YEAR(GETDATE()), 2));
SET
#aux2 = SUBSTRING(
#aux, CHARINDEX('-', #aux) + 1,
LEN(#aux) - CHARINDEX('-', #aux) - CHARINDEX('-', REVERSE(#aux)));
SET #aux3 = CAST(#aux2 AS int) + 1;
SET #aux4 = #aux3;
IF #aux3 < 1000
IF #aux3 >= 10
SET #aux4 = CONCAT('0', #aux4);
ELSE
SET #aux4 = CONCAT('00', #aux4);
ELSE
SET #aux4 = #aux4;
DECLARE #f varchar(100);
DECLARE #y varchar(50);
SELECT TOP 1
#y = id_workorder
FROM workorder
WHERE (RIGHT(id_workorder, 2)) = (RIGHT(YEAR(GETDATE()), 2))
ORDER BY id_workorder DESC;
DECLARE #yy varchar(10);
SET
#yy = RIGHT(#y, 2);
DECLARE #yn varchar(10);
SET
#yn = RIGHT(YEAR(GETDATE()), 2);
BEGIN
IF #yn = #yy
BEGIN
DECLARE #laux varchar(20)
SET #f = 'OT-' + #aux4 + '-' + RIGHT(YEAR(GETDATE()), 2);
INSERT INTO workorder (id_workorder, name, area, initial_date, end_date)
VALUES (#f, #name, #area, #idate, #edate);
SELECT
#laux = id_workorder
FROM idaux
IF (#laux IS NULL)
BEGIN
INSERT idaux (id_workorder) VALUES (#f);
END
ELSE
BEGIN
UPDATE idaux SET id_workorder = #f;
END
END
ELSE
BEGIN
SET #f = CONCAT('OT-001-', (RIGHT(YEAR(GETDATE()), 2)));
INSERT INTO workorder (id_workorder, name, area, initial_date, end_date)
VALUES (#f, #name, #area, #idate, #edate);
SELECT #laux = id_workorder FROM idaux;
IF (#laux IS NULL)
BEGIN
INSERT idaux (id_workorder) VALUES (#f);
END
ELSE
BEGIN
UPDATE idaux SET id_workorder = #f;
END
END
END
END
Basically, i created an auxiliar table to save the last Work Order ID, then from this table called idaux i take the ID and i compared to new possible ID by a string handling. Then if the year of the last ID saved are equal to the current year the counter increases, but if not the counter is restarted to 001, the new ID is updated in the auxiliar table and the Work Order is inserted to the table workorder.
My stored procedure works, but i need your help to optimize the stored procedure. Any question post on comments.
Here is how I'd setup the stored procedure and the underlying table to keep track of your work orders:
create database tmpWorkOrders;
go
use tmpWorkOrders;
go
/*
The work order ID (as you wish to see it) and the
work order counter (per year) will be separated into
two separate columns (with a unique constraint).
The work order ID (you wish to see) is automatically
generated for you and stored "persisted":
http://stackoverflow.com/questions/916068/sql-server-2005-computed-column-is-persisted
*/
create table WorkOrders
(
SurrogateKey int identity(1, 1) primary key not null,
WorkOrderYear int not null,
WorkOrderCounter int not null,
WorkOrderID as
N'OT-' + right(N'000' + cast(WorkOrderCounter as nvarchar), 3)
+ N'-' + right(cast(WorkOrderYear as nvarchar), 2)persisted,
WorkOrderDescription nvarchar(200),
constraint UQ_WorkOrderIDs
unique nonclustered (WorkOrderYear, WorkOrderCounter)
);
go
create procedure newWorkOrder
(#WorkOrderYear int = null,
#WorkOderCounter int = null,
#WorkOrderDescription nvarchar(200) = null
)
as
begin
/*
If no year is given the the current year is assumed
*/
if #WorkOrderYear is null
begin
set #WorkOrderYear = year(current_timestamp);
end;
/*
If no work order counter (for the above year) is given
then the next available one will be given
*/
if #WorkOderCounter is null
begin
set #WorkOderCounter
= isnull(
(
select max(WorkOrderCounter)
from WorkOrders
where WorkOrderYear = #WorkOrderYear
) + 1,
0
);
end;
else
/*
If a work order counter has been passed to the
stored procedure then it must be validated first
*/
begin
/*
Does the work order counter (for the given year)
already exist?
*/
if exists
(
select 1
from dbo.WorkOrders as wo
where wo.WorkOrderYear = #WorkOrderYear
and wo.WorkOrderCounter = #WorkOderCounter
)
begin
/*
If the given work order counter already exists
then the next available one should be assigned.
*/
while exists
(
select 1
from dbo.WorkOrders as wo
where wo.WorkOrderYear = #WorkOrderYear
and wo.WorkOrderCounter = #WorkOderCounter
)
begin
set #WorkOderCounter = #WorkOderCounter + 1;
end;
end;
end;
/*
The actual insert of the new work order ID
*/
insert into dbo.WorkOrders
(
WorkOrderYear,
WorkOrderCounter,
WorkOrderDescription
)
values
(#WorkOrderYear,
#WorkOderCounter,
#WorkOrderDescription
);
end;
go
/*
Some test runs with the new table and stored procedure...
*/
exec dbo.newWorkOrder #WorkOrderYear = null,
#WorkOderCounter = null,
#WorkOrderDescription = null;
exec dbo.newWorkOrder #WorkOrderYear = null,
#WorkOderCounter = 3,
#WorkOrderDescription = null;
exec dbo.newWorkOrder #WorkOrderYear = null,
#WorkOderCounter = 0,
#WorkOrderDescription = null;
exec dbo.newWorkOrder #WorkOrderYear = null,
#WorkOderCounter = 0,
#WorkOrderDescription = null;
exec dbo.newWorkOrder #WorkOrderYear = null,
#WorkOderCounter = 0,
#WorkOrderDescription = null;
/*
...reviewing the result of the above.
*/
select *
from dbo.WorkOrders as wo;
Note, that the "next available" work order counter is once given (1) as the maximum + 1 and once (2) increased until it does not violate the unique key constraint on the table anymore. Like this you have two different possibilities to go about it.
There are a number of observations based on your code that you could alter to optimize and guarantee your results.
I am not aware of your Table Structure, but it seems you are using natural keys for your IDs.
Instead, use a surrogate key, such as INT/BIGINT to not only add efficiency in your table joins (no strings required), but potentially add another layer of security in your current design.
Alternatively, normalize the column into the flags they are. For example: OT-001-05 has three elements: OT is a type of work order, 001 is the ID, and 15 is the year. Presently, OT determines the ID which determines the year.
SELECT #aux = id_workorder FROM idaux;
idaux was not described. Is it a single value? If tabular, guarantee the result or it might break in the future.
Even if you add MAX(id_workorder), your result will not work as you think. Since this is a VARCHAR, the greatest value of the leftmost character not tied will return.
#aux, CHARINDEX('-', #aux) + 1,
LEN(#aux) - CHARINDEX('-', #aux) - CHARINDEX('-', REVERSE(#aux)));
This is fine, but overall you could make the code clearer and easier to debug by splitting all three of those elements into their own variable. Your still using your method, but simplified a little (personally, CHARINDEX can be a pain).
SET #aux = #Type -- 'OT'
SET #aux2 = #ID -- The result of your previous code
SET #aux3 = #Year -- your YY from GETDATE()
-- then join
SET #Work_Order = CONCAT(#aux, '-', #aux2, '-', #aux3)
Update:
Currently, your column in idaux has the ID in the MIDDLE of your column. This will produce disastrous results since any comparison of IDs will happen in the middle of the column. This means at best you might get away with PATINDEX but are still performing a table scan on the table. No index (save for FULLTEXT) will be utilized much less optimized.
I should add, if you put the ID element into its own column, you might find using BINARY collations on the column will improve its performance. Note I have not tested attempting a BINARY collation on a mixed column
Related
My problem is this; I have a field in a table that contains values like this:
NP
NP;MC;PE
MC;AB;AT;MI;TC;WM
OS
OG
I want to convert these abbreviations to their full name. i.e. NP becomes Nuclear Power, OG becomes Oil and Gas, MI becomes Military etc.
My desired output would be:
Nuclear Power
Nuclear Power;Military;Pesticides
and so on.
I'm creating this as a function. I got it working for just the one abbreviation and then the same for two. However my issue is that I may have 5 abbreviations or 7. I know my current approach is dreadful but cannot figure out how to loop it in the right way.
Please note: I've shortened the list of abbreviations for StackOverflow but there's 25 in total.
Please further note: I did the function bottom up (I don't know why) and got the two value and single value working. I've removed anything I did for values over 3 as nothing I did worked.
ALTER FUNCTION [dbo].[get_str_full]
(
-- Add the parameters for the function here
#str_input VARCHAR(250)
)
RETURNS VARCHAR(250)
AS
BEGIN
-- Declare the return variable here
DECLARE #Result VARCHAR(250)
DECLARE #TEMPSTRING VARCHAR(250)
DECLARE #TEMPSTRING_RIGHT AS VARCHAR(250)
-- DECLARE #PI_COUNT BIGINT
DECLARE #COUNTER INT
DECLARE #TOTAL_VALS BIGINT
DECLARE #STRING_ST VARCHAR(250)
DECLARE #POS_STR BIGINT
DECLARE #REMAINING_STR VARCHAR(250)
-- Used for easy loop skips
DECLARE #LEFTSKIP AS BIGINT
SET #LEFTSKIP = 1
SET #Result = #str_input
SET #STRING_ST = #Result
SET #COUNTER = (LEN(#Result) - LEN(REPLACE(#Result,';',''))) + 1
SET #TOTAL_VALS = (LEN(#Result) - LEN(REPLACE(#Result,';',''))) + 1
-- If the string has a semicolon then there's more than one PI value
IF CHARINDEX(';', #Result) > 0
BEGIN
WHILE #COUNTER > 0
BEGIN
IF #TOTAL_VALS >= 3 -- If counter is more than 2 then there's three or more
BEGIN
DECLARE #TEMP_VAL BIGINT
SET #TEMP_VAL = 5
END
ELSE IF #TOTAL_VALS = 2-- Theres 2
BEGIN
-- Do left two chars first
IF #LEFTSKIP = 1
BEGIN
SET #TEMPSTRING = LEFT(#Result, 2)
SELECT #TEMPSTRING = CASE #TEMPSTRING
WHEN 'MC' THEN 'Military Contracting'
WHEN 'NP' THEN 'Nuclear'
WHEN 'OG' THEN 'Oil & Gas'
WHEN 'OS' THEN 'Oil Sands'
WHEN 'PM' THEN 'Palm Oil'
WHEN 'PE' THEN 'Pesticides'
ELSE #TEMPSTRING
END
SET #LEFTSKIP = 2
END
ELSE IF #LEFTSKIP = 2
BEGIN
SET #TEMPSTRING_RIGHT = RIGHT(#Result, 2)
SELECT #TEMPSTRING_RIGHT = CASE #TEMPSTRING_RIGHT
WHEN 'MC' THEN 'Military Contracting'
WHEN 'NP' THEN 'Nuclear'
WHEN 'OG' THEN 'Oil & Gas'
WHEN 'OS' THEN 'Oil Sands'
WHEN 'PM' THEN 'Palm Oil'
WHEN 'PE' THEN 'Pesticides'
ELSE #TEMPSTRING_RIGHT
END
END
END
SET #COUNTER = #COUNTER - 1
END
SET #Result = CONCAT(#TEMPSTRING,';', #TEMPSTRING_RIGHT)
END
ELSE
BEGIN
SET #Result = REPLACE(#Result, 'MC', 'Military Contracting')
SET #Result = REPLACE(#RESULT, 'NP', 'Nuclear Power')
SET #Result = REPLACE(#Result, 'OG', 'Oil & Gas')
SET #Result = REPLACE(#Result, 'OS', 'Oil Sands')
SET #Result = REPLACE(#Result, 'PM', 'Palm Oil')
SET #Result = REPLACE(#Result, 'PE', 'Pesticides')
END
-- Return the result of the function
RETURN #Result
END
First for some easily consumable sample data:
DECLARE #tranlation TABLE(tCode VARCHAR(10), tString VARCHAR(40));
DECLARE #t TABLE(String VARCHAR(1000));
INSERT #t VALUES('PE;N'),('NP'),('NP;MC;PE;XX')
INSERT #tranlation VALUES ('N','Nukes'),('NP','Nuclear Power'),('MC','Military'),
('PE','Pesticides');
Note my updated sample data which includes "XX", which has no match , and an "N" for "Nukes" which would wreck any solution which leverages REPLACE. If you are on SQL 2016+ you can use STRING_SPLIT and STRING_AGG.
SELECT
OldString = t.String,
NewString = STRING_AGG(ISNULL(tx.tString,items.[value]),';')
FROM #t AS t
OUTER APPLY STRING_SPLIT(t.String,';') AS items
LEFT JOIN #tranlation AS tx
ON items.[value] = tx.tCode
GROUP BY t.String ;
Returns:
OldString NewString
----------------- -------------------------------------------
NP Nuclear Power
NP;MC;PE;XX Nuclear Power;Military;Pesticides;XX
PE;N Pesticides;Nukes
You should really fix your table design so that you do not store multiple pieces of info in one column.
If you would like it as a function, I would strongly recommend an inline Table-Valued function rather than a scalar function.
If you have SQL Server version 2017+ you can use STRING_SPLIT and STRING_AGG for this.
CREATE OR ALTER FUNCTION GetFullStr
( #str varchar(250) )
RETURNS TABLE
AS RETURN
(
SELECT STRING_AGG(ISNULL(v.FullStr, s.value), ';') result
FROM STRING_SPLIT(#str, ';') s
LEFT JOIN (VALUES
('MC', 'Military Contracting'),
('NP', 'Nuclear'),
('OG', 'Oil & Gas'),
('OS', 'Oil Sands'),
('PM', 'Palm Oil'),
('PE', 'Pesticides')
) v(Abbr, FullStr) ON v.Abbr = s.value
);
GO
You can, and should, replace the VALUES with a real table.
On 2016 you would need FOR XML PATH instead of STRING_AGG:
CREATE OR ALTER FUNCTION GetFullStr
( #str varchar(250) )
RETURNS TABLE
AS RETURN
(
SELECT STUFF(
(SELECT ';' + ISNULL(v.FullStr, s.value)
FROM STRING_SPLIT(#str, ';') s
LEFT JOIN (VALUES
('MC', 'Military Contracting'),
('NP', 'Nuclear'),
('OG', 'Oil & Gas'),
('OS', 'Oil Sands'),
('PM', 'Palm Oil'),
('PE', 'Pesticides')
) v(Abbr, FullStr) ON v.Abbr = s.value
FOR XML PATH(''), TYPE
).value('text()[1]','varchar(2500)'),
, 1, 1, '')
);
GO
You use it like this:
SELECT s.result AS FullStr
FROM table
OUTER APPLY GetFullStr(value) AS s;
-- alternatively
SELECT (SELECT * FROM GetFullStr(value)) AS FullStr
FROM table;
You could assign your abbreviation mappings to a TABLE variable and then use that for your REPLACE. You could build this into a function, then pass your string values in.
The test below returns Military:Nuclear Power:XX.
declare #mapping table (abbrev varchar(50), fullname varchar(100))
insert into #mapping(abbrev, fullname)
values ('NP','Nuclear Power'),
('MC','Military')
declare #testString varchar(100), #newString varchar(100)
set #teststring = 'MC:NP:XX'
set #newString = #testString
SELECT #newString = REPLACE(#newString, abbrev, fullname) FROM #mapping
select #newString
I have some customer data that needs to be anonymised. I have customerIds which consists of numbers.
for example:
CustomerID
3937487
I need to swap each digit with an alternative, which should be enough for my requirement. Based on the following lookup table
Only issue I'm having is when I use the REPLACE function on the field:
REPLACE(REPLACE(CustomerID,2,9),9,6)
which gives me
CustomerID
3637487
It's swapping the digit 2 to a 9, then that same 9 to a 6. It needs to only replace the digits ONCE.
As I'm going to be changing millions of records in one go, using temp tables isn't possible from a performance perspective. Can this be done in one query, recursively?
I can't think of any way of accomplishing this in a single query. If I wanted to do this I'd create a function something along the lines of
CREATE FUNCTION [dbo].[AnonymiseId]
(
#Id [int]
)
RETURNS [int]
AS
BEGIN
-- Declare the return variable here
DECLARE #ResultVar int;
DECLARE #substitutions nvarchar(10) = '7295380146';
DECLARE #stringId nvarchar(100) = CONVERT(nvarchar(100), #Id);
DECLARE #i int = 1
DECLARE #substituteStringId nvarchar(100) = '';
WHILE #i <= LEN(#stringID)
BEGIN
DECLARE #char nvarchar = SUBSTRING(#stringId, #i, 1);
DECLARE #charValue int = CONVERT(int, #char);
DECLARE #subsChar nvarchar = SUBSTRING(#substitutions, #charValue + 1, 1);
SET #substituteStringId = CONCAT(#substituteStringId, #subsChar);
SET #i = #i + 1
END
SET #ResultVar = CONVERT(int, #substituteStringId);
-- Return the result of the function
RETURN #ResultVar;
END
GO
and then just use it in the query
SELECT dbo.AnonymiseId(CustomerID) FROM ???
I have a while loop query that I only want to run until 11PM everyday - I'm aware this can be achieved with a WAITFOR statement, and then just END the query.
However, on the following day, once I re-run my query, I want it to continue from where it stopped on the last run. So I'm thinking of creating a log table that will contain the last processed ID.
How can I achieve this?
DECLARE #MAX_Value BIGINT = ( SELECT MAX(ID) FROM dbo.TableA )
DECLARE #MIN_Value BIGINT = ( SELECT MIN(ID) FROM dbo.TableA )
WHILE (#MIN_Value < #MAX_Value )
BEGIN
INSERT INTO dbo.MyResults
/* Do some processing*/
….
….
….
SET #MIN_Value = MIN_Value + 1
/*I only want the above processing to run until 11PM*/
/* Once it’s 11PM, I want to save the last used #MIN_Value
into my LoggingTable (dbo.Logging) and kill the above processing.*/
/* Once I re-run the query I want my processing to restart from the
above #MIN_Value which is recorded in dbo.Logging */
END
Disclaimer: I do not recommend using WHILE loops in SQL Server but considering the comment that you want a solution in SQL, here you go:
-- First of all, I strongly recommend using a different way of assigning variable values to avoid scenarios with the variable being NULL when the table is empty, also you can do it in a single select.
-- Also, if something started running at 10:59:59 it will let the processing for the value finish and will not simply rollback at 11.
CREATE TABLE dbo.ProcessingValueLog (
LogEntryId BIGINT IDENTITY(1,1) NOT NULL,
LastUsedValue BIGINT NOT NULL,
LastUsedDateTime DATETIME NOT NULL DEFAULT(GETDATE()),
CompletedProcessing BIT NOT NULL DEFAULT(0)
)
DECLARE #MAX_Value BIGINT = 0;
DECLARE #MIN_Value BIGINT = 0;
SELECT
#MIN_Value = MIN(ID),
#MAX_Value = MAX(ID)
FROM
dbo.TableA
SELECT TOP 1
#MIN_Value = LastUsedValue
FROM
dbo.ProcessingValueLog
WHERE
CompletedProcessing = 1
ORDER BY
LastUsedDateTime DESC
DECLARE #CurrentHour TINYINT = HOUR(GETDATE());
DECLARE #LogEntryID BIGINT;
WHILE (#MIN_Value < #MAX_Value AND #CurrentHour < 23)
BEGIN
INSERT INTO dbo.ProcessingValueLog (LastUsedValue)
VALUE(#MIN_Value)
SELECT #LogEntryID = SCOPE_IDENTITY()
// Do some processing...
SET #MIN_Value = #MIN_Value + 1;
UPDATE dbo.ProcessingValueLog
SET CompletedProcessing = 1
WHERE LogEntryId = #LogEntryID
SET #CurrentHour = HOUR(GETDATE())
END
I am trying to do an insert, so that when a clientID value is NULL then insert the client data into the client table.
I have a loop that cycles through data entered into a temp table, when the clientID is null it should do an insert and choose the next sequential client reference for that client then delete the row from the temp table and move onto the next.
The problem is when the loop does the second insert or more, it is using the SAME client reference even though I have specified +1. The below is an extract of the loop, can anyone figure out why after the first insert the client reference stays the same? If I run the insert by itself with the loop and select #result it shows the vaues sequentially so I don't understand why when the script runs it doesn't insert the reference sequentially.
Declare #Id int
WHILE EXISTS(SELECT * FROM #Temp)
begin
select top 1 #id = ID from #temp
IF (select clientID from #m1 where id = #id) is null AND (select renewalinsuredid from #m1 where id=#id) is not null and (select renewalmasterID from #m1 where id=#id) is not null
BEGIN
declare #result varchar(10)
SELECT #Result = (MAX(CAST(SUBSTRING(ClientReference1,3,6) AS INTEGER)) + 1) FROM Client
set #result = 'CR0' + #result
INSERT INTO Client (clientid,InsuredName,InsuredId,MasterInsuredId,ClientReference1)
SELECT newid(),insuredname,RenewalInsuredID,RenewalMasterID,#result from #M1 where id = #id
PRINT 'Client ref '+ cast(#result as varchar(64)) +' inserted for policy ' + #result2
END
DELETE from #temp where ID = #Id
END
you do...
SELECT #Result = (MAX(CAST(SUBSTRING(ClientReference1,3,6) AS INTEGER)) + 1) FROM Client
...to get the maximum client reference CAST to an integer, then add 1 to it - this doesn't guarantee that #Result is 'greater' than all the other ClientReference1, because ClientReference1 appears to be a text field - consider a field '9', cast it to integer, and add 1 - what have you got? A 10 - if I change back to text, then '10' < '9' in terms of alphanumerics
interchanging from numeric to string, and sorting numerics that have changed to strings, can have unwanted effects. Also you add'CR0' to the start of the string, that could confuse things possibly.
So I managed to get it to work after converting the value. It seems to be due to the datatype being int as when converted to varchar the insert works correctly. The main thing that I don't understand is how if i just did select #result to see what the output was - it was correct every time, just didn't seem to insert the value correctly. Thanks for the help people.
declare #result varchar(10)
declare #Length int
declare #Refresult varchar(10)
SELECT #Result = (MAX(CAST(SUBSTRING(ClientReference1,3,6) AS INTEGER)) + 1) FROM Client
SET #Length = CONVERT(INT,#Result)
SET #Result = CONVERT(VARCHAR(10),#Length)
SET #Length = LEN(#Result)
IF(#Length =5)
SET #Result = '0' + #Result
IF #Result IS NULL
BEGIN
SET #Result = '00000' + '1'
END
SET #Refresult = 'CR' + #Result
To begin - I am new at SQL; be gentle.
I work with a school district and have recently been given "the keys" to access the database. I am interested in getting a list of students, and then generating a list of passwords for them. I have found some code which allows me to generate random passwords that I would like to incorporate into a SQL query which is gathering information from our Student database. (Thank you if this code is yours!)
My issue is that I have not been able to use the variables to create a different password for each record. I get the same randomly generated password for each student. (On a good note; at least the password changes each time I execute the query.)
I should mention that I have two accounts set up for the database access; one to simply read the information, and another with full editing rights. (I have only used this once and closed my eyes as I pushed the big red button to update some trivial information).
Results from the first run:
Name Password
JACEK mtwsz2ybu
CARL mtwsz2ybu
LARS mtwsz2ybu
Results from the second run:
Name Password
JACEK je4tm5ptw
CARL je4tm5ptw
LARS je4tm5ptw
This is the query I am running:
USE XXTableXX
DECLARE #position int, #string char(100), #length int,
#rand int, #newstring char(15), #newchar char(15);
SET #position = 1;
SET #string = 'abcdefghijkmnopqrstuvwxyz23456789';
SET #length = 9;
SET #newstring = ''
SET #newchar = ''
WHILE #position <= #length
BEGIN
SET #rand = FLOOR(RAND()*(33-1)+1);
SET #newchar = SUBSTRING(#string,#rand,1);
SET #newstring = STUFF(#newstring,len(#newstring)+1,1,#newchar)
SET #position = #position +1;
END;
SELECT DISTINCT s.firstname, #newstring AS [Password]
FROM XXStudentTableXX s
Put all the stuff above the query in a function and call the function in your query. A side note, it will execute for each row, which means a bit of a performance hit for large sets.
Here is a way to do this.
First, you will need to make the RAND() into it's own view since you can't call it from a function
CREATE VIEW [dbo].[NewRandom]
AS
SELECT RAND() AS [RandSeed]
GO
Now you must create your function that uses the view. This function accepts an integer so you can do variable password lengths. You can hardcode it in your query or take out the parameter and hard code it in the function.
CREATE FUNCTION [dbo].[ufn_GeneratePassword] ( #PasswordLength INT )
RETURNS VARCHAR(20)
AS
BEGIN
DECLARE #position int, #string char(100), #length int,
#rand int, #newstring char(15), #newchar char(15);
SET #position = 1;
SET #string = 'abcdefghijkmnopqrstuvwxyz23456789';
SET #length = #PasswordLength;
SET #newstring = ''
SET #newchar = ''
WHILE #position <= #length
BEGIN
SET #rand = FLOOR((SELECT RandSeed FROM dbo.[NewRandom])*(33-1)+1);
SET #newchar = SUBSTRING(#string,#rand,1);
SET #newstring = STUFF(#newstring,len(#newstring)+1,1,#newchar)
SET #position = #position +1;
END
RETURN #newstring
END
Now you can call the function on every row of your table
SELECT DISTINCT s.firstname, [dbo].[ufn_GeneratePassword](9) AS [Password]
FROM XXStudentTableXX s
I think what you will need to do for this is create a table, or table variable, then instead of selecting make it insert into the table. Also Add a loop that loops 1x for each student ID. This will then insert 1 row for each student and run the randomizer 1 time.
First covert your procedure into a scalar SQL function.
CREATE VIEW dbo.RandomNumberView
AS
SELECT RandomNumber = RAND();
GO
CREATE FUNCTION dbo.GeneratePassword()
RETURNS char(15)
AS
-- Generates and Returns a random password
BEGIN
DECLARE #position int, #string char(100), #length int,
#rand int, #newstring char(15), #newchar char(15);
SET #position = 1;
SET #string = 'abcdefghijkmnopqrstuvwxyz23456789';
SET #length = 9;
SET #newstring = ''
SET #newchar = ''
WHILE #position <= #length
BEGIN
SELECT #rand = FLOOR(RandomNumber *(33-1)+1) FROM RandomNumberView;
SET #newchar = SUBSTRING(#string,#rand,1);
SET #newstring = STUFF(#newstring,len(#newstring)+1,1,#newchar)
SET #position = #position +1;
END;
RETURN #newstring;
END;
GO
And then you can easily use it whatever way you want.
SELECT DISTINCT s.firstname, dbo.GeneratePassword() AS [Password]
FROM XXStudentTableXX s
Note that each time you run this query you would get different password against the same record. I would prefer to use this function to only generate and insert password in the table and not as a direct select statement.
To get the data from a random table, SQL Server provides the NEWID () function. As its name says every execution is generated a new GUID (Global Unique Identifier), these GUIDs are unique, so the value of the order will never be the same.
Therefore you do not need to create a random function that does it for you. So you may have your answer using a simple SQL.
CREATE TABLE STUDENT (
[ID] INT IDENTITY (1, 1) NOT NULL,
[NAME] NVARCHAR (100) NULL
);
INSERT INTO STUDENT (NAME) VALUES ('John');
INSERT INTO STUDENT (NAME) VALUES ('Carl');
INSERT INTO STUDENT (NAME) VALUES ('Mary');
INSERT INTO STUDENT (NAME) VALUES ('Joan');
select
ID
,NAME
,CONCAT(
CAST((ABS(CHECKSUM(NEWID()))%10) as varchar(1))
, CHAR(ASCII('a') + (ABS(CHECKSUM(NEWID())) % 25))
, CHAR(ASCII('A') + (ABS(CHECKSUM(NEWID())) % 25))
, LEFT(LOWER(NEWID()),2)
, LEFT(NEWID(),2)
, LEFT(LOWER(NEWID()),2)
) AS PASSWORD
from STUDENT
SQL Fiddle
However, you may wish to Make your own generator taking into account a high level of complexity generated password.
https://www.simple-talk.com/blogs/2009/09/30/strong-password-generator/