Gerar uma string de 8 caracteres aleatória e exclusiva usando MySQL

110

Estou trabalhando em um jogo que envolve veículos em algum momento. Eu tenho uma tabela MySQL chamada "veículos" contendo os dados sobre os veículos, incluindo a coluna "placa" que armazena as placas dos veículos.

Agora vem a parte com a qual estou tendo problemas. Preciso encontrar uma placa de licença não utilizada antes de criar um novo veículo - deve ser uma string alfanumérica aleatória de 8 caracteres. Como consegui isso, usei um loop while em Lua, que é a linguagem em que estou programando, para gerar strings e consultar o banco de dados para ver se ele é usado. No entanto, conforme o número de veículos aumenta, espero que isso se torne ainda mais ineficiente do que é agora. Portanto, decidi tentar resolver esse problema usando uma consulta MySQL.

A consulta de que preciso deve simplesmente gerar uma string alfanumérica de 8 caracteres que ainda não está na tabela. Pensei na abordagem do loop de geração e verificação novamente, mas não estou limitando esta pergunta apenas no caso de haver uma mais eficiente. Consegui gerar strings definindo uma string contendo todos os caracteres permitidos e criando substrings aleatoriamente, e nada mais.

Qualquer ajuda é apreciada.

Funstein
fonte
Quão aleatório você precisa que eles sejam? Se alguém receber uma placa específica, é importante ou não se eles podem calcular a placa anterior ou seguinte que você distribuiu?
Damien_The_Unbeliever
@YaK Veja minha resposta sobre como evitar até mesmo a mínima possibilidade de colisão
Eugen Rieck

Respostas:

87

Este problema consiste em dois subproblemas muito diferentes:

  • a string deve ser aparentemente aleatória
  • a string deve ser única

Embora a aleatoriedade seja facilmente alcançada, a exclusividade sem um loop de repetição não é. Isso nos leva a nos concentrarmos primeiro na singularidade. A exclusividade não aleatória pode ser alcançada trivialmente com AUTO_INCREMENT. Portanto, usar uma transformação pseudo-aleatória que preserva a exclusividade seria bom:

  • Hash foi sugerido por @paul
  • A criptografia AES também se encaixa
  • Mas há um bom: RAND(N)ele mesmo!

Uma sequência de números aleatórios criados pela mesma semente tem a garantia de ser

  • reproduzível
  • diferente para as primeiras 8 iterações
  • se a semente é um INT32

Portanto, usamos a abordagem de @ AndreyVolk ou @GordonLinoff, mas com uma semente RAND :

por exemplo, Assumin idé uma AUTO_INCREMENTcoluna:

INSERT INTO vehicles VALUES (blah); -- leaving out the number plate
SELECT @lid:=LAST_INSERT_ID();
UPDATE vehicles SET numberplate=concat(
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@lid)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed)*36+1, 1)
)
WHERE id=@lid;
Eugen Rieck
fonte
Abordagem muito interessante, mas provavelmente você quis dizer RAND(LAST_INSERT_ID()); UPDATE vehicles (...) , rand()*36+1, (...)(ou então ele retorna 8 vezes o mesmo caractere). Como podemos ter certeza de que 8 chamadas sucessivas para rand()têm garantia de retornar uma sequência diferente se inicializadas com uma semente diferente?
RandomSeed
8
Eu só estava pensando. Por que você usa esses números ..4294967296)) * 36 + 1?
Mick
7
Isso é um pouco antigo, mas eu gostaria de observar que tive que adicionar FLOOR()os parâmetros da segunda substring: substring('ABC … 789', floor(rand(@seed:= … )*36+1), 1), Em algumas ocasiões, a substring estava tentando escolher o caractere 36,9, que quando arredondado para 37, resultaria em nenhum caractere sendo escolhido.
Phillip Dodson
4
Você não pode chamar uma string de random se ela for reproduzível. E duplicatas também são possíveis, porque você está usando floor(). Este sqlfiddle mostra que duplicatas são criadas para strings de três caracteres.
Paul Spiegel de
6
@EugenRieck Não entendo como você consegue seus números ("primeiras 2 ^ 32 iterações"). Mas preciso apenas de um exemplo de duplicatas para refutar esse conceito. Para os IDs 193844e 775771seu algoritmo irá gerar a mesma string T82X711( demo ).
Paul Spiegel de
113

