Linq to Entities,随机顺序

41

如何以随机顺序返回匹配的实体?
只是为了明确,这是有关Entity Framework和LINQ to Entities的内容。

(代码未经测试)

IEnumerable<MyEntity> results = from en in context.MyEntity
                                where en.type == myTypeVar
                                orderby ?????
                                select en;

感谢

编辑:
我尝试将此添加到上下文中:

public Guid Random()
{
    return new Guid();
}

使用以下查询语句:

IEnumerable<MyEntity> results = from en in context.MyEntity
                                where en.type == myTypeVar
                                orderby context.Random()
                                select en;

但我收到了这个错误:

System.NotSupportedException: LINQ to Entities does not recognize the method 'System.Guid Random()' method, and this method cannot be translated into a store expression..

编辑(当前代码):

IEnumerable<MyEntity> results = (from en in context.MyEntity
                                 where en.type == myTypeVar
                                 orderby context.Random()
                                 select en).AsEnumerable();
12个回答

55

一种简单的方法是按 Guid.NewGuid() 排序,但这样排序发生在客户端。你可能能够说服EF在服务器端进行某些随机操作,但这不一定简单 - 并且使用 "order by random number" 方式 显然是有问题的

为了使排序发生在 .NET 端而不是在 EF 中,你需要使用 AsEnumerable:

IEnumerable<MyEntity> results = context.MyEntity
                                       .Where(en => en.type == myTypeVar)
                                       .AsEnumerable()
                                       .OrderBy(en => context.Random());

最好是将无序版本放在列表中,然后对其进行洗牌。

Random rnd = ...; // Assume a suitable Random instance
List<MyEntity> results = context.MyEntity
                                .Where(en => en.type == myTypeVar)
                                .ToList();

results.Shuffle(rnd); // Assuming an extension method on List<T>

洗牌比排序更高效,不管其他的。关于获取适当的Random实例的详细信息,请参阅我的随机性文章。Stack Overflow上有许多 Fisher-Yates 洗牌的实现。


1
也许在EF4中是新的,但你可以在数据库中做到这一点:https://dev59.com/7XRB5IYBdhLWcg3wV156#4120132 - Drew Noakes
按照不稳定的排名函数排序看起来不安全。根据底层排序算法的不同,可能会导致错误。有关详细信息,请参见此博客此问题中的错误案例。 - Frédéric
@Frederic:在LINQ to Objects中,OrderBy仅对每个元素评估一次关键投影。虽然这肯定不是我现在会使用的东西 - 我会进行编辑。 - Jon Skeet
好的,谢谢您提供这些信息。这个 bug 案例出现在 Linq-to-entities 而不是 Linq-to-objects 中。但由于当前 EF 支持 NewGuid,许多开发人员会删除 AsEnumerable 以便在数据库端进行排序。然后,在他们的查询中包含时,他们可能会在 EF 6 中获得重复的结果。 - Frédéric
@Frederic:好的,我会编辑第一句话来表明这一点。 - Jon Skeet
显示剩余9条评论

42

Jon的回答很有帮助,但实际上你可以使用Guid和Linq to Entities让数据库进行排序(至少在EF4中可以):

from e in MyEntities
orderby Guid.NewGuid()
select e

这将生成类似于以下SQL的内容:

SELECT
[Project1].[Id] AS [Id], 
[Project1].[Column1] AS [Column1]
FROM ( SELECT 
    NEWID() AS [C1],                     -- Guid created here
    [Extent1].[Id] AS [Id], 
    [Extent1].[Column1] AS [Column1],
    FROM [dbo].[MyEntities] AS [Extent1]
)  AS [Project1]
ORDER BY [Project1].[C1] ASC             -- Used for sorting here

在我的测试中,使用Take(10)对查询结果进行处理(转换为SQL中的TOP 10),针对一个拥有1,794,785行的表格,该查询的运行时间始终在0.42至0.46秒之间。我不知道SQL Server是否对此进行了任何优化,或者是否为该表中的每一行都生成了GUID。但无论哪种情况,这比将所有这些行带入到我的进程中并尝试在那里进行排序要快得多。


