【问题标题】:Guarantee random inserting保证随机插入
【发布时间】:2018-11-01 13:28:47
【问题描述】:

我正在尝试预生成一些字母数字字符串并将结果插入表中。字符串的长度为 5。例如:a5r67。基本上我想为客户生成一些可读的字符串,这样他们就可以访问他们的订单,比如 www.example.com/order/a5r67。现在我有一个选择语句:

;WITH 
    cte1 AS(SELECT * FROM (VALUES('0'),('1'),('2'),('3'),('4'),('5'),('6'),('7'),('8'),('9'),('a'),('b'),('c'),('d'),('e'),('f'),('g'),('h'),('i'),('j'),('k'),('l'),('m'),('n'),('o'),('p'),('q'),('r'),('s'),('t'),('u'),('v'),('w'),('x'),('y'),('z')) AS v(t)),
    cte2 AS(SELECT * FROM (VALUES('0'),('1'),('2'),('3'),('4'),('5'),('6'),('7'),('8'),('9'),('a'),('b'),('c'),('d'),('e'),('f'),('g'),('h'),('i'),('j'),('k'),('l'),('m'),('n'),('o'),('p'),('q'),('r'),('s'),('t'),('u'),('v'),('w'),('x'),('y'),('z')) AS v(t)),
    cte3 AS(SELECT * FROM (VALUES('0'),('1'),('2'),('3'),('4'),('5'),('6'),('7'),('8'),('9'),('a'),('b'),('c'),('d'),('e'),('f'),('g'),('h'),('i'),('j'),('k'),('l'),('m'),('n'),('o'),('p'),('q'),('r'),('s'),('t'),('u'),('v'),('w'),('x'),('y'),('z')) AS v(t)),
    cte4 AS(SELECT * FROM (VALUES('0'),('1'),('2'),('3'),('4'),('5'),('6'),('7'),('8'),('9'),('a'),('b'),('c'),('d'),('e'),('f'),('g'),('h'),('i'),('j'),('k'),('l'),('m'),('n'),('o'),('p'),('q'),('r'),('s'),('t'),('u'),('v'),('w'),('x'),('y'),('z')) AS v(t)),
    cte5 AS(SELECT * FROM (VALUES('0'),('1'),('2'),('3'),('4'),('5'),('6'),('7'),('8'),('9'),('a'),('b'),('c'),('d'),('e'),('f'),('g'),('h'),('i'),('j'),('k'),('l'),('m'),('n'),('o'),('p'),('q'),('r'),('s'),('t'),('u'),('v'),('w'),('x'),('y'),('z')) AS v(t))
INSERT INTO ProductHandles(ID, Used)
SELECT cte1.t + cte2.t + cte3.t + cte4.t + cte5.t, 0
FROM cte1
CROSS JOIN cte2
CROSS JOIN cte3
CROSS JOIN cte4
CROSS JOIN cte5

现在的问题是我需要写这样的东西来从表中获取一个值:

SELECT TOP 1 ID 
FROM ProductHandles
WHERE Used = 0

我将在Used 列上建立索引,这样它会很快。问题在于它带有顺序:

00000
00001
00002
...

我知道我可以通过NEWID() 订购,但这会慢得多。我知道除非我们指定Order By 子句,否则无法保证订购。需要的是相反的。我需要保证混乱,但不是每次客户创建订单时都通过NEWID() 订购。

我打算这样使用它:

WITH cte as (
                SELECT TOP 1 * FROM ProductHandles WHERE Used = 0
                --I don't want to order by newid() here as it will be slow
            )
UPDATE cte 
SET Used = 1
OUTPUT INSERTED.ID

【问题讨论】:

  • 如果你想要混乱,为什么你关心他们得到哪个 ID,只要它不被使用?您似乎正在尝试创建简化的 GUID
  • 这是一个要求。这不是我的意愿。是的,比如readable guid
  • 嗯,你可以通过 id 的子字符串随机排序...不确定它是否会表现良好,但它会是随机的 DECLARE @sort int = (SELECT ROUND(((6 - 1 -1) * RAND() + 1), 0)) ORDER BY substring(ID,@sort,5)
  • 这是为了满足“不为订单发出顺序/容易猜到的标识符”类型的要求吗?
  • 我认为没有一种方法可以在不使用该顺序的情况下从表中拉出随机行。也许相反,您可以使插入本身更加随机并获得按 ID 排序的最高值?

