Riga casuale da Linq a Sql


112

Qual è il modo migliore (e più veloce) per recuperare una riga casuale utilizzando Linq in SQL quando ho una condizione, ad esempio un campo deve essere vero?


Hai due opzioni per l'ordine e controlla le condizioni reali. Se la condizione vera si verifica sulla maggior parte degli articoli, prendi un oggetto a caso, quindi prova e ripeti finché è falso. Se raro, lascia che il database limiti le opzioni alla condizione vera e poi prendine una a caso.
Rex Logan

1
Come con molte risposte su questo sito, il secondo classificato è molto meglio di quello accettato.
nikib3ro

Risposte:


169

Puoi farlo nel database, usando una falsa UDF; in una classe parziale, aggiungi un metodo al contesto dati:

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

Quindi solo order by ctx.Random(); questo eseguirà un ordine casuale per gentile concessione di SQL Server NEWID(). vale a dire

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

Notare che questo è adatto solo per tavoli di piccole e medie dimensioni; per tabelle enormi, avrà un impatto sulle prestazioni sul server e sarà più efficiente trovare il numero di righe ( Count), quindi sceglierne uno a caso ( Skip/First).


per approccio conteggio:

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

3
Se è 30k dopo il filtro, direi di no: non utilizzare questo approccio. Fai 2 viaggi di andata e ritorno; 1 per ottenere il conteggio () e 1 per ottenere una riga casuale ...
Marc Gravell

1
E se vuoi cinque (o "x") righe casuali? È meglio fare solo sei round trip o esiste un modo conveniente per implementarlo in una stored procedure?
Neal Stublen

2
@Neal S .: l'ordine di ctx.Random () potrebbe essere mescolato con Take (5); ma se stai usando l'approccio Count (), mi aspetto che 6 round trip siano l'opzione più semplice.
Marc Gravell

1
non dimenticare di aggiungere un riferimento a System.Data.Linq o l'attributo System.Data.Linq.Mapping.Function non funzionerà.
Jaguir

8
So che questo è vecchio, ma se stai selezionando molte righe casuali da una tabella di grandi dimensioni, vedi questo: msdn.microsoft.com/en-us/library/cc441928.aspx Non so se esiste un equivalente LINQ.
jwd

60

Un altro esempio per Entity Framework:

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

Questo non funziona con LINQ to SQL. Sta OrderBysemplicemente cadendo.


4
Hai profilato questo e confermato che funziona? Nei miei test che utilizzano LINQPad, la clausola order by viene eliminata.
Jim Wooley

Questa è la migliore soluzione a questo problema
reach4thelasers

8
Questo non funziona in LINQ to SQL ... forse funziona in Entity Framework 4 (non confermandolo). Puoi usare .OrderBy con Guid solo se stai ordinando una lista ... con DB non funzionerà.
nikib3ro

2
Solo per confermare finalmente che funziona in EF4: è un'ottima opzione in questo caso.
nikib3ro

1
Potresti modificare la tua risposta e spiegare perché orderBy con un nuovo Guid fa il trucco? Bella risposta a proposito :)
Jean-François Côté

32

EDIT: Ho appena notato che questo è LINQ to SQL, non LINQ to Objects. Usa il codice di Marc per fare in modo che il database lo faccia per te. Ho lasciato questa risposta qui come potenziale punto di interesse per LINQ to Objects.

Stranamente, non hai davvero bisogno di ottenere il conteggio. Tuttavia, è necessario recuperare ogni elemento a meno che non si ottenga il conteggio.

Quello che puoi fare è mantenere l'idea di un valore "corrente" e il conteggio corrente. Quando prendi il valore successivo, prendi un numero casuale e sostituisci "corrente" con "nuovo" con una probabilità di 1 / n dove n è il conteggio.

Quindi, quando leggi il primo valore, lo rendi sempre il valore "corrente". Quando leggi il secondo valore, potresti renderlo il valore corrente (probabilità 1/2). Quando leggi il terzo valore, potresti renderlo il valore corrente (probabilità 1/3) ecc. Quando hai esaurito i dati, il valore corrente è casuale tra tutti quelli che leggi, con probabilità uniforme.

Per applicarlo con una condizione, ignora semplicemente tutto ciò che non soddisfa la condizione. Il modo più semplice per farlo è considerare solo la sequenza di "corrispondenza" con cui iniziare, applicando prima una clausola Where.

Ecco una rapida implementazione. Io penso che va bene ...

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;
}

4
Cordiali saluti - Ho eseguito un rapido controllo e questa funzione ha una distribuzione di probabilità uniforme (il conteggio incrementale è essenzialmente lo stesso meccanismo dello shuffle di Fisher-Yates, quindi sembra ragionevole che dovrebbe essere).
Greg Beech

@ Greg: Fantastico, grazie. Mi è sembrato a posto con un rapido controllo, ma è così facile ottenere errori di volta in volta in codice come questo. Praticamente irrilevante per LINQ to SQL, ovviamente, ma comunque utile.
Jon Skeet

@JonSkeet, ciao, si può verificare questo e fammi sapere che cosa manco
shaijut

@TylerLaing: No, non dovrebbe esserci una pausa. Alla prima iterazione, currentsarà sempre impostato sul primo elemento. Nella seconda iterazione, c'è una modifica del 50% che verrà impostata sul secondo elemento. Alla terza iterazione, c'è una probabilità del 33% che venga impostato sul terzo elemento. L'aggiunta di un'istruzione break significherebbe uscire sempre dopo aver letto il primo elemento, rendendolo non casuale.
Jon Skeet,

