Fila aleatoria de Linq a Sql


112

¿Cuál es la mejor (y más rápida) forma de recuperar una fila aleatoria usando Linq to SQL cuando tengo una condición, por ejemplo, algún campo debe ser verdadero?


Tienes dos opciones para el pedido, comprueba las verdaderas condiciones. Si la condición verdadera ocurre en la mayoría de los elementos, simplemente tome un elemento aleatorio y luego pruebe y repita mientras sea falso. Si es raro, deje que la base de datos limite las opciones a la condición real y luego tome una al azar.
Rex Logan

1
Al igual que con muchas respuestas en este sitio, la segunda calificación es mucho mejor que la aceptada.
nikib3ro

Respuestas:


169

Puede hacer esto en la base de datos, utilizando una UDF falsa; en una clase parcial, agregue un método al contexto de datos:

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

Entonces solo order by ctx.Random(); esto hará un pedido aleatorio en el SQL-Server cortesía de NEWID(). es decir

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

Tenga en cuenta que esto solo es adecuado para mesas de tamaño pequeño a mediano; para tablas grandes, tendrá un impacto en el rendimiento en el servidor y será más eficiente encontrar el número de filas ( Count) y luego elegir una al azar ( Skip/First).


para el enfoque de conteo:

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 son 30k después del filtro, diría que no: no use este enfoque. Haz 2 viajes de ida y vuelta; 1 para obtener el recuento () y 1 para obtener una fila aleatoria ...
Marc Gravell

1
¿Qué pasa si quieres cinco (o "x") filas aleatorias? ¿Es mejor solo hacer seis viajes de ida y vuelta o hay una forma conveniente de implementarlo en un procedimiento almacenado?
Neal Stublen

2
@Neal S .: el orden por ctx.Random () podría mezclarse con Take (5); pero si está utilizando el enfoque Count (), espero que 6 viajes de ida y vuelta sean la opción más simple.
Marc Gravell

1
no olvide agregar una referencia a System.Data.Linq o el atributo System.Data.Linq.Mapping.Function no funcionará.
Jaguir

8
Sé que esto es antiguo, pero si está seleccionando muchas filas aleatorias de una tabla grande, vea esto: msdn.microsoft.com/en-us/library/cc441928.aspx No sé si hay un equivalente LINQ.
jwd

60

Otra muestra de Entity Framework:

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

Esto no funciona con LINQ to SQL. El OrderBysimplemente se está eliminando.


4
¿Ha perfilado esto y ha confirmado que funciona? En mis pruebas con LINQPad, la cláusula order by se elimina.
Jim Wooley

Esta es la mejor solución a este problema
reach4thelasers

8
Esto no funciona en LINQ to SQL ... tal vez funcione en Entity Framework 4 (sin confirmarlo). Solo puede usar .OrderBy con Guid si está ordenando una Lista ... con DB no funcionará.
nikib3ro

2
Solo para finalmente confirmar que esto funciona en EF4, es una gran opción en ese caso.
nikib3ro

1
¿Podría editar su respuesta y explicar por qué el orderBy con un nuevo Guid funciona? Buena respuesta por cierto :)
Jean-François Côté

32

EDITAR: Acabo de notar que esto es LINQ to SQL, no LINQ to Objects. Utilice el código de Marc para que la base de datos haga esto por usted. Dejé esta respuesta aquí como un posible punto de interés para LINQ to Objects.

Por extraño que parezca, en realidad no es necesario contarlo. Sin embargo, necesita buscar todos los elementos a menos que obtenga el recuento.

Lo que puede hacer es mantener la idea de un valor "actual" y el recuento actual. Cuando obtenga el siguiente valor, tome un número aleatorio y reemplace el "actual" con "nuevo" con una probabilidad de 1 / n donde n es el recuento.

Entonces, cuando lee el primer valor, siempre lo convierte en el valor "actual". Cuando lea el segundo valor, puede convertirlo en el valor actual (probabilidad 1/2). Cuando lea el tercer valor, es posible que convertirlo en el valor actual (probabilidad 1/3), etc. Cuando se haya quedado sin datos, el valor actual es aleatorio de todos los que leyó, con probabilidad uniforme.

Para aplicar eso con una condición, simplemente ignore todo lo que no cumpla con la condición. La forma más sencilla de hacerlo es considerar solo la secuencia de "coincidencia" para empezar, aplicando primero una cláusula Where.

Aquí hay una implementación rápida. Yo creo que está bien ...

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
Para su información, realicé una verificación rápida y esta función tiene una distribución de probabilidad uniforme (el recuento creciente es esencialmente el mismo mecanismo que el shuffle de Fisher-Yates, por lo que parece razonable que debería serlo).
Greg Beech

@Greg: Genial, gracias. Me pareció bien con una verificación rápida, pero es muy fácil obtener errores de uno en uno en un código como este. Prácticamente irrelevante para LINQ to SQL, por supuesto, pero útil de todos modos.
Jon Skeet

@JonSkeet, hola, se puede comprobar esto y quiero saber lo que me falta
shaijut

@TylerLaing: No, no está destinado a ser un descanso. En la primera iteración, currentserá siempre estar configurado en el primer elemento. En la segunda iteración, hay un cambio del 50% que se establecerá en el segundo elemento. En la tercera iteración, hay un 33% de posibilidades de que se establezca en el tercer elemento. Agregar una declaración de interrupción significaría que siempre saldría después de leer el primer elemento, por lo que no es aleatorio en absoluto.
Jon Skeet

