Qual é a melhor maneira de obter uma encomenda aleatória?

27

Eu tenho uma consulta em que desejo que os registros resultantes sejam ordenados aleatoriamente. Ele usa um índice em cluster, portanto, se eu não incluir um order by, provavelmente retornará registros na ordem desse índice. Como posso garantir um pedido aleatório de linhas?

Entendo que provavelmente não será "verdadeiramente" aleatório, o pseudo-aleatório é bom o suficiente para minhas necessidades.

goric
fonte

Respostas:

19

ORDER BY NEWID () classifica os registros aleatoriamente. Um exemplo aqui

SELECT *
FROM Northwind..Orders 
ORDER BY NEWID()
Nômade
fonte
7
ORDER BY NEWID () é efetivamente aleatório, mas não estatisticamente aleatório. Há uma pequena diferença, e na maioria das vezes a diferença não importa.
mrdenny
4
Do ponto de vista do desempenho, isso é bastante lento - você pode obter uma melhoria significativa com o ORDER BY CHECKSUM (NEWID ())
Miles D
11
@mrdenny - Em que você baseia o "não estatisticamente aleatório"? A resposta aqui diz que acaba usando CryptGenRandomno final. dba.stackexchange.com/a/208069/3690
Martin Smith
15

A primeira sugestão do Pradeep Adiga,, ORDER BY NEWID()é boa e algo que usei no passado por esse motivo.

Cuidado ao usar RAND()- em muitos contextos, ele é executado apenas uma vez por instrução, portanto ORDER BY RAND()não terá efeito (pois você está obtendo o mesmo resultado de RAND () para cada linha).

Por exemplo:

SELECT display_name, RAND() FROM tr_person

retorna cada nome da nossa tabela pessoal e um número "aleatório", que é o mesmo para cada linha. O número varia cada vez que você executa a consulta, mas é o mesmo para cada linha de cada vez.

Para mostrar que o mesmo é o caso de RAND()usado em uma ORDER BYcláusula, tento:

SELECT display_name FROM tr_person ORDER BY RAND(), display_name

Os resultados ainda são ordenados pelo nome, indicando que o campo de classificação anterior (aquele que se espera que seja aleatório) não tem efeito, portanto, presumivelmente, sempre tem o mesmo valor.

A ordenação por NEWID()funciona, no entanto, porque se NEWID () nem sempre fosse reavaliado, a finalidade dos UUIDs seria quebrada ao inserir muitas novas linhas em uma statemnt com identificadores exclusivos, conforme a chave:

SELECT display_name FROM tr_person ORDER BY NEWID()

não pedir os nomes "aleatoriamente".

Outros DBMS

O acima exposto é verdadeiro para o MSSQL (pelo menos em 2005 e 2008, e se bem me lembro de 2000). Uma função que retorne um novo UUID deve ser avaliada sempre que todos os DBMSs NEWID () estiverem no MSSQL, mas vale a pena verificar isso na documentação e / ou nos seus próprios testes. É mais provável que o comportamento de outras funções de resultado arbitrário, como RAND (), varie entre DBMSs; verifique novamente a documentação.

Também vi a ordenação por valores UUID sendo ignorada em alguns contextos, pois o banco de dados assume que o tipo não possui ordenação significativa. Se você achar que esse é o caso, converta explicitamente o UUID para um tipo de seqüência de caracteres na cláusula de ordenação ou agrupe alguma outra função ao redor, como CHECKSUM()no SQL Server (pode haver uma pequena diferença de desempenho disso também, pois a ordenação será feita em valores de 32 bits e não de 128 bits, embora o benefício disso supere o custo de execução CHECKSUM()por valor primeiro, deixarei você testar).

Nota

Se você deseja uma ordem arbitrária, mas um tanto repetível, solicite por algum subconjunto relativamente descontrolado dos dados nas próprias linhas. Por exemplo, um ou estes retornarão os nomes em uma ordem arbitrária, mas repetível:

SELECT display_name FROM tr_person ORDER BY CHECKSUM(display_name), display_name -- order by the checksum of some of the row's data
SELECT display_name FROM tr_person ORDER BY SUBSTRING(display_name, LEN(display_name)/2, 128) -- order by part of the name field, but not in any an obviously recognisable order)

Ordens arbitrárias, mas repetíveis, geralmente não são úteis em aplicativos, mas podem ser úteis em testes, se você quiser testar algum código nos resultados em uma variedade de ordens, mas desejar repetir cada execução da mesma maneira várias vezes (para obter um tempo médio resultados em várias execuções ou testar se uma correção feita no código remove um problema ou ineficiência destacado anteriormente por um determinado conjunto de resultados de entrada ou apenas para testar se o seu código é "estável" e retorna o mesmo resultado sempre se enviou os mesmos dados em uma determinada ordem).