注意:如果您在LinqPad中运行此代码时出现问题,请参考以下链接:https://dev59.com/MXrZa4cB1Zd3GeqP9fmw#20953863 - Walter Stabosz
1
请查看这个问题,不幸的是它已经失效了。似乎OrderBy假定排名函数是稳定的,但在使用随机生成器时并非如此。Linq to entities将其转换为SQL查询,对于相同的实体可能会得到不同的排名(只要您的查询使用Include)。这会导致实体在结果列表中重复出现。 - Frédéric

30

简单的解决方案是创建一个数组(或者 List<T>),然后随机打乱它的索引。

编辑:

static IEnumerable<T> Randomize<T>(this IEnumerable<T> source) {
  var array = source.ToArray();
  // randomize indexes (several approaches are possible)
  return array;
}

编辑:就我个人而言,我认为Jon Skeet的答案更加优雅:

var results = from ... in ... where ... orderby Guid.NewGuid() select ...

当然,你可以使用随机数生成器代替Guid.NewGuid()


1
嗨toro,抱歉,我不知道在List<T>上使用哪种方法,请详细说明一下好吗?谢谢。 - NikolaiDante
1
没有一个框架方法可以做到这一点。我建议使用http://en.wikipedia.org/wiki/Fisher-Yates_shuffle。 - mqp
谢谢mquander,这正是我想要的。 - NikolaiDante
1
我想对数百万行执行此操作,因此无法实际将所有这些实体带入我的上下文并开始对它们进行排序。您可以在数据库中使用我下面发布的Jon答案的变体来完成此操作:https://dev59.com/7XRB5IYBdhLWcg3wV156#4120132 - Drew Noakes
1
按照不稳定的排名函数排序看起来不太安全。取决于底层排序算法,可能会导致错误。有关详细信息,请参见此博客此问题中的错误案例。 - Frédéric
我也不会这样做,因为洗牌是一个O(n)算法,而排序是一个O(n log n)的算法。 - Michael Damatov

4
NewGuid 的排序方法虽然可以在服务器端进行,但是在连接(或使用贪婪加载)的情况下会导致实体被复制。
关于此问题,请参见此问题
为了解决这个问题,您可以在服务器端计算一些唯一值上使用SQL checksum代替NewGuid,并通过在客户端计算出随机种子来使其随机化。请参见先前链接问题上的我的答案

2

2
这个解决方案可能会在服务器上执行,但它还要求您能够访问 SQL 服务器本身上的函数和视图设置。 - Jared

1
lolo_house有一个非常巧妙、简单和通用的解决方案。你只需要将代码放在一个单独的静态类中即可使其工作。
using System;
using System.Collections.Generic;
using System.Linq;

namespace SpanishDrills.Utilities
{
    public static class LinqHelper
    {
        public static IEnumerable<T> Randomize<T>(this IEnumerable<T> pCol)
        {
            List<T> lResultado = new List<T>();
            List<T> lLista = pCol.ToList();
            Random lRandom = new Random();
            int lintPos = 0;

            while (lLista.Count > 0)
            {
                lintPos = lRandom.Next(lLista.Count);
                lResultado.Add(lLista[lintPos]);
                lLista.RemoveAt(lintPos);
            }

            return lResultado;
        }
    }
}

然后使用代码只需执行以下操作:
var randomizeQuery = Query.Randomize();

如此简单!谢谢 lolo_house。

0

(从EF Code First:如何获取随机行转帖)

比较两个选项:


跳过(随机数量的行)

方法

private T getRandomEntity<T>(IGenericRepository<T> repo) where T : EntityWithPk<Guid> {
    var skip = (int)(rand.NextDouble() * repo.Items.Count());
    return repo.Items.OrderBy(o => o.ID).Skip(skip).Take(1).First();
}
  • 需要2个查询

