Losowy wiersz od Linq do Sql

112

Jaki jest najlepszy (i najszybszy) sposób na pobranie losowego wiersza za pomocą Linq do SQL, gdy mam warunek, np. Jakieś pole musi być prawdziwe?

Julien Poulin
źródło
Masz dwie opcje zamówienia, w którym sprawdzasz prawdziwe warunki. Jeśli prawdziwy stan występuje na większości przedmiotów, po prostu chwyć losowy przedmiot, a następnie przetestuj i powtórz, gdy jest fałszywy. Jeśli to rzadkie, pozwól bazie danych ograniczyć opcje do rzeczywistego warunku, a następnie wybierz losowo jedną.
Rex Logan
1
Podobnie jak w przypadku wielu odpowiedzi na tej stronie - druga ocena jest znacznie lepsza niż akceptowana.
nikib3ro

Odpowiedzi:

169

Możesz to zrobić w bazie danych, używając fałszywego UDF; w klasie częściowej dodaj metodę do kontekstu danych:

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

Wtedy po prostu order by ctx.Random(); spowoduje to losowe uporządkowanie w SQL-Server dzięki uprzejmości NEWID(). to znaczy

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

Zauważ, że jest to odpowiednie tylko dla małych i średnich tabel; w przypadku dużych tabel będzie to miało wpływ na wydajność serwera i efektywniejsze będzie znalezienie liczby wierszy ( Count), a następnie wybranie jednego losowo ( Skip/First).


dla podejścia liczącego:

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
źródło
3
Jeśli jest to 30k po filtrze, powiedziałbym nie: nie używaj tego podejścia. Zrób 2 podróże w obie strony; 1, aby uzyskać Count () i 1, aby uzyskać losowy rząd ...
Marc Gravell
1
Co jeśli chcesz mieć pięć (lub „x”) losowych wierszy? Czy najlepiej jest po prostu wykonać sześć przejazdów w obie strony, czy też istnieje wygodny sposób zaimplementowania tego w procedurze składowanej?
Neal Stublen
2
@Neal S .: zlecenie ctx.Random () można łączyć z Take (5); ale jeśli używasz metody Count (), spodziewam się, że 6 podróży w obie strony jest najprostszą opcją.
Marc Gravell
1
nie zapomnij dodać odwołania do System.Data.Linq lub atrybutu System.Data.Linq.Mapping.Function nie zadziała.
Jaguir,
8
Wiem, że jest stary, ale jeśli wybierasz wiele losowych wierszy z dużej tabeli, zobacz to: msdn.microsoft.com/en-us/library/cc441928.aspx Nie wiem, czy istnieje odpowiednik LINQ.
jwd,
60

Kolejny przykład dla Entity Framework:

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

To nie działa z LINQ to SQL. OrderByJest po prostu przed upadkiem.

Konstantin Tarkus
źródło
4
Czy sprofilowałeś to i potwierdziłeś, że działa? W moich testach przy użyciu LINQPad klauzula order by jest pomijana.
Jim Wooley,
To najlepsze rozwiązanie tego problemu
dotrzyj do 4thelasers
8
To nie działa w LINQ to SQL ... może działa w Entity Framework 4 (bez potwierdzenia). Możesz używać tylko .OrderBy z Guid, jeśli sortujesz Listę ... z DB to nie zadziała.
nikib3ro
2
Aby w końcu potwierdzić, że działa to w EF4 - w takim przypadku jest to świetna opcja.
nikib3ro
1
Czy mógłbyś edytować swoją odpowiedź i wyjaśnić, dlaczego order by z nowym Guidem załatwia sprawę? Przy okazji miła odpowiedź :)
Jean-François Côté
32

EDYCJA: Właśnie zauważyłem, że jest to LINQ to SQL, a nie LINQ to Objects. Użyj kodu Marca, aby uzyskać bazę danych, która zrobi to za Ciebie. Zostawiłem tę odpowiedź tutaj jako potencjalny punkt zainteresowania dla LINQ to Objects.

O dziwo, tak naprawdę nie musisz liczyć. Musisz jednak pobrać każdy element, chyba że otrzymasz liczbę.

Możesz zachować ideę „bieżącej” wartości i aktualnej liczby. Kiedy pobierasz następną wartość, weź liczbę losową i zamień „bieżącą” na „nową” z prawdopodobieństwem 1 / n, gdzie n jest liczbą.

