Ligne aléatoire de Linq à Sql


112

Quelle est la meilleure (et la plus rapide) façon de récupérer une ligne aléatoire en utilisant Linq to SQL lorsque j'ai une condition, par exemple un champ doit être vrai?


Vous avez deux options pour la commande dont vous vérifiez les vraies conditions. Si la condition vraie se produit sur la plupart des éléments, saisissez simplement un élément aléatoire, puis testez et répétez tout en étant faux. Si rare, laissez la base de données limiter les options à la condition vraie, puis en saisir une au hasard.
Rex Logan

1
Comme pour beaucoup de réponses sur ce site, la deuxième cote est bien meilleure que celle acceptée.
nikib3ro

Réponses:


169

Vous pouvez le faire dans la base de données, en utilisant un faux UDF; dans une classe partielle, ajoutez une méthode au contexte de données:

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

Alors juste order by ctx.Random(); cela fera un ordre aléatoire sur le serveur SQL avec la permission de NEWID(). c'est à dire

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

Notez que cela ne convient que pour les tables de petite à moyenne taille; pour les tables volumineuses, cela aura un impact sur les performances du serveur, et il sera plus efficace de trouver le nombre de lignes ( Count), puis d'en choisir une au hasard ( Skip/First).


pour l'approche de comptage:

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
Si c'est 30k après le filtre, je dirais non: n'utilisez pas cette approche. Faites 2 allers-retours; 1 pour obtenir le Count (), et 1 pour obtenir une ligne aléatoire ...
Marc Gravell

1
Que faire si vous voulez cinq (ou "x") lignes aléatoires? Est-il préférable d'effectuer simplement six allers-retours ou existe-t-il un moyen pratique de l'implémenter dans une procédure stockée?
Neal Stublen le

2
@Neal S.: la commande par ctx.Random () pourrait être mélangée avec Take (5); mais si vous utilisez l'approche Count (), je suppose que 6 allers-retours sont l'option la plus simple.
Marc Gravell

1
n'oubliez pas d'ajouter une référence à System.Data.Linq ou l'attribut System.Data.Linq.Mapping.Function ne fonctionnera pas.
Jaguir

8
Je sais que c'est vieux, mais si vous sélectionnez plusieurs lignes aléatoires dans une grande table, voyez ceci: msdn.microsoft.com/en-us/library/cc441928.aspx Je ne sais pas s'il existe un équivalent LINQ.
jwd

60

Un autre exemple pour Entity Framework:

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

Cela ne fonctionne pas avec LINQ to SQL. Le OrderByest simplement abandonné.


4
Avez-vous profilé cela et confirmé que cela fonctionne? Dans mes tests utilisant LINQPad, la clause order by est supprimée.
Jim Wooley

C'est la meilleure solution à ce problème
reach4thelasers

8
Cela ne fonctionne pas dans LINQ to SQL ... peut-être que cela fonctionne dans Entity Framework 4 (sans le confirmer). Vous ne pouvez utiliser .OrderBy avec Guid que si vous triez une liste ... avec DB cela ne fonctionnera pas.
nikib3ro

2
Juste pour confirmer enfin que cela fonctionne dans EF4 - c'est une excellente option dans ce cas.
nikib3ro

1
Pourriez-vous modifier votre réponse et expliquer pourquoi la commande avec un nouveau Guid fait l'affaire? Bonne réponse au fait :)
Jean-François Côté

32

EDIT: Je viens de remarquer que c'est LINQ to SQL, pas LINQ to Objects. Utilisez le code de Marc pour obtenir la base de données pour faire cela pour vous. J'ai laissé cette réponse ici comme un point d'intérêt potentiel pour LINQ to Objects.

Curieusement, vous n'avez pas besoin d'obtenir le décompte. Cependant, vous devez récupérer chaque élément à moins que vous n'obteniez le nombre.