标签: sql sql-server


【解决方案1】:

如果您向表中添加一个标识列,并在插入记录时使用order by newid()(这会很慢,但据我了解,这是一次脱机完成的事情),那么您可以使用order by on identity 列以选择记录按照它们插入表的顺序

来自 Microsoft Docs 中的Limitations and Restrictions part of the INSERT page

使用带有 ORDER BY 的 SELECT 来填充行的 INSERT 查询保证了标识值的计算方式,但不保证插入行的顺序。

这意味着通过这样做,您实际上使identity 列按照与insert...select 语句中选择的行相同的随机顺序排序。

此外,无需重复相同的 cte 5 次 - 您已经在重复交叉应用:

CREATE TABLE ProductHandles(sort int identity(1,1), ID char(5), used bit)


;WITH 
    cte AS(SELECT * FROM (VALUES('0'),('1'),('2'),('3'),('4'),('5'),('6'),('7'),('8'),('9'),('a'),('b'),('c'),('d'),('e'),('f'),('g'),('h'),('i'),('j'),('k'),('l'),('m'),('n'),('o'),('p'),('q'),('r'),('s'),('t'),('u'),('v'),('w'),('x'),('y'),('z')) AS v(t))        
INSERT INTO ProductHandles(ID, Used)
SELECT a.t + b.t + c.t + d.t + e.t, 0
FROM cte a
CROSS JOIN cte b
CROSS JOIN cte c
CROSS JOIN cte d
CROSS JOIN cte e
ORDER BY NEWID()

然后 cte 可以有一个 order by 子句,以保证与填充此表的 select 语句返回的行相同的随机顺序:

WITH cte as (
                SELECT TOP 1 * 
                FROM ProductHandles 
                WHERE Used = 0
                ORDER BY sort 
            )
UPDATE cte 
SET Used = 1
OUTPUT INSERTED.ID

You can see a live demo on rextester.(只有数字,否则耗时太长)

【讨论】:

  • @ZoharPeled 。 . .我错过了。您正在使用标识列。荣誉。
  • 顺便说一句,有更好的(嗯,至少从性能的角度来看)填充随机字符串池的方法 - 阅读我的博客文章了解详细信息:zoharpeled.wordpress.com/2019/09/15/…
【解决方案2】:

这里有一个稍微不同的选项... 与其尝试一次生成所有可能的值,不如一次生成一百万或两个,并在它们用完时生成更多。 使用这种方法,您可以大大减少初始创建时间,并消除维护大量值表的需要,其中大部分值永远不会被使用。

CREATE TABLE dbo.ProductHandles (
    rid INT NOT NULL
        CONSTRAINT pk_ProductHandles 
        PRIMARY KEY CLUSTERED,
    ID_Value CHAR(5) NOT NULL
        CONSTRAINT uq_ProductHandles_IDValue 
        UNIQUE WITH (IGNORE_DUP_KEY = ON),      -- prevents the insertion of duplicate values w/o generating any errors.
    Used BIT NOT NULL
        CONSTRAINT df_ProductHandles_Used 
        DEFAULT (0)
    );

-- Create a filtered index to help facilitate fast searches
-- of unused values.
CREATE NONCLUSTERED INDEX ixf_ProductHandles_Used_rid    
    ON dbo.ProductHandles (Used, rid)
    INCLUDE(ID_Value)
WHERE Used = 0;

--==========================================================

