Linha aleatória de Linq para Sql

112

Qual é a melhor (e mais rápida) maneira de recuperar uma linha aleatória usando Linq para SQL quando tenho uma condição, por exemplo, algum campo deve ser verdadeiro?

Julien Poulin
fonte
Você tem duas opções para o pedido, você verifica as verdadeiras condições. Se a condição verdadeira acontecer na maioria dos itens, basta pegar um item aleatório, testar e repetir enquanto falsa. Se for raro, deixe o banco de dados limitar as opções à condição verdadeira e, em seguida, pegue uma ao acaso.
Rex Logan,
1
Tal como acontece com muitas respostas neste site - a segunda classificação é muito melhor do que a aceita.
nikib3ro

Respostas:

169

Você pode fazer isso no banco de dados, usando um UDF falso; em uma classe parcial, adicione um método ao contexto de dados:

partial class MyDataContext {
     [Function(Name="NEWID", IsComposable=true)] 
     public Guid Random() 
     { // to prove not used by our C# code... 
         throw new NotImplementedException(); 
     }
}

Então apenas order by ctx.Random(); isso fará uma ordem aleatória no SQL-Server, cortesia de NEWID(). ie

var cust = (from row in ctx.Customers
           where row.IsActive // your filter
           orderby ctx.Random()
           select row).FirstOrDefault();

Observe que isso só é adequado para tabelas de tamanho pequeno a médio; para tabelas grandes, isso terá um impacto no desempenho do servidor e será mais eficiente encontrar o número de linhas ( Count) e, em seguida, escolher uma ao acaso ( Skip/First).


para abordagem de contagem:

var qry = from row in ctx.Customers
          where row.IsActive
          select row;

int count = qry.Count(); // 1st round-trip
int index = new Random().Next(count);

Customer cust = qry.Skip(index).FirstOrDefault(); // 2nd round-trip
Marc Gravell
fonte
3
Se for 30k após o filtro, eu diria não: não use essa abordagem. Faça 2 viagens de ida e volta; 1 para obter o Count () e 1 para obter uma linha aleatória ...
Marc Gravell
1
E se você quiser cinco (ou "x") linhas aleatórias? É melhor apenas fazer seis viagens de ida e volta ou existe uma maneira conveniente de implementá-lo em um procedimento armazenado?
Neal Stublen
2
@Neal S .: a ordem de ctx.Random () pode ser misturada com Take (5); mas se você estiver usando a abordagem Count (), espero que 6 viagens de ida e volta seja a opção mais simples.
Marc Gravell
1
não se esqueça de adicionar uma referência a System.Data.Linq ou o atributo System.Data.Linq.Mapping.Function não funcionará.
Jaguir
8
Eu sei que isso é antigo, mas se você estiver selecionando muitas linhas aleatórias de uma grande tabela, veja isto: msdn.microsoft.com/en-us/library/cc441928.aspx Não sei se há um equivalente LINQ.
JWD
60

Outro exemplo para Entity Framework:

var customers = db.Customers
                  .Where(c => c.IsActive)
                  .OrderBy(c => Guid.NewGuid())
                  .FirstOrDefault();

Isso não funciona com LINQ to SQL. O OrderByestá simplesmente sendo descartado.

Konstantin Tarkus
fonte
4
Você já traçou um perfil e confirmou que funciona? Em meus testes usando LINQPad, a cláusula order by está sendo descartada.
Jim Wooley
Esta é a melhor solução para este problema
reach4thelasers
8
Isso não funciona no LINQ to SQL ... talvez funcione no Entity Framework 4 (não confirmando). Você só pode usar .OrderBy com Guid se estiver classificando uma lista ... com DB não funcionará.
nikib3ro
2
Só para finalmente confirmar que funciona no EF4 - é uma ótima opção nesse caso.
nikib3ro
1
Você poderia editar sua resposta e explicar por que o orderBy com um novo Guid resolve o problema? A propósito, boa resposta :)
Jean-François Côté
32