Como afirmei em meu comentário, não me incomodaria com a probabilidade de colisão. Apenas gere uma string aleatória e verifique se ela existe. Se isso acontecer, tente novamente e você não precisará fazer isso mais do que algumas vezes, a menos que já tenha um grande número de pratos atribuídos.

Outra solução para gerar uma string pseudoaleatória de 8 caracteres em SQL puro (My):

SELECT LEFT(UUID(), 8);

Você pode tentar o seguinte (pseudocódigo):

DO 
    SELECT LEFT(UUID(), 8) INTO @plate;
    INSERT INTO plates (@plate);
WHILE there_is_a_unique_constraint_violation
-- @plate is your newly assigned plate number

Uma vez que esta postagem recebeu um nível inesperado de atenção, deixe-me destacar o comentário do ADTC : o trecho de código acima é bastante burro e produz dígitos sequenciais.

Para uma aleatoriedade um pouco menos estúpida, tente algo assim:

SELECT LEFT(MD5(RAND()), 8)

E para a verdadeira aleatoriedade (criptograficamente segura), use em RANDOM_BYTES()vez de RAND()(mas então eu consideraria mover essa lógica para a camada de aplicativo).

RandomSeed
fonte
Obrigado pela sua solução, tenho uma pergunta sobre UUID. quantas chances o id que você gerou 8 caracteres é repetido novamente.
TR-Ahmed de
1
@ user3099183 Oficialmente , "muito baixo". 16 ^ 8 é cerca de 4 bilhões de strings possíveis.
RandomSeed de
23
Observe que os primeiros 8 caracteres do UUID não são aleatórios, mas sequenciais, pois se baseiam no carimbo de data / hora.
ADTC de
Quero gerar uma string aleatória de 9 caracteres e, quando uso 9em seu código SELECT LEFT(UUID(), 9);, sempre há -no final da string gerada como o nono caractere. É constante. Por quê?
Martin AJ
3
@MartinAJ porque a string é um uuid . Você pode facilmente substituir os hífens, por exemploSELECT LEFT(REPLACE(UUID(), '-', ''), 16);
jchook
53

Que tal calcular o hash MD5 (ou outro) de inteiros sequenciais e, em seguida, pegar os primeiros 8 caracteres.

ie

MD5(1) = c4ca4238a0b923820dcc509a6f75849b => c4ca4238
MD5(2) = c81e728d9d4c2f636f067f89cc14862c => c81e728d
MD5(3) = eccbc87e4b5ce2fe28308fd9f2a7baf3 => eccbc87e

etc.

advertência: não tenho ideia de quantos você poderia alocar antes de uma colisão (mas seria um valor conhecido e constante).

editar: Esta é agora uma resposta antiga, mas eu a vi novamente com o tempo em minhas mãos, então, pela observação ...

Chance de todos os números = 2,35%

Chance de todas as letras = 0,05%

Primeira colisão quando MD5 (82945) = "7b763dcb ..." (mesmo resultado que MD5 (25302))

Paulo
fonte
2
Boa ideia, poderia usá-lo na chave primária. Manteremos isso em mente para projetos futuros, obrigado!
funstein
2
Há uma chance de que isso possa resultar apenas com números
Mladen Janjetovic
9
Não é nada aleatório.
paul
1
Você poderia torná-lo mais "aleatório" se, em vez de usar o id incremental automático, você usasse a data e hora em que a inserção foi feita, que também é única.
Javier La Banca,
35

Crie uma string aleatória

Aqui está uma função MySQL para criar uma string aleatória de um determinado comprimento.

DELIMITER $$

CREATE DEFINER=`root`@`%` FUNCTION `RandString`(length SMALLINT(3)) RETURNS varchar(100) CHARSET utf8
begin
    SET @returnStr = '';
    SET @allowedChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    SET @i = 0;

    WHILE (@i < length) DO
        SET @returnStr = CONCAT(@returnStr, substring(@allowedChars, FLOOR(RAND() * LENGTH(@allowedChars) + 1), 1));
        SET @i = @i + 1;
    END WHILE;

    RETURN @returnStr;
END