Esse truque também pode ser usado para obter resultados mais arbitrários de funções, que não permitem chamadas não determinísticas como NEWID () dentro de seus corpos. Novamente, isso não é algo que provavelmente seja útil no mundo real, mas pode ser útil se você quiser que uma função retorne algo aleatório e "random-ish" seja bom o suficiente (mas tenha cuidado para lembrar as regras que determinam quando as funções definidas pelo usuário são avaliadas, ou seja, geralmente apenas uma vez por linha, ou seus resultados podem não ser o que você espera / exige).

atuação

Como aponta EBarr, pode haver problemas de desempenho com qualquer uma das opções acima. Por mais de algumas linhas, você tem quase a garantia de ver a saída em spool para tempdb antes que o número solicitado de linhas seja lido na ordem correta, o que significa que, mesmo se você estiver procurando pelas 10 principais, poderá encontrar um índice completo A verificação (ou pior, a verificação da tabela) acontece junto com um enorme bloco de gravação no tempdb. Portanto, pode ser de vital importância, como na maioria das coisas, fazer benchmarks com dados realistas antes de usá-los na produção.

David Spillett
fonte
14

Esta é uma pergunta antiga, mas um aspecto da discussão está faltando, na minha opinião - DESEMPENHO. ORDER BY NewId()é a resposta geral. Quando fantasia alguém get acrescentam que você realmente deve envolver NewID()em CheckSum(), você sabe, para o desempenho!

O problema com esse método é que você ainda garante uma verificação completa do índice e, em seguida, uma espécie completa dos dados. Se você trabalhou com qualquer volume de dados sério, isso pode rapidamente se tornar caro. Veja este plano de execução típico e observe como a classificação leva 96% do seu tempo ...

insira a descrição da imagem aqui

Para dar uma idéia de como isso é dimensionado, darei dois exemplos de um banco de dados com o qual trabalho.

  • Tabela A - possui 50.000 linhas em 2500 páginas de dados. A consulta aleatória gera 145 leituras em 42ms.
  • Tabela B - possui 1,2 milhão de linhas em 114.000 páginas de dados. A execução Order By newid()nesta tabela gera 53.700 leituras e leva 16 segundos.

A moral da história é que, se você tiver tabelas grandes (pense em bilhões de linhas) ou precisar executar essa consulta com frequência, o newid()método será quebrado. Então, o que um garoto deve fazer?

Conheça TABLESAMPLE ()

No SQL 2005, um novo recurso chamado TABLESAMPLEfoi criado. Eu só vi um artigo discutindo seu uso ... deveria haver mais. Documentos do MSDN aqui . Primeiro um exemplo:

SELECT Top (20) *
FROM Northwind..Orders TABLESAMPLE(20 PERCENT)
ORDER BY NEWID()

A idéia por trás da amostra da tabela é fornecer aproximadamente o tamanho do subconjunto solicitado. O SQL numera cada página de dados e seleciona X por cento dessas páginas. O número real de linhas que você recebe pode variar com base no que existe nas páginas selecionadas.

Então, como eu o uso? Selecione um tamanho de subconjunto que cubra mais do que o número de linhas necessárias e adicione a Top(). A idéia é que você possa fazer com que sua mesa ginormous pareça menor antes do tipo caro.

Pessoalmente, tenho usado para limitar o tamanho da minha tabela. Portanto, nessa tabela de milhões de linhas, top(20)...TABLESAMPLE(20 PERCENT)a consulta cai para 5600 leituras em 1600ms. Há também uma REPEATABLE()opção em que você pode passar um "Seed" para a seleção da página. Isso deve resultar em uma seleção de amostra estável.

Enfim, apenas pensei que isso deveria ser adicionado à discussão. Espero que ajude alguém.

EBarr
fonte
Seria bom poder escrever uma consulta escalável de ordem aleatória que não apenas se amplia, mas também trabalha com pequenos conjuntos de dados. Parece que você precisa alternar manualmente entre ter e não ter, com TABLESAMPLE()base na quantidade de dados que possui. Eu acho que TABLESAMPLE(x ROWS)isso nem garantiria que pelo menos as x linhas fossem retornadas porque a documentação diz “O número real de linhas retornadas pode variar significativamente. Se você especificar um número pequeno, como 5, poderá não receber resultados na amostra. ”- então a ROWSsintaxe realmente ainda é apenas um disfarce PERCENT?
binki
Claro, auto-magia é legal. Na prática, raramente vi uma tabela de 5 linhas ser dimensionada para milhões de linhas sem aviso prévio. TABLESAMPLE () parece basear a seleção do número de páginas em uma tabela; portanto, o tamanho da linha fornecido influencia o que volta. O objetivo da amostra da tabela, pelo menos na minha opinião, é fornecer um bom subconjunto do qual você pode selecionar - como uma tabela derivada.
EBarr
3

Muitas tabelas possuem uma coluna de ID numérica indexada relativamente densa (poucos valores ausentes).