EDIT: Acabei de notar que este é LINQ to SQL, não LINQ to Objects. Use o código de Marc para que o banco de dados faça isso por você. Deixei esta resposta aqui como um ponto de interesse potencial para LINQ to Objects.

Estranhamente, você realmente não precisa fazer a contagem. No entanto, você precisa buscar todos os elementos, a menos que obtenha a contagem.

O que você pode fazer é manter a ideia de um valor "atual" e a contagem atual. Ao buscar o próximo valor, pegue um número aleatório e substitua o "atual" por "novo" com uma probabilidade de 1 / n, onde n é a contagem.

Portanto, quando você lê o primeiro valor, sempre o torna o valor "atual". Quando você lê o segundo valor, pode torná-lo o valor atual (probabilidade 1/2). Quando você lê o terceiro valor, pode torná-lo o valor atual (probabilidade 1/3) etc. Quando você fica sem dados, o valor atual é aleatório de todos os que você lê, com probabilidade uniforme.

Para aplicar isso com uma condição, simplesmente ignore qualquer coisa que não atenda à condição. A maneira mais fácil de fazer isso é considerar apenas a sequência de "correspondência" para começar, aplicando primeiro uma cláusula Where.

Aqui está uma implementação rápida. Eu acho que está tudo bem ...

public static T RandomElement<T>(this IEnumerable<T> source,
                                 Random rng)
{
    T current = default(T);
    int count = 0;
    foreach (T element in source)
    {
        count++;
        if (rng.Next(count) == 0)
        {
            current = element;
        }            
    }
    if (count == 0)
    {
        throw new InvalidOperationException("Sequence was empty");
    }
    return current;
}
Jon Skeet
fonte
4
FYI - Eu executei uma verificação rápida e esta função tem uma distribuição de probabilidade uniforme (a contagem de incremento é essencialmente o mesmo mecanismo do embaralhamento de Fisher-Yates, então parece razoável que deveria ser).
Greg Beech,
@Greg: Legal, obrigado. Pareceu-me bem com uma verificação rápida, mas é tão fácil obter erros indevidos em códigos como este. Praticamente irrelevante para LINQ to SQL, é claro, mas útil mesmo assim.
Jon Skeet,
@JonSkeet, oi, você pode verificar isso e me dizer o que estou perdendo
shaijut de
@TylerLaing: Não, não é para ser uma pausa. Na primeira iteração, currentirá sempre ser definido para o primeiro elemento. Na segunda iteração, há uma mudança de 50% que será definido para o segundo elemento. Na terceira iteração, há 33% de chance de ser definido para o terceiro elemento. Adicionar uma instrução break significaria que você sempre sairia após ler o primeiro elemento, tornando-o nem um pouco aleatório.
Jon Skeet
@JonSkeet Doh! Eu interpretei mal o seu uso de contagem (por exemplo, estava pensando que era o estilo Fisher-Yates com um intervalo aleatório como ni). Mas selecionar o primeiro elemento em Fisher-Yates é escolher razoavelmente qualquer um dos elementos. No entanto, isso exige saber o número total de elementos. Vejo agora que sua solução é ótima para um IEnumerable, pois a contagem total não é conhecida e não há necessidade de iterar em toda a origem apenas para obter a contagem e, em seguida, iterar novamente para algum índice escolhido aleatoriamente. Em vez disso, isso resolve em uma passagem, como você afirmou: "precisa buscar todos os elementos, a menos que você obtenha a contagem".
Tyler Laing
19

Uma maneira de fazer isso com eficiência é adicionar uma coluna aos seus dados Shuffleque é preenchida com um int aleatório (conforme cada registro é criado).

A consulta parcial para acessar a tabela em ordem aleatória é ...

Random random = new Random();
int seed = random.Next();
result = result.OrderBy(s => (~(s.Shuffle & seed)) & (s.Shuffle | seed)); // ^ seed);

Isso faz uma operação XOR no banco de dados e ordena pelos resultados desse XOR.

