如何在Postgres 9.6+中生成长度为N的随机,唯一,字母数字ID?

时间:2017-02-01 01:28:06

标签: sql database postgresql random

我在StackOverflow上看到了一堆different solutions多年来和许多Postgres版本,但有一些较新的功能,如gen_random_bytes我想再问一下,看看是否有在较新版本中是一个更简单的解决方案。

鉴于包含a-zA-Z0-9的ID,并根据他们使用的位置而变化,例如......

bTFTxFDPPq
tcgHAdW3BD
IIo11r9J0D
FUW5I8iCiS

uXolWvg49Co5EfCo
LOscuAZu37yV84Sa
YyrbwLTRDb01TmyE
HoQk3a6atGWRMCSA

HwHSZgGRStDMwnNXHk3FmLDEbWAHE1Q9
qgpDcrNSMg87ngwcXTaZ9iImoUmXhSAv
RVZjqdKvtoafLi1O5HlvlpJoKzGeKJYS
3Rls4DjWxJaLfIJyXIEpcjWuh51aHHtK

(与IDs that Stripe uses一样。)

如何在Postgres 9.6 +中为随机安全地生成它们(尽可能减少冲突并降低可预测性),并为不同的用例指定不同的长度,

我认为理想情况下解决方案的签名类似于:

generate_uid(size integer) returns text

size可根据您自己的权衡来定制,以降低碰撞机会与减少字符串大小以实现可用性。

据我所知,它必须使用gen_random_bytes()代替random()才能获得真正的随机性,以减少猜测它们的可能性。

谢谢!

我知道UUID有gen_random_uuid(),但我不想在这种情况下使用它们。我正在寻找能够为我提供类似于Stripe(或其他人)使用的ID的内容,它们看起来像:"id": "ch_19iRv22eZvKYlo2CAxkjuHxZ"尽可能短,同时仍然只包含字母数字字符。

这个要求也是encode(gen_random_bytes(), 'hex')在这种情况下不正确的原因,因为它减少了字符集,因此迫使我增加字符串的长度以避免冲突。

我目前正在应用层中执行此操作,但我希望将其移至数据库层以减少相互依赖性。以下是在应用程序层中执行此操作的Node.js代码可能如下所示:

var crypto = require('crypto');
var set = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

function generate(length) {
  var bytes = crypto.randomBytes(length);
  var chars = [];

  for (var i = 0; i < bytes.length; i++) {
    chars.push(set[bytes[i] % set.length]);
  }

  return chars.join('');
}

6 个答案:

答案 0 :(得分:3)

评分,

  1. [a-z]
  2. 中的26个字符
  3. [A-Z]
  4. 中的26个字符
  5. [0-9]
  6. 中的10个字符
  7. [a-zA-Z0-9](base62)
  8. 中的62个字符
  9. 函数substring(string [from int] [for int])看起来很有用。
  10. 所以它看起来像这样。首先,我们证明我们可以采用随机范围并从中拉出来。

    SELECT substring(
      'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
      1, -- 1 is 'a', 62 is '9'
      1,
    );
    

    现在我们需要163

    之间的范围
    SELECT trunc(random()*62+1)::int+1
    FROM generate_series(1,1e2) AS gs(x)
    

    这让我们在那里..现在我们只需加入这两个......

    SELECT substring(
      'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
      trunc(random()*62)::int+1
      1
    )
    FROM generate_series(1,1e2) AS gs(x);
    

    然后我们将它包装在ARRAY constructor (because this is fast)

    SELECT ARRAY(
      SELECT substring(
        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
        trunc(random()*62)::int+1,
        1
      )
      FROM generate_series(1,1e2) AS gs(x)
    );
    

    并且,我们致电array_to_string()获取文字。

    SELECT array_to_string(
      ARRAY(
          SELECT substring(
            'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
            trunc(random()*62)::int+1,
            1
          )
          FROM generate_series(1,1e2) AS gs(x)
      )
      , ''
    );
    

    从这里我们甚至可以把它变成一个函数..

    CREATE FUNCTION random_string(randomLength int)
    RETURNS text AS $$
    SELECT array_to_string(
      ARRAY(
          SELECT substring(
            'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
            trunc(random()*62)::int+1,
            1
          )
          FROM generate_series(1,randomLength) AS gs(x)
      )
      , ''
    )
    $$ LANGUAGE SQL
    RETURNS NULL ON NULL INPUT
    VOLATILE LEAKPROOF;
    

    然后

    SELECT * FROM random_string(10);
    