Uso SELECT RANDSTRING(8)para retornar uma string de 8 caracteres.

Você pode personalizar o @allowedChars.

A exclusividade não é garantida - como você verá nos comentários de outras soluções, isso simplesmente não é possível. Em vez disso, você precisará gerar uma string, verificar se já está em uso e tentar novamente se estiver.


Verifique se a string aleatória já está em uso

Se quisermos manter o código de verificação de colisão fora do aplicativo, podemos criar um gatilho:

DELIMITER $$

CREATE TRIGGER Vehicle_beforeInsert
  BEFORE INSERT ON `Vehicle`
  FOR EACH ROW
  BEGIN
    SET @vehicleId = 1;
    WHILE (@vehicleId IS NOT NULL) DO 
      SET NEW.plate = RANDSTRING(8);
      SET @vehicleId = (SELECT id FROM `Vehicle` WHERE `plate` = NEW.plate);
    END WHILE;
  END;$$
DELIMITER ;
Paddy Mann
fonte
6
esta deve ser a resposta aceita, clara e direta, funcionou muito bem para mim, obrigado @ paddy-mann
Saif
Essa é a melhor solução, eu acho. Obrigado cara!
Pronoy999
23

Aqui está uma maneira, usando alfanuméricos como caracteres válidos:

select concat(substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1)
             ) as LicensePlaceNumber;

Observe que não há garantia de exclusividade. Você terá que verificar isso separadamente.

Gordon Linoff
fonte
7
use floor (rand () * 36 + 1) em vez disso. Caso contrário, alguns resultados serão 'curtos'.
Fraggle
2
deve haver 35 + 1, não 36 + 1! Caso contrário, você obterá algumas cordas com 8 caracteres e algumas delas com 7
BIOHAZARD
23

Aqui está outro método para gerar uma string aleatória:

SELECT SUBSTRING(MD5(RAND()) FROM 1 FOR 8) AS myrandomstring

seralex
fonte
16

Você pode usar as funções rand () e char () do MySQL :

select concat( 
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97)
) as name;
Andrey Volk
fonte
14

Você pode gerar uma string alfanumérica aleatória com:

lpad(conv(floor(rand()*pow(36,8)), 10, 36), 8, 0);

Você pode usá-lo em um BEFORE INSERTgatilho e verificar se há uma duplicata em um loop while:

CREATE TABLE `vehicles` (
    `plate` CHAR(8) NULL DEFAULT NULL,
    `data` VARCHAR(50) NOT NULL,
    UNIQUE INDEX `plate` (`plate`)
);

DELIMITER //
CREATE TRIGGER `vehicles_before_insert` BEFORE INSERT ON `vehicles`
FOR EACH ROW BEGIN

    declare str_len int default 8;
    declare ready int default 0;
    declare rnd_str text;
    while not ready do
        set rnd_str := lpad(conv(floor(rand()*pow(36,str_len)), 10, 36), str_len, 0);
        if not exists (select * from vehicles where plate = rnd_str) then
            set new.plate = rnd_str;
            set ready := 1;
        end if;
    end while;

END//
DELIMITER ;

Agora basta inserir seus dados como

insert into vehicles(col1, col2) values ('value1', 'value2');

E o gatilho irá gerar um valor para a platecoluna.

( demonstração sqlfiddle )

Isso funciona dessa maneira se a coluna permitir NULLs. Se você quiser que seja NOT NULL, você precisará definir um valor padrão

`plate` CHAR(8) NOT NULL DEFAULT 'default',

Você também pode usar qualquer outro algoritmo de geração de string aleatório no acionador se alfanuméricos maiúsculos não forem o que você deseja. Mas o gatilho cuidará da exclusividade.