@JonSkeet Doh! Ho letto male il tuo uso del conteggio (ad esempio, pensavo che fosse lo stile Fisher-Yates con un intervallo casuale come ni). Ma selezionare il primo elemento in Fisher-Yates è scegliere in modo equo uno qualsiasi degli elementi. Tuttavia, ciò richiede la conoscenza del numero totale di elementi. Vedo ora che la tua soluzione è pulita per un IEnumerable in quanto il conteggio totale non è noto e non è necessario iterare sull'intera sorgente solo per ottenere il conteggio, per poi iterare di nuovo su un indice scelto a caso. Piuttosto questo si risolve in un passaggio, come hai affermato: "è necessario recuperare ogni elemento a meno che non si ottenga il conteggio".
Tyler Laing

19

Un modo per ottenere risultati efficienti è aggiungere una colonna ai dati Shuffle che viene popolata con un int casuale (man mano che viene creato ogni record).

La query parziale per accedere alla tabella in ordine casuale è ...

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

Questo esegue un'operazione XOR nel database e ordina in base ai risultati di tale XOR.

Vantaggi: -

  1. Efficiente: SQL gestisce l'ordinamento, non è necessario recuperare l'intera tabella
  2. Ripetibile: (buono per il test) - può utilizzare lo stesso seme casuale per generare lo stesso ordine casuale

Questo è l'approccio utilizzato dal mio sistema domotico per randomizzare le playlist. Sceglie un nuovo seme ogni giorno dando un ordine coerente durante il giorno (consentendo facili funzionalità di pausa / ripresa) ma un nuovo sguardo a ogni playlist ogni nuovo giorno.


quale sarebbe l'effetto sulla casualità se invece di aggiungere un campo int casuale si usasse semplicemente un campo identità auto-incrementante esistente (il seme rimarrebbe ovviamente casuale)? inoltre - un valore di inizializzazione con un massimo uguale al numero di record nella tabella è adeguato o dovrebbe essere superiore?
Bryan

D'accordo, questa è un'ottima risposta che IMO dovrebbe avere più voti positivi. L'ho usato in una query di Entity Framework e l'operatore XOR bit per bit ^ sembra funzionare direttamente rendendo così la condizione un po 'più pulita: result = result.OrderBy(s => s.Shuffle ^ seed);(cioè non è necessario implementare XOR tramite gli operatori ~, & e |).
Steven Rands

7

se vuoi ottenere ad esempio var count = 16righe casuali dalla tabella, puoi scrivere

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

qui ho usato EF e la tabella è un Dbset


1

Se lo scopo di ottenere file casuali è campionamento, ho parlato molto brevemente qui di un approccio di bello da squadra Larson et al., Microsoft Research in cui hanno messo a punto un quadro di campionamento per SQL Server utilizzando viste materializzate. C'è anche un collegamento al documento effettivo.


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();

Spiegazione: Inserendo il guid (che è casuale) l'ordine con orderby sarebbe casuale.


Le guide non sono "casuali", non sono sequenziali. C'è una differenza. In pratica probabilmente non ha importanza per qualcosa di banale come questo.
Chris Marisic

0

Sono venuto qui chiedendomi come ottenere alcune pagine casuali da un piccolo numero di esse, quindi ogni utente ottiene alcune 3 pagine casuali diverse.

Questa è la mia soluzione finale, lavorando su query con LINQ su un elenco di pagine in Sharepoint 2010. È in Visual Basic, mi dispiace: p

Dim Aleatorio As New Random()

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

Probabilmente dovrei ottenere alcuni profili prima di interrogare un gran numero di risultati, ma è perfetto per il mio scopo


0

Ho una query di funzione casuale contro DataTables:

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

0

L'esempio seguente chiamerà la sorgente per recuperare un conteggio e quindi applicherà un'espressione di salto alla sorgente con un numero compreso tra 0 e n. Il secondo metodo applicherà l'ordine utilizzando l'oggetto casuale (che ordinerà tutto in memoria) e selezionerà il numero passato alla chiamata al metodo.

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);
    }
}

Qualche spiegazione sarebbe carina
Andrew Barber

Questo codice non è thread-safe e può essere utilizzato solo in codice a thread singolo (quindi non ASP.NET)
Chris Marisic

0

Uso questo metodo per prendere notizie casuali e funziona bene;)

    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;
    }

0

Utilizzo di LINQ to SQL in LINQPad con l'aspetto delle istruzioni C #

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

L'SQL generato è

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

0

Se usi LINQPad , passa alla modalità programma C # e procedi in questo modo:

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

[Function(Name = "NEWID", IsComposable = true)]
public Guid Random()
{
    throw new NotImplementedException();
}

0
var cust = (from c in ctx.CUSTOMERs.ToList() select c).OrderBy(x => x.Guid.NewGuid()).Taket(2);

Seleziona 2 righe casuali


0

Da aggiungere alla soluzione di Marc Gravell. Se non stai lavorando con la classe datacontext stessa (perché in qualche modo lo fai tramite proxy, ad esempio per falsificare il datacontext a scopo di test), non puoi usare direttamente l'UDF definito: non sarà compilato in SQL perché non lo stai usando in un sottoclasse o classe parziale della classe del contesto dati reale.

Una soluzione alternativa a questo problema è creare una funzione Randomize nel proxy, alimentandola con la query che desideri venga randomizzata:

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());
    }
}

Ecco come lo useresti nel tuo codice:

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

Per completare, ecco come implementarlo nel FAKE datacontext (che utilizza nelle entità di memoria):

public IOrderedQueryable<T> Randomize<T>(IQueryable<T> query)
{
    return query.OrderBy(x => Guid.NewGuid());
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.