Isso nos permite determinar o intervalo de valores existentes e escolher linhas usando valores de ID gerados aleatoriamente nesse intervalo. Isso funciona melhor quando o número de linhas a serem retornadas é relativamente pequeno e o intervalo de valores de ID é densamente preenchido (portanto, a chance de gerar um valor ausente é pequena o suficiente).

Para ilustrar, o código a seguir escolhe 100 usuários aleatórios distintos da tabela de usuários Estouro de pilha, que possui 8.123.937 linhas.

A primeira etapa é determinar o intervalo de valores de ID, uma operação eficiente devido ao índice:

DECLARE 
    @MinID integer,
    @Range integer,
    @Rows bigint = 100;

--- Find the range of values
SELECT
    @MinID = MIN(U.Id),
    @Range = 1 + MAX(U.Id) - MIN(U.Id)
FROM dbo.Users AS U;

Consulta de intervalo

O plano lê uma linha de cada extremidade do índice.

Agora, geramos 100 IDs aleatórios distintos no intervalo (com linhas correspondentes na tabela de usuários) e retornamos essas linhas:

WITH Random (ID) AS
(
    -- Find @Rows distinct random user IDs that exist
    SELECT DISTINCT TOP (@Rows)
        Random.ID
    FROM dbo.Users AS U
    CROSS APPLY
    (
        -- Random ID
        VALUES (@MinID + (CONVERT(integer, CRYPT_GEN_RANDOM(4)) % @Range))
    ) AS Random (ID)
    WHERE EXISTS
    (
        SELECT 1
        FROM dbo.Users AS U2
            -- Ensure the row continues to exist
            WITH (REPEATABLEREAD)
        WHERE U2.Id = Random.ID
    )
)
SELECT
    U3.Id,
    U3.DisplayName,
    U3.CreationDate
FROM Random AS R
JOIN dbo.Users AS U3
    ON U3.Id = R.ID
-- QO model hint required to get a non-blocking flow distinct
OPTION (MAXDOP 1, USE HINT ('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

consulta de linhas aleatórias

O plano mostra que, nesse caso, eram necessários 601 números aleatórios para encontrar 100 linhas correspondentes. É bem rápido:

Tabela 'Usuários'. Contagem de varreduras 1, leituras lógicas 1937, leituras físicas 2, leituras antecipadas 408
Tabela 'Mesa de trabalho'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras antecipadas 0
Tabela 'Arquivo de Trabalho'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras antecipadas 0

 Tempos de execução do SQL Server:
   Tempo de CPU = 0 ms, tempo decorrido = 9 ms.

Experimente no Stack Exchange Data Explorer.

Paul White diz que a GoFundMonica
fonte
0

Como expliquei neste artigo , para embaralhar o conjunto de resultados SQL, você precisa usar uma chamada de função específica do banco de dados.

Observe que a classificação de um grande conjunto de resultados usando uma função RANDOM pode acabar sendo muito lenta, portanto, faça isso em pequenos conjuntos de resultados.

Se você precisar embaralhar um grande conjunto de resultados e limitá-lo posteriormente, é melhor usar o SQL Server TABLESAMPLEno SQL Server em vez de uma função aleatória na cláusula ORDER BY.

Portanto, supondo que tenhamos a seguinte tabela de banco de dados:

insira a descrição da imagem aqui

E as seguintes linhas na songtabela:

| id | artist                          | title                              |
|----|---------------------------------|------------------------------------|
| 1  | Miyagi & Эндшпиль ft. Рем Дигга | I Got Love                         |
| 2  | HAIM                            | Don't Save Me (Cyril Hahn Remix)   |
| 3  | 2Pac ft. DMX                    | Rise Of A Champion (GalilHD Remix) |
| 4  | Ed Sheeran & Passenger          | No Diggity (Kygo Remix)            |
| 5  | JP Cooper ft. Mali-Koa          | All This Love                      |

No SQL Server, você precisa usar a NEWIDfunção, conforme ilustrado no seguinte exemplo:

SELECT
    CONCAT(CONCAT(artist, ' - '), title) AS song
FROM song
ORDER BY NEWID()

Ao executar a consulta SQL acima mencionada no SQL Server, obteremos o seguinte conjunto de resultados:

| song                                              |
|---------------------------------------------------|
| Miyagi & Эндшпиль ft. Рем Дигга - I Got Love      |
| JP Cooper ft. Mali-Koa - All This Love            |
| HAIM - Don't Save Me (Cyril Hahn Remix)           |
| Ed Sheeran & Passenger - No Diggity (Kygo Remix)  |
| 2Pac ft. DMX - Rise Of A Champion (GalilHD Remix) |

Observe que as músicas estão sendo listadas aleatoriamente, graças à NEWIDchamada de função usada pela cláusula ORDER BY.

Vlad Mihalcea
fonte