Więc kiedy czytasz pierwszą wartość, zawsze ustawiasz ją jako „bieżącą”. Kiedy czytasz drugą wartość, możesz ustawić ją jako wartość bieżącą (prawdopodobieństwo 1/2). Kiedy czytasz trzecią wartość, możesz uznać, że jest to wartość bieżąca (prawdopodobieństwo 1/3) itd. Kiedy skończą się dane, bieżąca wartość jest losową spośród wszystkich, które czytasz, z jednakowym prawdopodobieństwem.

Aby zastosować to z warunkiem, po prostu zignoruj ​​wszystko, co nie spełnia warunku. Najłatwiejszym sposobem na to jest rozważenie na początku tylko „pasującej” sekwencji, stosując najpierw klauzulę Where.

Oto szybka implementacja. Myślę, że to w porządku ...

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
źródło
4
Do Twojej wiadomości - przeprowadziłem szybkie sprawdzenie i ta funkcja ma jednolity rozkład prawdopodobieństwa (zwiększająca się liczba jest zasadniczo tym samym mechanizmem, co tasowanie Fishera-Yatesa, więc wydaje się rozsądne, że tak powinno być).
Greg Beech
@Greg: Super, dzięki. Wydawało mi się to w porządku po szybkim sprawdzeniu, ale tak łatwo jest usunąć pojedyncze błędy w kodzie takim jak ten. Praktycznie nieistotne dla LINQ to SQL, oczywiście, ale mimo to przydatne.
Jon Skeet
@JonSkeet, cześć, czy możesz to sprawdzić i dać mi znać, czego mi brakuje
shaijut
@TylerLaing: Nie, nie powinno być przerwy. Na pierwszej iteracji currentbędzie zawsze być ustawiony na pierwszym elemencie. W drugiej iteracji następuje 50% zmiana, że ​​zostanie ustawiony na drugi element. W trzeciej iteracji istnieje 33% szansa, że ​​zostanie ustawiony na trzeci element. Dodanie instrukcji break oznaczałoby, że zawsze wychodzisz po przeczytaniu pierwszego elementu, dzięki czemu nie jest on wcale przypadkowy.
Jon Skeet
@JonSkeet Doh! Źle odczytałem twoje użycie count (np. Myślałem, że to styl Fishera-Yatesa z losowym zakresem, takim jak ni). Ale aby wybrać pierwszy element w Fisher-Yates, należy uczciwie wybrać dowolny z elementów. Wymaga to jednak znajomości całkowitej liczby elementów. Widzę teraz, że twoje rozwiązanie jest dobre dla IEnumerable, ponieważ całkowita liczba nie jest znana i nie ma potrzeby iteracji po całym źródle, aby uzyskać liczbę, a następnie powtórzyć iterację do jakiegoś losowo wybranego indeksu. Raczej to rozwiązuje się w jednym przebiegu, jak powiedziałeś: „musisz pobrać każdy element, chyba że uzyskasz liczbę”.
Tyler Laing
19

Jednym ze sposobów na efektywne osiągnięcie tego jest dodanie kolumny do danych Shuffle która jest wypełniona losową liczbą int (podczas tworzenia każdego rekordu).

Częściowe zapytanie o dostęp do tabeli w kolejności losowej to ...

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

Wykonuje operację XOR w bazie danych i porządkuje wyniki tego XOR.

Zalety:-

  1. Wydajność: SQL obsługuje porządkowanie, nie ma potrzeby pobierania całej tabeli
  2. Powtarzalne: (dobre do testowania) - można użyć tego samego losowego ziarna do wygenerowania tej samej kolejności losowej

To jest podejście używane przez mój system automatyki domowej do losowego tworzenia list odtwarzania. Każdego dnia wybiera nowe ziarno, zapewniając stałą kolejność w ciągu dnia (umożliwiając łatwe wstrzymywanie / wznawianie), ale każdego nowego dnia zapewnia świeże spojrzenie na każdą listę odtwarzania.