Vantagens: -

  1. Eficiente: o SQL lida com a ordenação, sem necessidade de buscar a tabela inteira
  2. Repetível: (bom para teste) - pode usar a mesma semente aleatória para gerar a mesma ordem aleatória

Esta é a abordagem usada pelo meu sistema de automação residencial para randomizar playlists. Ele pega uma nova semente a cada dia, dando uma ordem consistente durante o dia (permitindo recursos fáceis de pausar / retomar), mas uma nova visão de cada lista de reprodução a cada novo dia.

Ian Mercer
fonte
qual seria o efeito na aleatoriedade se, em vez de adicionar um campo int aleatório, você apenas usasse um campo de identidade de incremento automático existente (a semente obviamente permaneceria aleatória)? também - um valor de semente com um máximo igual ao número de registros na tabela é adequado ou deveria ser maior?
Bryan,
Concordo, esta é uma ótima resposta que a IMO deveria ter mais votos positivos. Eu usei isso em uma consulta do Entity Framework e o operador bit a bit XOR ^ parece funcionar diretamente, tornando a condição um pouco mais limpa: result = result.OrderBy(s => s.Shuffle ^ seed);(ou seja, não há necessidade de implementar o XOR por meio dos operadores ~, & e |).
Steven Rands
7

se você quiser obter, por exemplo var count = 16 linhas aleatórias da tabela, você pode escrever

var rows = Table.OrderBy(t => Guid.NewGuid())
                        .Take(count);

aqui eu usei EF, e a Tabela é um Dbset

Artur Keyan
fonte
1

Se o objetivo de obter linhas aleatórias é amostrar, falei brevemente aqui sobre uma boa abordagem de Larson et al., Equipe de pesquisa da Microsoft, onde desenvolveram uma estrutura de amostragem para Sql Server usando visualizações materializadas. Também existe um link para o artigo em si.

naiemk
fonte
1
List<string> lst = new List<string>();
lst.Add("Apple"); 
lst.Add("Guva");
lst.Add("Graps"); 
lst.Add("PineApple");
lst.Add("Orange"); 
lst.Add("Mango");

var customers = lst.OrderBy(c => Guid.NewGuid()).FirstOrDefault();

Explicação: Ao inserir o guid (que é aleatório), a ordem com orderby seria aleatória.

Nayeem Mansoori
fonte
Os Guids não são "aleatórios", eles são não sequenciais. Há uma diferença. Na prática, provavelmente não importa para algo trivial como isso.
Chris Marisic
0

Vim aqui querendo saber como obter algumas páginas aleatórias de um pequeno número delas, de modo que cada usuário receba algumas 3 páginas aleatórias diferentes.

Esta é minha solução final, trabalhando em consultas com LINQ em uma lista de páginas no Sharepoint 2010. Está no Visual Basic, desculpe: p

Dim Aleatorio As New Random()

Dim Paginas = From a As SPListItem In Sitio.RootWeb.Lists("Páginas") Order By Aleatorio.Next Take 3

Provavelmente deveria obter algum perfil antes de consultar um grande número de resultados, mas é perfeito para o meu propósito

Fran
fonte
0

Eu tenho uma consulta de função aleatória contra DataTables:

var result = (from result in dt.AsEnumerable()
              order by Guid.NewGuid()
              select result).Take(3); 
Midhun Sankar
fonte
0

O exemplo abaixo chamará a fonte para recuperar uma contagem e, em seguida, aplicará uma expressão de salto na fonte com um número entre 0 e n. O segundo método aplicará a ordem usando o objeto aleatório (que ordenará tudo na memória) e selecionará o número passado na chamada do método.

public static class IEnumerable
{
    static Random rng = new Random((int)DateTime.Now.Ticks);

    public static T RandomElement<T>(this IEnumerable<T> source)
    {
        T current = default(T);
        int c = source.Count();
        int r = rng.Next(c);
        current = source.Skip(r).First();
        return current;
    }