答案 1 :(得分:1)

此查询生成所需的字符串。只需更改generate_series的第二个寄生计,即可选择随机字符串的长度。

SELECT
     string_agg(c, '')
FROM (
     SELECT
          chr(r + CASE WHEN r > 25 + 9 THEN 97 - 26 - 9 WHEN r > 9 THEN 64 - 9 ELSE 48 END) AS c
     FROM (
           SELECT
                 i,
                 (random() * 60)::int AS r
           FROM
                 generate_series(0, 62) AS i
          ) AS a
      ORDER BY i
     ) AS A;

答案 2 :(得分:1)

  

我正在寻找能够提供“短代码”(类似于Youtube用于视频ID的内容)的内容,它们尽可能短,同时仍然只包含字母数字字符。

这与你刚才提出的问题有着根本不同的问题。那么你想要的是在表格上放置serial类型,并使用hashids.org code for PostgreSQL

  • 以1:1返回唯一编号(序列号)
  • 不要重复或有碰撞的可能。
  • 同时base62 [a-zA-Z0-9]

代码看起来像这样,

SELECT id, hash_encode(foo.id)
FROM foo; -- Result: jNl for 1001

SELECT hash_decode('jNl') -- returns 1001

该模块也支持盐。

答案 3 :(得分:1)

想出这个,这里有一个功能:

CREATE OR REPLACE FUNCTION generate_uid(size INT) RETURNS TEXT AS $$
DECLARE
  characters TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  bytes BYTEA := gen_random_bytes(size);
  l INT := length(characters);
  i INT := 0;
  output TEXT := '';
BEGIN
  WHILE i < size LOOP
    output := output || substr(characters, get_byte(bytes, i) % l + 1, 1);
    i := i + 1;
  END LOOP;
  RETURN output;
END;
$$ LANGUAGE plpgsql VOLATILE;

然后运行它只需:

generate_uid(10)
-- '3Rls4DjWxJ'

警告

