views:

6586

answers:

4

What is the best (and fastest) way to retreive a random row using Linq to SQL when I have a condition, e.g. some field must be true ?

A: 

Why make it random? What about

foo.Where(p => p.test == condition).First();

If you really want random, you can write an extension method that will return a random row where the random number is constrained between 0 and the number of elements - 1

Steve
I'm building an image rating system (much like HotOrNot) in asp.net and I want to display a random image (otherwise, the users will get bored really quick^^)
Julien Poulin
well, then the extension method should work just fine.
Steve
+6  A: 

EDIT: I've only just noticed this is LINQ to SQL, not LINQ to Objects. Use Marc's code to get the database to do this for you. I've left this answer here as a potential point of interest for LINQ to Objects.

Strangely enough, you don't actually need to get the count. You do, however, need to fetch every element unless you get the count.

What you can do is keep the idea of a "current" value and the current count. When you fetch the next value, take a random number and replace the "current" with "new" with a probability of 1/n where n is the count.

So when you read the first value, you always make that the "current" value. When you read the second value, you might make that the current value (probability 1/2). When you read the third value, you might make that the current value (probability 1/3) etc. When you've run out of data, the current value is a random one out of all the ones you read, with uniform probability.

To apply that with a condition, just ignore anything which doesn't meet the condition. The easiest way to do that is to only consider the "matching" sequence to start with, by applying a Where clause first.

Here's a quick implementation. I think it's okay...

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
FYI - I ran a quick check and this function does have a uniform probability distribution (the incrementing count is essentially the same mechanism as the Fisher-Yates shuffle so it seems reasonable that it should be).
Greg Beech
@Greg: Cool, thanks. It looked okay to me with a quick check, but it's so easy to get off-by-one errors in code like this. Virtually irrelevant to LINQ to SQL of course, but useful nonetheless.
Jon Skeet
+39  A: 

You can do this at the database, by using a fake UDF; in a partial class, add a method to the data context:

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

Then just order by ctx.Random(); this will do a random ordering at the SQL-Server courtesy of NEWID(). i.e.

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

Note that this is only suitable for small-to-mid-size tables; for huge tables, it will have a performance impact at the server, and it will be more efficient to find the number of rows (Count), then pick one at random (Skip/First).


for count approach:

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
My table has approximately 30.000 rows, would you say it's small-to-mid size?
Julien Poulin
If is it 30k *after* the filter, I'd say no: don't use this approach. Do 2 round-trips; 1 to get the Count(), and 1 to get a random row...
Marc Gravell
+1 This is a much better approach than what I had said. I nominated mine for deletion.
Reed Copsey
Thank you. Based on your comment, il use the 2 round-trip version.
Julien Poulin
What if you want five (or "x") random rows? Is it best just to make six round-trips or is there a convenient way to implement it in a stored procedure?
Neal S.
@Neal S.: the order by ctx.Random() could be mixed with Take(5); but if you are using the Count() approach, I expect 6 round trips is the simplest option.
Marc Gravell
don't forget to add a reference to System.Data.Linq or the System.Data.Linq.Mapping.Function attribute wont work.
Jared
+1  A: 

One way to achieve efficiently is to add a column to your data Shuffle that is populated with a random int (as each record is created).

The partial query to access the table in random order is ...

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

This does an XOR operation in the database and orders by the results of that XOR.

Advantages:-

  1. Efficient: SQL handles the ordering, no need to fetch the whole table
  2. Repeatable: (good for testing) - can use the same random seed to generate the same random order

This is the approach used by my home automation system to randomize playlists. It picks a new seed each day giving a consistent order during the day (allowing easy pause / resume capabilities) but a fresh look at each playlist each new day.

Hightechrider