Paul Spiegel
fonte
Surpreendente! Isso é exatamente o que eu queria. Eu só gostaria de entender como ele é convertido em uma string. Por que existe um pow, o que fazer, somar números e assim por diante. De qualquer forma, obrigado.
Akxe
@Akxe conv () pode ser usado para converter um número em uma string alfanumérica. pow(36,8)-1é a representação numérica de ZZZZZZZZ. Portanto, geramos um número inteiro aleatório entre 0e '36 ^ 8-1 '(de 0a 2821109907455) e o convertemos em uma string alfanumérica entre 0e ZZZZZZZZunsing conv(). lapad () preencherá a string com zeros até atingir o comprimento de 8.
Paul Spiegel
Senhor, você é um gênio. Imagino que adicionar letras minúsculas seja impossível devido a não continuidade dos caracteres? (91-96) Não que eu precise, apenas curiosidade ...
Akxe
@Akxe conv()suporta apenas uma base de até 36 (10 dígitos + 26 letras maiúsculas). Se você quiser incluir letras minúsculas, precisará de outra maneira de converter um número em uma string.
Paul Spiegel
Advertência: não funciona para str_len> 13. A partir de 14, você sempre obtém '3W5E11264SGSF'. ;-)
Gerard H. Pille
6

Para gerar string aleatória, você pode usar:

SUBSTRING(MD5(RAND()) FROM 1 FOR 8)

Você recebe algo assim:

353E50CC

Nikita G.
fonte
5

Para uma String que consiste em 8 números aleatórios e letras maiúsculas e minúsculas, esta é a minha solução:

LPAD(LEFT(REPLACE(REPLACE(REPLACE(TO_BASE64(UNHEX(MD5(RAND()))), "/", ""), "+", ""), "=", ""), 8), 8, 0)

Explicado de dentro para fora:

  1. RAND gera um número aleatório entre 0 e 1
  2. MD5 calcula a soma MD5 de (1), 32 caracteres de af e 0-9
  3. UNHEX traduz (2) em 16 bytes com valores de 00 a FF
  4. TO_BASE64 codifica (3) como base64, 22 caracteres de az e AZ e 0-9 mais "/" e "+", seguido por dois "="
  5. os três REPLACEs removem os caracteres "/", "+" e "=" de (4)
  6. LEFT pega os primeiros 8 caracteres de (5), mude 8 para outro se precisar de mais ou menos caracteres em sua string aleatória
  7. LPADinsere zeros no início de (6) se tiver menos de 8 caracteres; novamente, mude 8 para algo diferente, se necessário
Jan Uhlig
fonte
Ótimo, exatamente o que eu estava procurando para criar um ID semelhante a token nativamente no MySQL
rabudde
4

I Use dados de outra coluna para gerar um "hash" ou string única

UPDATE table_name SET column_name = Right( MD5(another_column_with_data), 8 )
Richard Fragiacomo
fonte
4

8 letras do alfabeto - todas maiúsculas:

UPDATE `tablename` SET `tablename`.`randomstring`= concat(CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25)))CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))));
TV-C-15
fonte
3

Se você não tem um id ou seed, como é para uma lista de valores no insert:

REPLACE(RAND(), '.', '')
Ekerner
fonte
2

Solução simples e eficiente para obter uma string aleatória de 10 caracteres com letras maiúsculas e minúsculas e dígitos:

select substring(base64_encode(md5(rand())) from 1+rand()*4 for 10);
Antares
fonte
1

Se você concorda com placas "aleatórias", mas totalmente previsíveis, você pode usar um registrador de deslocamento de feedback linear para escolher o próximo número de placa - é garantido que todos os números sejam passados ​​antes de repetir. No entanto, sem alguma matemática complexa, você não será capaz de passar por cada string alfanumérica de 8 caracteres (você obterá 2 ^ 41 dos 36 ^ 8 (78%) placas possíveis). Para fazer isso preencher melhor o seu espaço, você pode excluir uma letra das placas (talvez O), dando a você 97%.

τεκ
fonte
1

Levando em conta o número total de caracteres que você precisa, você teria uma chance muito pequena de gerar duas placas de matrícula exatamente semelhantes. Portanto, você provavelmente conseguirá gerar os números em LUA.

Você tem 36 ^ 8 placas de número exclusivas diferentes (2.821.109.907.456, isso é muito), mesmo se você já tivesse um milhão de placas de número, você teria uma chance muito pequena de gerar uma que já tem, cerca de 0,000035%

Claro, tudo depende de quantas placas numéricas você acabará criando.

Lawrence Andrews
fonte
É verdade, vou continuar fazendo isso no jogo real, em vez de SQL. Muito obrigado.
funstein
1

Esta função gera uma string aleatória com base no comprimento de entrada e caracteres permitidos como este:

SELECT str_rand(8, '23456789abcdefghijkmnpqrstuvwxyz');

código de função:

DROP FUNCTION IF EXISTS str_rand;

DELIMITER //

CREATE FUNCTION str_rand(
    u_count INT UNSIGNED,
    v_chars TEXT
)
RETURNS TEXT
NOT DETERMINISTIC
NO SQL
SQL SECURITY INVOKER
COMMENT ''
BEGIN
    DECLARE v_retval TEXT DEFAULT '';
    DECLARE u_pos    INT UNSIGNED;
    DECLARE u        INT UNSIGNED;

    SET u = LENGTH(v_chars);
    WHILE u_count > 0
    DO
      SET u_pos = 1 + FLOOR(RAND() * u);
      SET v_retval = CONCAT(v_retval, MID(v_chars, u_pos, 1));
      SET u_count = u_count - 1;
    END WHILE;

    RETURN v_retval;
END;
//
DELIMITER ;

Este código é baseado na função shuffle string enviada por "Ross Smith II"

Mahoor13
fonte
Esta função não irá gerar um valor único aleatório.
Faisal
1

Para criar um alfanumérico aleatório de 10 dígitos , excluindo caracteres semelhantes 01oOlI:

LPAD(LEFT(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(TO_BASE64(UNHEX(MD5(RAND()))), "/", ""), "+", ""), "=", ""), "O", ""), "l", ""), "I", ""), "1", ""), "0", ""), "o", ""), 10), 10, 0)

Isso é exatamente o que eu precisava para criar um código de voucher . Caracteres confusos são removidos para reduzir erros ao digitá-los em um formulário de código de voucher.

Espera que isso ajude alguém, com base na resposta brilhante de Jan Uhlig .

Consulte a resposta de Jan para uma análise de como esse código funciona.

Paul Harris
fonte
0
DELIMITER $$

USE `temp` $$

DROP PROCEDURE IF EXISTS `GenerateUniqueValue`$$

CREATE PROCEDURE `GenerateUniqueValue`(IN tableName VARCHAR(255),IN columnName VARCHAR(255)) 
BEGIN
    DECLARE uniqueValue VARCHAR(8) DEFAULT "";
    WHILE LENGTH(uniqueValue) = 0 DO
        SELECT CONCAT(SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1)
                ) INTO @newUniqueValue;
        SET @rcount = -1;
        SET @query=CONCAT('SELECT COUNT(*) INTO @rcount FROM  ',tableName,' WHERE ',columnName,'  like ''',@newUniqueValue,'''');
        PREPARE stmt FROM  @query;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
    IF @rcount = 0 THEN
            SET uniqueValue = @newUniqueValue ;
        END IF ;
    END WHILE ;
    SELECT uniqueValue;
    END$$

DELIMITER ;

Use este procedimento armazenado e use-o sempre como

Call GenerateUniqueValue('tableName','columnName')
Hariramprasath Nandhagopalan
fonte
0

Uma maneira fácil de gerar um número único

set @i = 0;
update vehicles set plate = CONCAT(@i:=@i+1, ROUND(RAND() * 1000)) 
order by rand();
Gautier
fonte
0

Eu estava procurando por algo semelhante e decidi fazer minha própria versão onde você também pode especificar uma semente diferente se quiser (lista de personagens) como parâmetro:

CREATE FUNCTION `random_string`(length SMALLINT(3), seed VARCHAR(255)) RETURNS varchar(255) CHARSET utf8
    NO SQL
BEGIN
    SET @output = '';

    IF seed IS NULL OR seed = '' THEN SET seed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; END IF;

    SET @rnd_multiplier = LENGTH(seed);

    WHILE LENGTH(@output) < length DO
        # Select random character and add to output
        SET @output = CONCAT(@output, SUBSTRING(seed, RAND() * (@rnd_multiplier + 1), 1));
    END WHILE;

    RETURN @output;
END

Pode ser usado como:

SELECT random_string(10, '')

Que usaria a semente embutida de caracteres maiúsculos e minúsculos + dígitos. NULL também seria um valor em vez de ''.

Mas pode-se especificar uma semente personalizada ao chamar:

SELECT random_string(10, '1234')
Maarten Ureel
fonte