WHILE 1 = 1     -- The while loop will attempt to insert new rows, in 1M blocks, until required minimum of unused values are available.
BEGIN 
    IF (SELECT COUNT(*) FROM dbo.ProductHandles ph WHERE ph.Used = 0) > 1000000     -- the minimum num of unused ID's you want to keep on hand.
    BEGIN
        BREAK;
    END;
    ELSE 
    BEGIN
        WITH 
            cte_n1 (n) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (n)), 
            cte_n2 (n) AS (SELECT 1 FROM cte_n1 a CROSS JOIN cte_n1 b),
            cte_n3 (n) AS (SELECT 1 FROM cte_n2 a CROSS JOIN cte_n2 b),
            cte_Tally (n) AS (
                SELECT TOP (1000000)    -- Sets the "block size" of each insert attempt.
                    ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
                FROM
                    cte_n3 a CROSS JOIN cte_n3 b
                )
        INSERT dbo.ProductHandles (rid, ID_Value, Used)
        SELECT 
            t.n + ISNULL((SELECT MAX(ph.rid) FROM dbo.ProductHandles ph), 0),
            CONCAT(ISNULL(c1.char_1, n1.num_1), ISNULL(c2.char_2, n2.num_2), ISNULL(c3.char_3, n3.num_3), ISNULL(c4.char_4, n4.num_4), ISNULL(c5.char_5, n5.num_5)),
            0
        FROM
            cte_Tally t
            -- for each of the 5 positions, randomly generate numbers between 0 & 36. 
            -- 0-9 are left as numbers. 
            -- 10 - 36 are converted to lower cased letters.
            CROSS APPLY ( VALUES (ABS(CHECKSUM(NEWID())) % 36) ) n1 (num_1)
            CROSS APPLY ( VALUES (CHAR(CASE WHEN n1.num_1 > 9 THEN n1.num_1 + 87 END)) ) c1 (char_1)
            CROSS APPLY ( VALUES (ABS(CHECKSUM(NEWID())) % 36) ) n2 (num_2)
            CROSS APPLY ( VALUES (CHAR(CASE WHEN n2.num_2 > 9 THEN n2.num_2 + 87 END)) ) c2 (char_2)
            CROSS APPLY ( VALUES (ABS(CHECKSUM(NEWID())) % 36) ) n3 (num_3)
            CROSS APPLY ( VALUES (CHAR(CASE WHEN n3.num_3 > 9 THEN n3.num_3 + 87 END)) ) c3 (char_3)
            CROSS APPLY ( VALUES (ABS(CHECKSUM(NEWID())) % 36) ) n4 (num_4)
            CROSS APPLY ( VALUES (CHAR(CASE WHEN n4.num_4 > 9 THEN n4.num_4 + 87 END)) ) c4 (char_4)
            CROSS APPLY ( VALUES (ABS(CHECKSUM(NEWID())) % 36) ) n5 (num_5)
            CROSS APPLY ( VALUES (CHAR(CASE WHEN n5.num_5 > 9 THEN n5.num_5 + 87 END)) ) c5 (char_5);
    END;
END;

在初始创建后,将 WHILE 循环中的代码移动到存储过程中,并安排它定期自动运行。

【讨论】:

  • Jason,你确定这不会产生重复吗?
  • Dupes 将被生成,但表上的 UNIQUE 约束将拒绝它们。如果运行代码,您会看到如下所示的消息:“重复键被忽略。(991691 行受影响)重复键被忽略。(975541 行受影响)”
【解决方案3】:

如果我理解这一点,看起来您试图将 URL/可见数据与数据库记录 ID 分开,就像大多数应用程序使用的那样,并提供与用户将使用的 ID 字段不直接相关的内容看。 NEWID() 确实允许控制字符数,因此您可以生成具有较小索引的较小字段。或者只使用完整 NEWID() 的一部分

SELECT CONVERT(varchar(255), NEWID())
SELECT SUBSTRING(CONVERT(varchar(40), NEWID()),0,5)

您可能还想查看checksum 字段,但我不知道它在索引方面是否更快。将随机 NEWID() 与跨 2 或 3 个字段的校验和结合起来可能会更疯狂。

SELECT BINARY_CHECKSUM(5 ,'EP30461105',1)

【讨论】:

  • 其中一个要求是唯一性。我认为这会很危险。
猜你喜欢
  • 1970-01-01
  • 2011-11-01
  • 2022-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-07-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多