    public static IEnumerable<T> RandomElements<T>(this IEnumerable<T> source, int number)
    {
        return source.OrderBy(r => rng.Next()).Take(number);
    }
}
user1619860
fonte
Seria bom alguma explicação
Andrew Barber,
Este código não é
seguro
0

eu uso este método para obter notícias aleatórias e seu funcionamento;

    public string LoadRandomNews(int maxNews)
    {
        string temp = "";

        using (var db = new DataClassesDataContext())
        {
            var newsCount = (from p in db.Tbl_DynamicContents
                             where p.TimeFoPublish.Value.Date <= DateTime.Now
                             select p).Count();
            int i;
            if (newsCount < maxNews)
                i = newsCount;
            else i = maxNews;
            var r = new Random();
            var lastNumber = new List<int>();
            for (; i > 0; i--)
            {
                int currentNumber = r.Next(0, newsCount);
                if (!lastNumber.Contains(currentNumber))
                { lastNumber.Add(currentNumber); }
                else
                {
                    while (true)
                    {
                        currentNumber = r.Next(0, newsCount);
                        if (!lastNumber.Contains(currentNumber))
                        {
                            lastNumber.Add(currentNumber);
                            break;
                        }
                    }
                }
                if (currentNumber == newsCount)
                    currentNumber--;
                var news = (from p in db.Tbl_DynamicContents
                            orderby p.ID descending
                            where p.TimeFoPublish.Value.Date <= DateTime.Now
                            select p).Skip(currentNumber).Take(1).Single();
                temp +=
                    string.Format("<div class=\"divRandomNews\"><img src=\"files/1364193007_news.png\" class=\"randomNewsImg\" />" +
                                  "<a class=\"randomNews\" href=\"News.aspx?id={0}\" target=\"_blank\">{1}</a></div>",
                                  news.ID, news.Title);
            }
        }
        return temp;
    }
sadati
fonte
0

Usando LINQ to SQL no LINQPad como instruções C # parecem

IEnumerable<Customer> customers = this.ExecuteQuery<Customer>(@"SELECT top 10 * from [Customers] order by newid()");
customers.Dump();

O SQL gerado é

SELECT top 10 * from [Customers] order by newid()
JCO
fonte
0

Se você usa LINQPad , alterne para o modo de programa C # e faça o seguinte:

void Main()
{
    YourTable.OrderBy(v => Random()).FirstOrDefault.Dump();
}

[Function(Name = "NEWID", IsComposable = true)]
public Guid Random()
{
    throw new NotImplementedException();
}
alexey
fonte
0
var cust = (from c in ctx.CUSTOMERs.ToList() select c).OrderBy(x => x.Guid.NewGuid()).Taket(2);

Selecione 2 linhas aleatórias

Bahadır ATASOY
fonte
0

Para adicionar à solução de Marc Gravell. Se você não estiver trabalhando com a classe do datacontext em si (porque você o faz proxy de alguma forma, por exemplo, para falsificar o datacontext para fins de teste), você não pode usar o UDF definido diretamente: ele não será compilado para SQL porque você não o está usando em um subclasse ou classe parcial de sua classe de contexto de dados reais.

Uma solução alternativa para esse problema é criar uma função Randomize em seu proxy, alimentando-o com a consulta que você deseja que seja randomizada:

public class DataContextProxy : IDataContext
{
    private readonly DataContext _context;

    public DataContextProxy(DataContext context)
    {
        _context = context;
    }

    // Snipped irrelevant code

    public IOrderedQueryable<T> Randomize<T>(IQueryable<T> query)
    {
        return query.OrderBy(x => _context.Random());
    }
}

Aqui está como você o usaria em seu código:

var query = _dc.Repository<SomeEntity>();
query = _dc.Randomize(query);

Para ser completo, esta é a forma de implementar isso no datacontext FAKE (que usa em entidades de memória):

public IOrderedQueryable<T> Randomize<T>(IQueryable<T> query)
{
    return query.OrderBy(x => Guid.NewGuid());
}
Dave de Jong
fonte