Ce que vous pouvez faire, c'est conserver l'idée d'une valeur «actuelle» et du décompte actuel. Lorsque vous récupérez la valeur suivante, prenez un nombre aléatoire et remplacez le "courant" par "nouveau" avec une probabilité de 1 / n où n est le nombre.

Ainsi, lorsque vous lisez la première valeur, vous en faites toujours la valeur "courante". Lorsque vous lisez la deuxième valeur, vous pouvez en faire la valeur actuelle (probabilité 1/2). Lorsque vous lisez la troisième valeur, vous pouvez faire la valeur actuelle (probabilité 1/3), etc. Lorsque vous êtes à court de données, la valeur actuelle est aléatoire parmi toutes celles que vous lisez, avec une probabilité uniforme.

Pour appliquer cela avec une condition, ignorez simplement tout ce qui ne remplit pas la condition. La manière la plus simple de le faire est de ne considérer que la séquence «correspondante» pour commencer, en appliquant d'abord une clause Where.

Voici une mise en œuvre rapide. Je pense que ça va ...

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
FYI - J'ai effectué une vérification rapide et cette fonction a une distribution de probabilité uniforme (le décompte incrémentiel est essentiellement le même mécanisme que le mélange Fisher-Yates, il semble donc raisonnable qu'il devrait l'être).
Greg Beech

@Greg: Cool, merci. Cela m'a semblé correct avec une vérification rapide, mais il est si facile d'obtenir des erreurs une par une dans un code comme celui-ci. Quasiment sans rapport avec LINQ to SQL bien sûr, mais néanmoins utile.
Jon Skeet

@JonSkeet, salut, pouvez-vous vérifier ceci et me faire savoir ce qui me manque
shaijut

@TylerLaing: Non, il n'y a pas de pause. Lors de la première itération, currentsera toujours défini sur le premier élément. Lors de la deuxième itération, il y a un changement de 50% selon lequel il sera défini sur le deuxième élément. À la troisième itération, il y a 33% de chances qu'il soit défini sur le troisième élément. L'ajout d'une instruction break signifierait que vous quitteriez toujours après avoir lu le premier élément, ce qui ne le rendrait pas du tout aléatoire.
Jon Skeet

@JonSkeet Doh! J'ai mal lu votre utilisation de count (par exemple, je pensais que c'était du style Fisher-Yates avec une plage aléatoire comme ni). Mais pour sélectionner le premier élément dans Fisher-Yates, c'est choisir équitablement l'un des éléments. Cependant, cela nécessite de connaître le nombre total d'éléments. Je vois maintenant que votre solution est soignée pour un IEnumerable en ce que le nombre total n'est pas connu, et il n'est pas nécessaire d'itérer sur toute la source juste pour obtenir le nombre, puis itérer à nouveau vers un index choisi au hasard. Au contraire, cela résout en un seul passage, comme vous l'avez dit: "besoin de récupérer chaque élément à moins que vous n'obteniez le décompte".
Tyler Laing

19

Un moyen efficace consiste à ajouter une colonne à vos données Shufflequi est remplie avec un int aléatoire (lorsque chaque enregistrement est créé).

La requête partielle pour accéder à la table dans un ordre aléatoire est ...

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

Cela effectue une opération XOR dans la base de données et trie les résultats de ce XOR.

Avantages: -

  1. Efficace: SQL gère la commande, pas besoin de récupérer toute la table
  2. Répétable: (bon pour les tests) - peut utiliser la même graine aléatoire pour générer le même ordre aléatoire

C'est l'approche utilisée par mon système domotique pour randomiser les listes de lecture. Il sélectionne une nouvelle graine chaque jour, donnant un ordre cohérent au cours de la journée (permettant des capacités de pause / reprise faciles), mais un regard neuf sur chaque playlist chaque nouveau jour.