执行此操作时,您需要确保您创建的ID的长度足以避免随着您创建的对象数量的增加而发生冲突,这可能会因为{而违反{ {3}}。 因此,对于任何合理常用的对象,您可能需要比10更长(或更大)的长度,我只使用10作为一个简单示例。

用法

定义了函数后,您可以在表定义中使用它,如下所示:

CREATE TABLE collections (
  id TEXT PRIMARY KEY DEFAULT generate_uid(10),
  name TEXT NOT NULL,
  ...
);

然后在插入数据时,如下:

INSERT INTO collections (name) VALUES ('One');
INSERT INTO collections (name) VALUES ('Two');
INSERT INTO collections (name) VALUES ('Three');
SELECT * FROM collections;

它会自动生成id值:

    id     |  name  | ...
-----------+--------+-----
owmCAx552Q | ian    |
ZIofD6l3X9 | victor |

使用前缀

或者,您可能希望在查看日志或调试器中的单个ID(类似于Birthday Paradox)时为方便起见添加前缀,如下所示:

CREATE TABLE collections (
  id TEXT PRIMARY KEY DEFAULT ('col_' || generate_uid(10)),
  name TEXT NOT NULL,
  ...
);

INSERT INTO collections (name) VALUES ('One');
INSERT INTO collections (name) VALUES ('Two');
INSERT INTO collections (name) VALUES ('Three');
SELECT * FROM collections;

      id       |  name  | ...
---------------+--------+-----
col_wABNZRD5Zk | ian    |
col_ISzGcTVj8f | victor |

答案 4 :(得分:1)

感谢Evan Carroll的回答,我看了hashids.org。 对于Postgres,您必须编译extension或运行一些TSQL functions。 但出于我的需要,我根据哈希值的想法(简短,难以猜测,唯一,自定义字母,避免使用诅咒词)创建了一些更简单的东西。

随机播放字母:

CREATE OR REPLACE FUNCTION consistent_shuffle(alphabet TEXT, salt TEXT) RETURNS TEXT AS $$
DECLARE
    SALT_LENGTH INT := length(salt);
    integer INT = 0;
    temp TEXT = '';
    j INT = 0;
    v INT := 0;
    p INT := 0;
    i INT := length(alphabet) - 1;
    output TEXT := alphabet;
BEGIN
    IF salt IS NULL OR length(LTRIM(RTRIM(salt))) = 0 THEN
        RETURN alphabet;
    END IF;
    WHILE i > 0 LOOP
        v := v % SALT_LENGTH;
        integer := ASCII(substr(salt, v + 1, 1));
        p := p + integer;
        j := (integer + v + p) % i;

        temp := substr(output, j + 1, 1);
        output := substr(output, 1, j) || substr(output, i + 1, 1) || substr(output, j + 2);
        output := substr(output, 1, i) || temp || substr(output, i + 2);

        i := i - 1;
        v := v + 1;
    END LOOP;
    RETURN output;
END;
$$ LANGUAGE plpgsql VOLATILE;

主要功能:

CREATE OR REPLACE FUNCTION generate_uid(id INT, min_length INT, salt TEXT) RETURNS TEXT AS $$
DECLARE
    clean_alphabet TEXT := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
    curse_chars TEXT := 'csfhuit';
    curse TEXT := curse_chars || UPPER(curse_chars);
    alphabet TEXT := regexp_replace(clean_alphabet, '[' || curse  || ']', '', 'gi');
    shuffle_alphabet TEXT := consistent_shuffle(alphabet, salt);
    char_length INT := length(alphabet);
    output TEXT := '';
BEGIN
    WHILE id != 0 LOOP
        output := output || substr(shuffle_alphabet, (id % char_length) + 1, 1);
        id := trunc(id / char_length);
    END LOOP;
    curse := consistent_shuffle(curse, output || salt);
    output := RPAD(output, min_length, curse);
    RETURN output;
END;
$$ LANGUAGE plpgsql VOLATILE;

使用方法示例:

-- 3: min-length
select generate_uid(123, 3, 'salt'); -- output: "0mH"

-- or as default value in a table
CREATE SEQUENCE IF NOT EXISTS my_id_serial START 1;
CREATE TABLE collections (
    id TEXT PRIMARY KEY DEFAULT generate_uid(CAST (nextval('my_id_serial') AS INTEGER), 3, 'salt')
);
insert into collections DEFAULT VALUES ;

答案 5 :(得分:0)

因此,对于类似这样的事情,我有自己的用例。我不是要提出一个最重要的问题的解决方案,但是如果您正在寻找类似我的东西,请尝试一下。

我的用例是,我需要创建一个随机的外部UUID(作为主键),并使用尽可能少的字符。值得庆幸的是,该方案并不需要大量的这些需求(可能只有数千个)。因此,一个简单的解决方案是结合使用generate_uid()和检查以确保没有使用下一个序列的方法。

这是我将其组合在一起的方式:

CREATE OR REPLACE FUNCTION generate_id (
    in length INT
,   in for_table text
,   in for_column text
,   OUT next_id TEXT
) AS
$$
DECLARE
    id_is_used BOOLEAN;
    loop_count INT := 0;
    characters TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    loop_length INT;
BEGIN

LOOP
    next_id := '';
    loop_length := 0;
    WHILE loop_length < length LOOP
    next_id := next_id || substr(characters, get_byte(gen_random_bytes(length), loop_length) % length(characters) + 1, 1);
    loop_length := loop_length + 1;
    END LOOP;

    EXECUTE format('SELECT TRUE FROM %s WHERE %s = %s LIMIT 1', for_table, for_column, quote_literal(next_id)) into id_is_used;

    EXIT WHEN id_is_used IS NULL;

    loop_count := loop_count + 1;

    IF loop_count > 100 THEN
        RAISE EXCEPTION 'Too many loops. Might be reaching the practical limit for the given length.';
    END IF;
END LOOP;


END
$$
LANGUAGE plpgsql
STABLE
;

这是表格用法的示例:

create table some_table (
    id
        TEXT
        DEFAULT generate_id(6, 'some_table', 'id')
        PRIMARY KEY
)
;

并进行测试以了解其破坏方式:

DO
$$
DECLARE
    loop_count INT := 0;

BEGIN

-- WHILE LOOP
WHILE loop_count < 1000000
LOOP

    INSERT INTO some_table VALUES (DEFAULT);
    loop_count := loop_count + 1;
END LOOP;

END
$$ LANGUAGE plpgsql
;