I have a sequence in SQL Server
CREATE SEQUENCE dbo.NextBusinessValue
START WITH 1
INCREMENT BY 1 ;
GO
And I'd like to use this to generate a 5 digit custom reference number that uses this sequence to create the number in the format A0000.
The rules for the reference number are that:
1-9999 would be A0001 - A9999
10000-19999 would be B0000 - B9999
20000-29999 would be C0000 - C9999 etc...
It won't ever get the the amount of data that requires going past Z.
I know I can get a letter by using:
SELECT CHAR(65)
So this would work for 1-9999:
declare #n int = 9999
SELECT CHAR(65) + right('0000' + convert(varchar(10), #n), 4)
But would fail when it reaches 10000.
What methods can be used to increment the letter each time the sequence hits the next block of 10000?
UPDATE AND WARNING
Having a primary key and a business key used for display, invoicing is very common. The business key has to be stored and indexed because business users will use it to search for records, documents etc. You shouldn't use the business key as the primary key though.
ORIGINAL
You already get the first digit with #n/10000. Add that to 65 to get the first letter.
To get the remainder you can perform a modulo operation, #n/10000 and format the result as a string:
select char(65 + #n/10000) + format(#n % 10000 ,'d')
Sequences and FORMAT were both introduced in SQL Server 2012, so you can be assured that FORMAT is always available.
9999 will return A9999, 19999 will return B9999 etc.
The scale can be a parameter itself
select char(65 + #n/#scale) + format(#n % #scale ,'d')
Personally I would handle this either in your display code or add it as a computed field either ti the table or view.
This would work upto Z:
declare #n int = 9999
-- Gives A9999
SELECT CHAR(#n / 10000 + 65 ) + right('0000' + convert(varchar(10), #n), 4)
SET #n = 10000
-- Gives B0000
SELECT CHAR(#n / 10000 + 65 ) + right('0000' + convert(varchar(10), #n), 4)
SET #n = 10001
-- Gives B0001
SELECT CHAR(#n / 10000 + 65 ) + right('0000' + convert(varchar(10), #n), 4)
SET #n = 20001
-- Gives C0001
SELECT CHAR(#n / 10000 + 65 ) + right('0000' + convert(varchar(10), #n), 4)
SET #n = 200001
-- Gives U0001
SELECT CHAR(#n / 10000 + 65 ) + right('0000' + convert(varchar(10), #n), 4)
SET #n = 300001
-- Gives _0001
SELECT CHAR(#n / 10000 + 65 ) + right('0000' + convert(varchar(10), #n), 4)
Something like this?
DECLARE #n INT = 9999;
WHILE #n < 26000
BEGIN
SELECT CHAR(65 + CONVERT(INT, #n / 10000)) + RIGHT('0000' + CONVERT(VARCHAR(10), #n), 4);
SELECT #n = #n + 1;
END;
(edited)
You should not use this as primary key, but rather calculate your format for the output on-the-fly. For a faster search I'd reccomend to use the following to calculate a persistant computed column, which you can use with an index.
DECLARE #mockingTbl TABLE(SomeSeqValue INT);
INSERT INTO #mockingTbl VALUES(0),(1),(999),(1000),(9999),(10000),(12345),(50000);
SELECT A.NumeralPart
,B.Rest
,C.StartLetter
,C.StartLetter+REPLACE(STR(A.NumeralPart,4),' ','0') AS YourCode
FROM #mockingTbl AS m
CROSS APPLY(SELECT m.SomeSeqValue % 10000 AS NumeralPart) AS A
CROSS APPLY(SELECT (m.SomeSeqValue-A.NumeralPart)/1000 AS Rest) AS B
CROSS APPLY(SELECT CHAR(B.Rest + ASCII('A'))) AS C(StartLetter)
Related
In my Microsoft SQL Server database, every date or datetime is represented as a decimal(17,0) value.
For example: 20210722054500000 would translate to 2021-07-22 05:45:00:000
I already came up with an solution to convert this:
SELECT
CONVERT(datetime, (SUBSTRING(CAST(20210722054500000 AS varchar(20)), 1, 4) + '-' +
SUBSTRING(CAST(20210722054500000 AS varchar(20)), 5, 2) + '-' +
SUBSTRING(CAST(20210722054500000 AS varchar(20)), 7, 2) + ' ' +
SUBSTRING(CAST(20210722054500000 AS varchar(20)), 9, 2) + ':' +
SUBSTRING(CAST(20210722054500000 AS varchar(20)), 11, 2) + ':00'), 120)
It works, but I feel there should be an better solution to convert this.
So my question is has anybody an approach that requires less code or would be better in regards to performance?
You can do it without casts and string functions, using datetimefromparts:
declare #input decimal(17,0) = 20210722054500000
select DATETIMEFROMPARTS(
#input/10000000000000 ,-- year
#input/100000000000 % 100 ,-- month
#input/1000000000 % 100 ,-- day
#input/10000000 % 100 ,-- hour
#input/100000%100 ,-- minute
#input/1000%100 ,-- seconds
#input%1000 -- milliseconds
)
One way is to extract the parts using a simple set of substrings. Given this table and data:
CREATE TABLE dbo.WhoDesignedThis
(
Id int,
TheDate decimal(17,0) -- this was hard to write with a straight face
);
INSERT dbo.WhoDesignedThis(Id, TheDate) VALUES
(1, 20210722054500000),
(2, 19991231132247699);
This query:
;WITH x AS
(
SELECT Id, TheDate, x = CONVERT(char(17), TheDate)
FROM dbo.WhoDesignedThis
)
SELECT Id, TheDate, output = DATETIMEFROMPARTS
(
LEFT(x,4),
SUBSTRING(x,5,2),
SUBSTRING(x,7,2),
SUBSTRING(x,9,2),
SUBSTRING(x,11,2),
SUBSTRING(x,13,2),
RIGHT(x,3)
)
FROM x;
Produces these results:
Id
TheDate
output
1
20210722054500000
2021-07-22 05:45:00.000
2
19991231132247699
1999-12-31 13:22:47.700
Example db<>fiddle
From the source database, I am getting HH:MM:SS as 832:24:12
Currently I am using below statement which is working fine for most of the cases hh:mm:ss but it fails when hours are more than 99
ISNULL(LEFT(COLUMN,2) * 3600 + RIGHT(LEFT(COLUMN,5),2) * 60 + RIGHT(COLUMN, 2) ,0)
Just another option with a small tweak to your original
Example
Declare #V varchar(50) = '832:24:12'
Select (left(#V,charindex(':',#V)-1)*3600) + (left(right(#V,5),2)*60) + right(#v,2)
Returns
2996652
You can use a tricky solution using PARSENAME() function.
DECALRE #Hours INT = 0, #Minutes INT = 0 , #Seconds INT = 0
SELECT #Hours = PARSENAME(REPLACE('832:24:12'+':00', ':', '.'),4),
#Minutes = PARSENAME(REPLACE('832:24:12'+':00', ':', '.'),3),
#Seconds = PARSENAME(REPLACE('832:24:12'+':00', ':', '.'),2)
SELECT #Hours * 3600 + #Minutes * 60 + #Seconds as TotalSeconds
I am replacing ':' with '.' character after appending dummy sequence of characters ':00' for PARSENAME() function to work by splitting into delimitted data.
For table query
SELECT PARSENAME(REPLACE(ISNULL(ColumnName + ':00',0), ':', '.'),4) * 3600 +
PARSENAME(REPLACE(ISNULL(ColumnName + ':00',0), ':', '.'),3) * 60 +
PARSENAME(REPLACE(ISNULL(ColumnName + ':00',0), ':', '.'),2) As TotalSecs
FROM TableName
This of a guess, however...
CREATE TABLE #Test (TimeString varchar(10))
INSERT INTO #Test
VALUES ('832:24:12')
SELECT TimeString,
(LEFT(TimeString, H.CI - 1) * 3600) + (SUBSTRING(TimeString,H.CI +1, M.CI - H.CI -1) * 60) + (RIGHT(TimeString, LEN(TimeString) - M.CI))
FROM #Test T
CROSS APPLY (VALUES(CHARINDEX(':',TimeString))) H(CI)
CROSS APPLY (VALUES(CHARINDEX(':',TimeString, H.CI+1))) M(CI);
DROP TABLE #Test;
Hours can be the leftwards chars minus 6 positions to take into account the positions for minutes and seconds in the string (:##:##).
The minutes can accessed by taking the left 2, of the rightmost 5 chars.
The seconds are the right 2 chars.
Ex:
DECLARE #tempval varchar(100) = '832:24:12'
SELECT LEFT(#tempval, LEN(#tempval) - 6) * 3600
+LEFT(RIGHT(#tempval, 5), 2) * 60
+RIGHT(#tempval, 2)
Returns
2996652
This question already has answers here:
TSQL Generate 5 character length string, all digits [0-9] that doesn't already exist in database
(6 answers)
Closed 7 years ago.
i want to create a coupon code generator by using SQL database but i don't know how to generate 1000 of random number without repeating them. so can someone help me it's important. thanks
Records in a relational database tables are unordered by nature.
therefor, you can simply create a table that has all the values between #First and #Last (0 and 9999 in your case), and then use a random order by when selecting from that table. you can also use a simple int in the database table and just format it when you select the data from the table.
Since my main database is Sql server, and I have no experience with sqlite, I will use Sql Server syntax in my code example, and leave it up to you to find the sqllite equivalent.
First, create the table:
CREATE TABLE Tbl
(
IntValue int PRIMARY KEY,
IsUsed bit NOT NULL DEFAULT 0
)
Then, populate it with numbers between 0 and 9999:
;With CTE AS (
SELECT 0 As IntValue
UNION ALL
SELECT IntValue + 1
FROM CTE
WHERE IntValue + 1 < 10000
)
INSERT INTO Tbl (IntValue)
SELECT IntValue
FROM CTE
OPTION(MAXRECURSION 0)
Then, you want to select multiple values each time, so I would write a stored procedure like this:
CREATE PROCEDURE stp_GetCouponCodes
(
#Number int = 5 -- or whatever number is default
)
AS
BEGIN
DECLARE #UsedValues AS TABLE
(
IntValue int
)
BEGIN TRY
BEGIN TRANSACTION
INSERT INTO #UsedValues
SELECT TOP(#Number) IntValue
FROM Tbl
WHERE IsUsed = 0
ORDER BY NEWID()
UPDATE Tbl
SET IsUsed = 1
FROM Tbl
INNER JOIN
#UsedValues uv ON(Tbl.IntValue = uv.IntValue)
SELECT RIGHT('00000' + CAST(IntValue as varchar), 5)
FROM #UsedValues
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
END
Then, when ever you want to generate coupons, simply execute the stored procedure with the number of coupons you want:
EXEC stp_GetCouponCodes 10;
See working fiddle example here.
The code below uses a quick method to generate 100 random 5-character strings based on the alphabet provided. You'll still need to perform duplicate checking, but this should get you started.
DECLARE #Quantity INT = 1000
DECLARE #Alphabet VARCHAR(100) = '0123456789'
DECLARE #Length INT = LEN(#Alphabet)
DECLARE #Top INT = SQRT(#Quantity) + 1
;WITH CTE AS (
SELECT TOP (#Top) *
FROM sys.objects
)
SELECT TOP (#Quantity)
SUBSTRING(#Alphabet, ABS(CHECKSUM(NEWID())) % #Length + 1, 1)
+ SUBSTRING(#Alphabet, ABS(CHECKSUM(NEWID())) % #Length + 1, 1)
+ SUBSTRING(#Alphabet, ABS(CHECKSUM(NEWID())) % #Length + 1, 1)
+ SUBSTRING(#Alphabet, ABS(CHECKSUM(NEWID())) % #Length + 1, 1)
+ SUBSTRING(#Alphabet, ABS(CHECKSUM(NEWID())) % #Length + 1, 1)
AS [Code]
FROM CTE X
CROSS JOIN CTE Y
I will be creating a sequential Serial Number made from Hexadecimal values
With this Format:
XX-XX-XX-YYYY
Which XX-XX-XX is default value
And YYYY is the incrementing hexa decimal value
Now to create the serial number based on hex value I need Add 6 to the last generated hex value
MIN: 2D41 + 6 = 2D47
2D47 + 6 ... and so on
MAX: 4100 generation of serial will stop when I meet the MAX value.
I already created it in c# but I need to do it on SQL
int num1 = int.Parse("2D41", NumberStyles.HexNumber); //Convert hex to int
int result = num1 + 6; //Add + 6 for increment
string myHex = result.ToString("X"); //Convert result to hex
MessageBox.Show(myHex); // result 2D47
How can this be done in T-SQL?
DECLARE #x VARBINARY(8) = 0x00002D41;
SELECT CONVERT(VARBINARY(8), CONVERT(INT, #x) + 6);
In order to handle the output as a string:
DECLARE #x VARBINARY(8) = 0x00002D41;
SELECT CONVERT(CHAR(10), CONVERT(VARBINARY(8), CONVERT(INT, #x) + 6), 1);
Hope this helps you
declare #seed varchar(max) = '2D41';
declare #limit varchar(max) = '4100';
select convert(int, convert(varbinary(max), '0x'+#seed,1)),
convert(int, convert(varbinary(max), '0x'+#limit,1));
;with seedlimit(seed, limit) as (
select convert(int, convert(varbinary(max), '0x'+#seed,1)),
convert(int, convert(varbinary(max), '0x'+#limit,1))
)
select SerialNumber = 'XX-XX-XX-' + right(convert(varchar(10),cast(s.seed + 6 * v.number as varbinary(max)),1),4)
from seedlimit s
join master.dbo.spt_values v on type='p'
where s.seed + 6 * v.number <= s.limit;
The basic ingredients are in there for you to create a view/procedure/function out of the answer,
Output:
SerialNumber
-------------
XX-XX-XX-2D41
XX-XX-XX-2D47
...
XX-XX-XX-40F7
XX-XX-XX-40FD
If you already have it in C#, leave it there and simply convert your code to a SQL CLR function.
For a simple example see:
http://blog.sqlauthority.com/2008/10/19/sql-server-introduction-to-clr-simple-example-of-clr-stored-procedure/
I have 2 tables:
tZipCodeNoCity with ZipCode and PointGeography
and MBLPosition with Latitude and Longitude
In this query I'm finding closest ZipCode to my positions. It's "poor mans" geocoding :)
How do I write this query so I don't have to do this SELECT TOP 1 inline?
It's pretty slow with even 150 devices (like 20 seconds)
I had to include 150 mile radius into this subselect to get it faster
SELECT LastPositions.DeviceId, P.Description, P.Latitude, P.Longitude, P.Speed, P.DeviceTime,
(
SELECT TOP 1 ZC.ZipCode
FROM dbo.tZipCodeNoCity ZC
WHERE ZC.PointGeography.STDistance(geography::STPointFromText('POINT(' + CAST(P.Longitude AS VARCHAR(20)) + ' ' + CAST(P.Latitude AS VARCHAR(20)) + ')', 4326)) < 150 * 1609.344
ORDER BY ZC.PointGeography.STDistance(geography::STPointFromText('POINT(' + CAST(P.Longitude AS VARCHAR(20)) + ' ' + CAST(P.Latitude AS VARCHAR(20)) + ')', 4326))
)
FROM dbo.MBLPosition P
INNER JOIN
(
SELECT D.DeviceId, MAX(P.PositionKey) LastPositionKey
FROM dbo.MBLPosition P
INNER JOIN IDATTApplication.dbo.MBLDevice D ON P.DeviceKey = D.DeviceKey
GROUP BY D.DeviceId
) LastPositions ON P.PositionKey = LastPositions.LastPositionKey
In a project I worked on about 12 years ago, I ran a query along these lines to reduce the list of possibilities before doing the actual distance calculation:
WHERE zip.lat < my.lat + 0.5 && zip.lat > my.lat - 0.5
&& zip.long < my.long + 0.5 && zip.long > my.long - 0.5
From that subset, I calculate the actual distance between the two points and sort on it. You'll have to adjust the "0.5" portion as appropriate to get a big enough box to be sure you're going to get a hit.
And I would imagine that there's a better way than STPointFromText to create your point object. Could you use STPointFromWKB? Could you convert to the geography type once?
See this page for an example of creating your point via SET.
DECLARE #p geography;
SET #p = geography::STGeomFromText('POINT(' + CAST(P.Longitude AS VARCHAR(20)) + ' ' + CAST(P.Latitude AS VARCHAR(20)) + ')', 4326);
SELECT TOP 1 ZC.ZipCode
FROM dbo.tZipCodeNoCity ZC
WHERE ZC.PointGeography.STDistance(#p)) < 150 * 1609.344
ORDER BY ZC.PointGeography.STDistance(#p))