quel serait l'effet sur le caractère aléatoire si, au lieu d'ajouter un champ int aléatoire, vous utilisiez simplement un champ d'identité auto-incrémenté existant (la graine resterait évidemment aléatoire)? aussi - une valeur de départ avec un maximum égal au nombre d'enregistrements dans le tableau est-elle adéquate ou devrait-elle être plus élevée?
Bryan

D'accord, c'est une excellente réponse que l'OMI devrait avoir plus de votes positifs. J'ai utilisé ceci dans une requête Entity Framework, et l'opérateur bitwise-XOR ^ semble fonctionner directement, rendant ainsi la condition un peu plus propre: result = result.OrderBy(s => s.Shuffle ^ seed);(c'est-à-dire pas besoin d'implémenter le XOR via les opérateurs ~, & et |).
Steven Rands

7

si vous voulez obtenir par exemple var count = 16des lignes aléatoires de la table, vous pouvez écrire

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

ici j'ai utilisé EF, et la table est un Dbset


1

Si le but d'obtenir des lignes aléatoires est l'échantillonnage, j'ai parlé très brièvement ici d'une belle approche de Larson et al., Équipe Microsoft Research où ils ont développé un cadre d'échantillonnage pour Sql Server en utilisant des vues matérialisées. Il existe également un lien vers le document actuel.


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

Explication: En insérant le guid (qui est aléatoire), l'ordre avec orderby serait aléatoire.


Les guides ne sont pas "aléatoires", ils ne sont pas séquentiels. Il existe une différence. En pratique, cela n'a probablement pas d'importance pour quelque chose de trivial comme celui-ci.
Chris Marisic

0

Je suis venu ici en me demandant comment obtenir quelques pages aléatoires à partir d'un petit nombre d'entre elles, afin que chaque utilisateur obtienne 3 pages aléatoires différentes.

Ceci est ma solution finale, travaillant avec des requêtes avec LINQ sur une liste de pages dans Sharepoint 2010. C'est dans Visual Basic, désolé: p

Dim Aleatorio As New Random()

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

Devrait probablement obtenir un profilage avant d'interroger un grand nombre de résultats, mais c'est parfait pour mon objectif


0

J'ai une requête de fonction aléatoire contre DataTables:

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

0

L'exemple ci-dessous appellera la source pour récupérer un décompte, puis appliquera une expression de saut sur la source avec un nombre compris entre 0 et n. La deuxième méthode appliquera l'ordre en utilisant l'objet aléatoire (qui ordonnera tout en mémoire) et sélectionnera le numéro passé dans l'appel de méthode.

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

Une explication serait bien
Andrew Barber

Ce code n'est pas threadsafe et ne peut être utilisé que dans du code à thread unique (donc pas ASP.NET)
Chris Marisic

0

J'utilise cette méthode pour prendre des nouvelles aléatoires et ça fonctionne bien;)

    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

Utilisation de LINQ to SQL dans LINQPad en tant qu'instruction C #

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

Le SQL généré est

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

0

Si vous utilisez LINQPad , passez en mode programme C # et procédez comme suit :

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

Sélectionnez 2 lignes aléatoires


0

A ajouter à la solution de Marc Gravell. Si vous ne travaillez pas avec la classe datacontext elle-même (parce que vous la procurez par proxy, par exemple pour simuler le datacontext à des fins de test), vous ne pouvez pas utiliser directement l'UDF défini: il ne sera pas compilé en SQL car vous ne l'utilisez pas dans un sous-classe ou classe partielle de votre classe de contexte de données réel.

Une solution de contournement à ce problème consiste à créer une fonction Randomize dans votre proxy, en l'alimentant avec la requête que vous souhaitez randomiser:

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

Voici comment vous l'utiliseriez dans votre code:

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

Pour être complet, voici comment implémenter cela dans le contexte de données FAKE (qui utilise en mémoire des entités):

public IOrderedQueryable<T> Randomize<T>(IQueryable<T> query)
{
    return query.OrderBy(x => Guid.NewGuid());
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.