@JonSkeet Doh! Leí mal su uso de count (por ejemplo, estaba pensando que este era el estilo de Fisher-Yates con un rango aleatorio como ni). Pero seleccionar el primer elemento en Fisher-Yates es elegir justamente cualquiera de los elementos. Sin embargo, eso requiere conocer el número total de elementos. Veo ahora que su solución es buena para un IEnumerable porque no se conoce el recuento total, y no hay necesidad de iterar sobre toda la fuente solo para obtener el recuento, para luego iterar nuevamente a algún índice elegido al azar. Más bien, esto se resuelve en una sola pasada, como dijiste: "es necesario recuperar todos los elementos a menos que obtengas el recuento".
Tyler Laing

19

Una forma de lograrlo de manera eficiente es agregar una columna a sus datos Shuffleque se completa con un int aleatorio (a medida que se crea cada registro).

La consulta parcial para acceder a la tabla en orden aleatorio es ...

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

Esto hace una operación XOR en la base de datos y ordena los resultados de ese XOR.

Ventajas: -

  1. Eficiente: SQL maneja el pedido, no es necesario buscar toda la tabla
  2. Repetible: (bueno para probar): puede usar la misma semilla aleatoria para generar el mismo orden aleatorio

Este es el enfoque utilizado por mi sistema de automatización del hogar para aleatorizar listas de reproducción. Recoge una nueva semilla cada día dando un orden constante durante el día (lo que permite una fácil pausa / reanudación) pero una nueva mirada a cada lista de reproducción cada nuevo día.


¿Cuál sería el efecto sobre la aleatoriedad si en lugar de agregar un campo int aleatorio solo usara un campo de identidad de incremento automático existente (la semilla obviamente permanecería aleatoria)? además, ¿es adecuado un valor semilla con un máximo igual al número de registros de la tabla o debería ser mayor?
Bryan

De acuerdo, esta es una gran respuesta que la OMI debería tener más votos a favor. Usé esto en una consulta de Entity Framework, y el operador XOR bit a bit ^ parece funcionar directamente, lo que hace que la condición sea un poco más limpia: result = result.OrderBy(s => s.Shuffle ^ seed);(es decir, no es necesario implementar el XOR a través de los operadores ~, & y |).
Steven Rands

7

si desea obtener, por ejemplo, var count = 16filas aleatorias de la tabla, puede escribir

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

aquí usé EF, y la tabla es un Dbset


1

Si el propósito de obtener filas aleatorias es el muestreo, he hablado muy brevemente aquí sobre un buen enfoque de Larson et al., El equipo de investigación de Microsoft, donde han desarrollado un marco de muestreo para Sql Server utilizando vistas materializadas. También hay un enlace al documento real.


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

Explicación: Al insertar el guid (que es aleatorio), el orden con orderby sería aleatorio.


Las guías no son "aleatorias", no son secuenciales. Hay una diferencia. En la práctica, es probable que no importe para algo tan trivial como esto.
Chris Marisic

0

Vine aquí preguntándome cómo obtener algunas páginas aleatorias de un pequeño número de ellas, para que cada usuario obtenga 3 páginas aleatorias diferentes.

Esta es mi solución final, trabajando consultando con LINQ contra una lista de páginas en Sharepoint 2010. Está en Visual Basic, lo siento: p

Dim Aleatorio As New Random()

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

Probablemente debería obtener algunos perfiles antes de consultar una gran cantidad de resultados, pero es perfecto para mi propósito.


0

Tengo una consulta de función aleatoria contra DataTables:

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

0

El siguiente ejemplo llamará a la fuente para recuperar un recuento y luego aplicará una expresión de omisión en la fuente con un número entre 0 y n. El segundo método aplicará orden utilizando el objeto aleatorio (que ordenará todo en la memoria) y seleccionará el número pasado a la llamada al 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);
    }
}

Alguna explicación estaría bien
Andrew Barber

Este código no es seguro para subprocesos y solo se puede usar en código de un solo subproceso (por lo tanto, no ASP.NET)
Chris Marisic

0

utilizo este método para tomar noticias aleatorias y funciona 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

Usar LINQ to SQL en LINQPad como se ven las declaraciones de C #

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

El SQL generado es

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

0

Si usa LINQPad , cambie al modo de programa C # y haga lo siguiente:

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

Seleccionar 2 filas al azar


0

Para agregar a la solución de Marc Gravell. Si no está trabajando con la clase de contexto de datos en sí (porque lo utiliza como proxy de alguna manera, por ejemplo, para falsificar el contexto de datos con fines de prueba), no puede usar la UDF definida directamente: no se compilará en SQL porque no la está usando en un subclase o clase parcial de su clase de contexto de datos reales.

Una solución para este problema es crear una función Randomize en su proxy, alimentándola con la consulta que desea que sea aleatoria:

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

Así es como lo usaría en su código:

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

Para ser completo, así es como implementar esto en el contexto de datos FALSO (que usa en entidades de memoria):

public IOrderedQueryable<T> Randomize<T>(IQueryable<T> query)
{
    return query.OrderBy(x => Guid.NewGuid());
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.