2

I've been trying to get this right for some time now with no use.

I have a table in mssql database and I want to insert new row using stored procedure

CREATE TABLE "Customers" (
"CustomerID" NCHAR(5) NOT NULL,
"CompanyName" NVARCHAR(40) NOT NULL,
"ContactName" NVARCHAR(30) NULL,
"ContactTitle" NVARCHAR(30) NULL,
"Address" NVARCHAR(60) NULL,
"City" NVARCHAR(15) NULL,
"Region" NVARCHAR(15) NULL,
"PostalCode" NVARCHAR(10) NULL,
"Country" NVARCHAR(15) NULL,
"Phone" NVARCHAR(24) NULL,
"Fax" NVARCHAR(24) NULL,
PRIMARY KEY ("CustomerID")
);

The problem is CustomerID field which contains unique string for each record (ALFKI, BERGS, BERGS, etc.)

I want to make a stored procedure which will insert a row with new data and create an unique CustomerID. Build in functions are out of a question as I need the string to be 5 chars long.

I have a procedure which generates 5 chars ID as follows

begin

declare @chars char(26) = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
declare @i int = 0
declare @id varchar(max) = ''

while @i < 5
begin
        set @id = @id + substring(@chars, cast(ceiling(rand() * 26) as int), 1)

    set @i = @i + 1
end

Select (cast(@id as nvarchar(400)))

end