Ian Mercer
źródło
Jaki byłby wpływ na losowość, gdyby zamiast dodawania losowego pola int po prostu użyłeś istniejącego automatycznie zwiększającego się pola tożsamości (ziarno oczywiście pozostanie losowe)? również - czy wartość początkowa z maksimum równym liczbie rekordów w tabeli jest odpowiednia, czy powinna być wyższa?
Bryan
Zgoda, to świetna odpowiedź, która IMO powinna mieć więcej głosów za. Użyłem tego w kwerendzie Entity Framework, a bitowy operator XOR ^ wydaje się działać bezpośrednio, dzięki czemu warunek jest nieco czystszy: result = result.OrderBy(s => s.Shuffle ^ seed);(tj. Nie ma potrzeby implementowania XOR za pośrednictwem operatorów ~, & i |).
Steven Rands
7

jeśli chcesz uzyskać np. var count = 16losowe wiersze z tabeli, możesz pisać

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

tutaj użyłem EF, a Table to Dbset

Artur Keyan
źródło
1

Jeśli celem pobierania losowych wierszy jest próbkowanie, omówiłem tutaj bardzo krótko o ładnym podejściu Larsona i in., Zespołu Microsoft Research, w którym opracowali ramy próbkowania dla Sql Server przy użyciu zmaterializowanych widoków. Jest też link do aktualnego artykułu.

naiemk
źródło
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();

Objaśnienie: Po wstawieniu guid (który jest losowy) kolejność z orderby będzie losowa.

Nayeem Mansoori
źródło
Guidy nie są „przypadkowe”, są niesekwencyjne. Jest różnica. W praktyce prawdopodobnie nie ma to znaczenia dla czegoś tak trywialnego.
Chris Marisic,
0

Przyszedłem tutaj, zastanawiając się, jak uzyskać kilka losowych stron z niewielkiej liczby z nich, aby każdy użytkownik otrzymał różne losowe 3 strony.

To jest moje ostateczne rozwiązanie, pracujące z LINQ na liście stron w Sharepoint 2010. Jest w Visual Basic, przepraszam: p

Dim Aleatorio As New Random()

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

Prawdopodobnie powinienem uzyskać pewne profilowanie przed zapytaniem o dużą liczbę wyników, ale jest to idealne do moich celów

Fran
źródło
0

Mam losowe zapytanie funkcji przeciwko DataTables:

var result = (from result in dt.AsEnumerable()
              order by Guid.NewGuid()
              select result).Take(3); 
Midhun Sankar
źródło
0

Poniższy przykład wywoła źródło w celu pobrania liczby, a następnie zastosuje wyrażenie pomijania do źródła o liczbie od 0 do n. Druga metoda zastosuje kolejność przy użyciu losowego obiektu (który uporządkuje wszystko w pamięci) i wybierze liczbę przekazaną do wywołania metody.

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
źródło
Jakieś wyjaśnienie byłoby miłe
Andrew Barber
Ten kod nie jest bezpieczny wątkowo i może być używany tylko w kodzie jednowątkowym (a więc nie w ASP.NET)
Chris Marisic
0

używam tej metody do pobierania losowych wiadomości i działa dobrze;)

    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
źródło
0

Używanie LINQ to SQL w LINQPad, jak wyglądają instrukcje C #

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

Wygenerowany kod SQL to

SELECT top 10 * from [Customers] order by newid()
JCO
źródło
0

Jeśli używasz LINQPad , przełącz się do trybu programu C # i zrób w ten sposób:

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

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

Wybierz losowo 2 wiersze

Bahadır ATASOY
źródło
0

Aby dodać do rozwiązania Marca Gravella. Jeśli nie pracujesz z samą klasą datacontext (ponieważ w jakiś sposób proxy ją proxy, np. Aby sfałszować kontekst danych do celów testowych), nie możesz bezpośrednio użyć zdefiniowanego UDF: nie zostanie on skompilowany do SQL, ponieważ nie używasz go w podklasa lub część klasy twojej prawdziwej klasy kontekstu danych.

Obejściem tego problemu jest utworzenie funkcji Randomize w serwerze proxy i przesłanie do niej zapytania, które ma być losowe:

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

Oto jak użyjesz go w swoim kodzie:

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

Aby być kompletnym, oto jak zaimplementować to w FAKE datacontext (który używa w jednostkach pamięci):

public IOrderedQueryable<T> Randomize<T>(IQueryable<T> query)
{
    return query.OrderBy(x => Guid.NewGuid());
}
Dave de Jong
źródło