生成的SQL

SELECT [GroupBy1].[A1] AS [C1]
FROM   (SELECT COUNT(1) AS [A1]
        FROM   [dbo].[People] AS [Extent1]) AS [GroupBy1];

SELECT TOP (1) [Extent1].[ID]            AS [ID],
               [Extent1].[Name]          AS [Name],
               [Extent1].[Age]           AS [Age],
               [Extent1].[FavoriteColor] AS [FavoriteColor]
FROM   (SELECT [Extent1].[ID]                                  AS [ID],
               [Extent1].[Name]                                AS [Name],
               [Extent1].[Age]                                 AS [Age],
               [Extent1].[FavoriteColor]                       AS [FavoriteColor],
               row_number() OVER (ORDER BY [Extent1].[ID] ASC) AS [row_number]
        FROM   [dbo].[People] AS [Extent1]) AS [Extent1]
WHERE  [Extent1].[row_number] > 15
ORDER  BY [Extent1].[ID] ASC;

Guid

方法

private T getRandomEntityInPlace<T>(IGenericRepository<T> repo) {
    return repo.Items.OrderBy(o => Guid.NewGuid()).First();
}

生成的 SQL

SELECT TOP (1) [Project1].[ID]            AS [ID],
               [Project1].[Name]          AS [Name],
               [Project1].[Age]           AS [Age],
               [Project1].[FavoriteColor] AS [FavoriteColor]
FROM   (SELECT NEWID()                   AS [C1],
               [Extent1].[ID]            AS [ID],
               [Extent1].[Name]          AS [Name],
               [Extent1].[Age]           AS [Age],
               [Extent1].[FavoriteColor] AS [FavoriteColor]
        FROM   [dbo].[People] AS [Extent1]) AS [Project1]
ORDER  BY [Project1].[C1] ASC

因此,在更新的 EF 中,您可以再次看到 NewGuid 被翻译成 SQL(由 @DrewNoakes https://dev59.com/7XRB5IYBdhLWcg3wV156#4120132 确认)。尽管两者都是“in-sql”方法,但我猜 Guid 版本更快?如果您不必按顺序排序以跳过它们,并且您可以合理地猜测要跳过的数量,那么也许 Skip 方法会更好。


0
这是一种不错的方法(主要适用于谷歌搜索的人)。
你还可以在末尾添加 .Take(n) 来仅获取指定数量的结果。
model.CreateQuery<MyEntity>(   
    @"select value source.entity  
      from (select entity, SqlServer.NewID() as rand  
            from Products as entity 
            where entity.type == myTypeVar) as source  
            order by source.rand");

0

Toro的回答是我会采用的,但是更像这样:

static IEnumerable<T> Randomize<T>(this IEnumerable<T> source)
{
  var list = source.ToList();
  var newList = new List<T>();

  while (source.Count > 0)
  {
     //choose random one and MOVE it from list to newList
  }

  return newList;
}

没必要创建两个列表 - 你可以通过打乱的方式交换列表中的元素。这需要小心操作,但我认为这比毫无意义地创建另一个副本更好。 - Jon Skeet
你可以这样做,但会使代码变得不太易读。在我看来,另一种方式更好,因为更加清晰明了。请记住,我们主要操作的是引用而非值,所以除了列表本身外,几乎没有什么内存开销。 - Migol

0

我认为最好不要向类添加属性,最好使用位置:

public static IEnumerable<T> Randomize<T>(this IEnumerable<T> pCol)
    {
        List<T> lResultado = new List<T>();
        List<T> lLista = pCol.ToList();
        Random lRandom = new Random();
        int lintPos = 0;

        while (lLista.Count > 0)
        {
            lintPos = lRandom.Next(lLista.Count);
            lResultado.Add(lLista[lintPos]);
            lLista.RemoveAt(lintPos);
        }

        return lResultado;
    }

而且调用将会(如 toList() 或 toArray()):

var result = IEnumerable.Where(..).Randomize();


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接