And the one that I tried to make work with no use. It is supposed to select an unique id (set @id = 'ANATR' is there on purpose to make it go into the loop

begin
declare @randID varchar(5) = ''
declare @selectID varchar(20) = ''
declare @chars char(26) = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
declare @i int = 0
declare @id varchar(10) = ''

while @i < 5
begin
   set @id = @id + substring(@chars, cast(ceiling(rand() * 26) as int), 1)           
    set @i = @i + 1
end
select @id
set @id = 'ANATR'

SET @selectID = (SELECT CustomerID FROM CUSTOMERS WHERE CustomerID = @id)


while @selectID <> 'NULL'
begin
    set @id = ''
    while @i < 5
        begin
            set @id = @id + substring(@chars, cast(ceiling(rand() * 26) as int), 1)           
            set @i = @i + 1
        end

    SET @selectID = (SELECT CustomerID FROM CUSTOMERS WHERE CustomerID = @id)   

    SELECT @id
end


end

Here is the insert procedure I have at the moment

CREATE PROCEDURE [dbo].[InsertCustomers]

(

@CustomerID nchar(5),

@CompanyName nvarchar(40),

@ContactName nvarchar(30) = NULL,

@ContactTitle nvarchar(30) = NULL,

@Address nvarchar(60) = NULL,

@City nvarchar(15) = NULL,

@Region nvarchar(15) = NULL,

@PostalCode nvarchar(10) = NULL,

@Country nvarchar(15) = NULL,

@Phone nvarchar(24) = NULL,

@Fax nvarchar(24) = NULL

)

AS

SET NOCOUNT OFF;

 INSERT INTO [dbo].[Customers] ([CustomerID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax]) VALUES (@CustomerID, @CompanyName, @ContactName, @ContactTitle, @Address, @City, @Region, @PostalCode, @Country, @Phone, @Fax);
4

3 回答 3

8

这里的主要问题是,从生成的字符串中检测冲突并重试的增量成本会随着您生成越来越多的字符串而增加(因为您必须阅读所有这些字符串以确保您没有生成重复) . 同时,重复命中的几率会上升,这意味着桌子越大,这个过程就会越慢。

为什么需要在运行时生成唯一字符串?提前构建它们。这篇文章这篇文章都是关于随机数的,但基本概念是一样的。您建立一组独特的字符串,并在需要时从堆栈中拉出一个。在应用程序的整个生命周期内,您的冲突几率始终保持在 0%(前提是您构建了足够多的唯一值)。在您自己的设置中预先支付碰撞成本,而不是随着时间的推移逐渐增加(并且以用户等待这些尝试最终产生唯一编号为代价)。

这将生成 100,000 个唯一的 5 个字符的字符串,并且一次性花费大约 1 秒(在我的机器上):

;WITH 
 a(a) AS 
 (
   SELECT TOP (26) number + 65 FROM master..spt_values 
   WHERE type = N'P' ORDER BY number
 ),
 b(a) AS 
 (
   SELECT TOP (10) a FROM a ORDER BY NEWID()
 )
SELECT DISTINCT CHAR(b.a) + CHAR(c.a) + CHAR(d.a) + CHAR(e.a) + CHAR(f.a)
FROM b, b AS c, b AS d, b AS e, b AS f;

这还不够吗?TOP (10)通过更改为 ,您可以生成大约 112 万个唯一值TOP (20)。这花了 18 秒。还是不够?TOP (24)将在大约 2 分钟内给你不到 800 万。随着您生成更多字符串,它的成本将成倍增加,因为每次添加客户时DISTINCT都必须进行相同的重复检查。

所以,创建一个表:

CREATE TABLE dbo.StringStack
(
  ID INT IDENTITY(1,1) PRIMARY KEY,
  String CHAR(5) NOT NULL UNIQUE
);

插入该集合:

;WITH 
 a(a) AS 
 (
   SELECT TOP (26) number + 65 FROM master..spt_values 
   WHERE type = N'P' ORDER BY number
 ),
 b(a) AS 
 (
   SELECT TOP (10) a FROM a ORDER BY NEWID()
 )
INSERT dbo.StringStack(String)
SELECT DISTINCT CHAR(b.a) + CHAR(c.a) + CHAR(d.a) + CHAR(e.a) + CHAR(f.a)
FROM b, b AS c, b AS d, b AS e, b AS f;

然后只需创建一个在需要时从堆栈中弹出一个的过程:

CREATE PROCEDURE dbo.AddCustomer
  @CustomerName VARCHAR(64) /* , other params */
AS
BEGIN
  SET NOCOUNT ON;
  
  DELETE TOP (1) dbo.StringStack
    OUTPUT deleted.String, @CustomerName /* , other params */
    INTO dbo.Customers(CustomerID, CustomerName /*, ...other columns... */);
END
GO

没有愚蠢的循环,不需要检查CustomerID你生成的是否存在,等等。你想要构建的唯一额外的东西是某种类型的检查,当你变低时通知你。

顺便说一句,这些是 CustomerID 的可怕标识符。顺序代理键(例如 IDENTITY 列)有什么问题?包含所有这些努力的 5 位随机字符串如何比系统可以更轻松地为您生成的唯一数字更好?

于 2013-12-13T01:51:44.160 回答
3

Muhammed Ali 的答案有效,但会证明资源相当密集(特别是当没有太多可用的 5 个字母组合时):您的函数使用随机生成器,并且需要一段时间才能找到适合的组合'未使用,特别是因为它对以前结果的记忆非常有限。这意味着它会尝试,并且可能会给你一些类似的东西(有点夸张):第一次是 BAGER,第二次是 ANSWE,第三次又是 BAGER。您会发现生成器一遍又一遍地为您提供相同的答案(尤其是超过 12M 种可能的组合)会浪费大量时间。

如果您正在寻找一个固定长度的 ID(因为您使用 NCHAR(5),我想这是一个很好的假设),我宁愿研究构建一个包含所有可能组合的表,并每次选择该表的一个值您必须有一个。一旦它被使用,您将删除它,或者将其标记为已使用(出于可重用性的原因,我更愿意这样做)。

这导致了我的最终评论(我不能将其作为评论,因为我没有足够的声誉):为什么不使用 MS-SQL 提供的 IDENTITY 函数?这提供了更好地处理主键生成...

于 2013-12-13T01:53:04.033 回答
-3

我相信你可以做这样的事情来确保你们都得到一个唯一的 id

begin

declare @chars char(26) = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
declare @i int = 0
declare @id varchar(max) = ''


while (1=1)
begin
        set @id = @id + substring(@chars, cast(ceiling(rand() * 26) as int), 1)

    set @i = @i + 1

   IF (NOT EXISTS(SELECT * FROM Customers WHERE CustomerID = @id) AND LEN(@id) = 5)
      BREAK
   ELSE
      CONTINUE
end

Select (cast(@id as nvarchar(400)))

end

将 while 条件设置为始终为真,并且仅当您的两个要求都为 TRUE 时才退出 while 循环,即Length of new ID is 5does not exist in the customers table already

于 2013-12-13T01:05:05.537 回答