Linq2sql:获取随机元素的有效方法?

时间:2009-11-24 10:14:33

标签: c# linq-to-sql random

Byt假设我有一个整数权重,即权重为10的元素的选择概率比权重为1的元素高10倍。

var ws = db.WorkTypes
.Where(e => e.HumanId != null && e.SeoPriority != 0)
.OrderBy(e => /*????*/ * e.SeoPriority)
.Select(e => new
{
   DescriptionText = e.DescriptionText,
   HumanId = e.HumanId
})
.Take(take).ToArray();

当我需要对结果进行加权时,如何解决在Linq中获取随机记录的问题?

我需要像Random Weighted Choice in T-SQL这样的东西,但是在linq中,不仅要获得一条记录吗?

如果我没有加权要求,我会使用NEWID方法,我可以采用这种方式吗?

partial class DataContext
{
    [Function(Name = "NEWID", IsComposable = true)]
    public Guid Random()
    {
        throw new NotImplementedException();
    }
}

...

var ws = db.WorkTypes
.Where(e => e.HumanId != null && e.SeoPriority != 0)
.OrderBy(e => db.Random())
.Select(e => new
{
   DescriptionText = e.DescriptionText,
   HumanId = e.HumanId
})
.Take(take).ToArray();

6 个答案:

答案 0 :(得分:4)

我的第一个想法与Ron Klein的想法相同 - 创建一个加权列表并从中随机选择。

这是一个LINQ扩展方法,用于从普通列表创建加权列表,给定一个知道对象权重属性的lambda函数。

如果您没有立即获得所有仿制药,请不要担心......下面的用法应该更清楚:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    public class Item
    {
        public int Weight { get; set; }
        public string Name { get; set; }
    }

    public static class Extensions
    {
        public static IEnumerable<T> Weighted<T>(this IEnumerable<T> list, Func<T, int> weight)
        {
            foreach (T t in list)
                for (int i = 0; i < weight(t); i++)
                    yield return t;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Item> list = new List<Item>();
            list.Add(new Item { Name = "one", Weight = 5 });
            list.Add(new Item { Name = "two", Weight = 1 });

            Random rand = new Random(0);

            list = list.Weighted<Item>(x => x.Weight).ToList();

            for (int i = 0; i < 20; i++)
            {
                int index = rand.Next(list.Count());
                Console.WriteLine(list.ElementAt(index).Name);
            }

            Console.ReadLine();
        }
    }
}

从输出中可以看出,结果是随机的,并且可以根据需要加权。

答案 1 :(得分:2)

我假设重量是整数。这是一种连接到虚拟表以增加每个权重的行数的方法;首先,让我们在TSQL上证明:

SET NOCOUNT ON
--DROP TABLE [index]
--DROP TABLE seo
CREATE TABLE [index] ([key] int not null) -- names for fun ;-p
CREATE TABLE seo (url varchar(10) not null, [weight] int not null)

INSERT [index] values(1) INSERT [index] values(2)
INSERT [index] values(3) INSERT [index] values(4)
INSERT [index] values(5) INSERT [index] values(6)
INSERT [index] values(7) INSERT [index] values(8)
INSERT [index] values(9) INSERT [index] values(10)

INSERT [seo] VALUES ('abc',1)  INSERT [seo] VALUES ('def',2)
INSERT [seo] VALUES ('ghi',1)  INSERT [seo] VALUES ('jkl',3)
INSERT [seo] VALUES ('mno',1)  INSERT [seo] VALUES ('mno',1)
INSERT [seo] VALUES ('pqr',2)

DECLARE @count int, @url varchar(10)
SET @count = 0
DECLARE @check_rand TABLE (url varchar(10) not null)

-- test it lots of times to check distribution roughly matches weights
WHILE @count < 11000
BEGIN
    SET @count = @count + 1

    SELECT TOP 1 @url = [seo].[url]
    FROM [seo]
    INNER JOIN [index] ON [index].[key] <= [seo].[weight]
    ORDER BY NEWID()

    -- this to check distribution
    INSERT @check_rand VALUES (@url)
END

SELECT ISNULL(url, '(total)') AS [url], COUNT(1) AS [hits]
FROM @check_rand
GROUP BY url WITH ROLLUP
ORDER BY url

输出如下内容:

url        hits
---------- -----------
(total)    11000
abc        1030
def        1970
ghi        1027
jkl        2972
mno        2014
pqr        1987

显示我们的整体分布正确。现在让我们把它带入LINQ-to-SQL;我已将两个表添加到数据上下文中(您需要创建类似[index]表的内容来执行此操作) - 我的DBML:

  <Table Name="dbo.[index]" Member="indexes">
    <Type Name="index">
      <Column Name="[key]" Member="key" Type="System.Int32" DbType="Int NOT NULL" CanBeNull="false" />
    </Type>
  </Table>
  <Table Name="dbo.seo" Member="seos">
    <Type Name="seo">
      <Column Name="url" Type="System.String" DbType="VarChar(10) NOT NULL" CanBeNull="false" />
      <Column Name="weight" Type="System.Int32" DbType="Int NOT NULL" CanBeNull="false" />
    </Type>
  </Table>

现在我们将消耗它;在数据上下文的partial class中,在添加中向Random方法添加编译查询(用于提高性能):

partial class MyDataContextDataContext
{
    [Function(Name = "NEWID", IsComposable = true)]
    public Guid Random()
    {
        throw new NotImplementedException();
    }
    public string GetRandomUrl()
    {
        return randomUrl(this);
    }
    static readonly Func<MyDataContextDataContext, string>
        randomUrl = CompiledQuery.Compile(
        (MyDataContextDataContext ctx) =>
                 (from s in ctx.seos
                 from i in ctx.indexes
                 where i.key <= s.weight
                 orderby ctx.Random()
                 select s.url).First());
}

这个LINQ-to-SQL查询非常类似于我们编写的TSQL的关键部分;让我们测试一下:

using (var ctx = CreateContext()) {
    // show sample query
    ctx.Log = Console.Out;
    Console.WriteLine(ctx.GetRandomUrl());
    ctx.Log = null;

    // check distribution
    var counts = new Dictionary<string, int>();
    for (int i = 0; i < 11000; i++) // obviously a bit slower than inside db
    {
        if (i % 100 == 1) Console.WriteLine(i); // show progress
        string s = ctx.GetRandomUrl();
        int count;
        if (counts.TryGetValue(s, out count)) {
            counts[s] = count + 1;
        } else {
            counts[s] = 1;
        }
    }
    Console.WriteLine("(total)\t{0}", counts.Sum(p => p.Value));
    foreach (var pair in counts.OrderBy(p => p.Key)) {
        Console.WriteLine("{0}\t{1}", pair.Key, pair.Value);
    }
}

这运行一次查询以显示TSQL是否合适,然后(像之前一样)11k次检查分发;输出(不包括进度更新):

SELECT TOP (1) [t0].[url]
FROM [dbo].[seo] AS [t0], [dbo].[index] AS [t1]
WHERE [t1].[key] <= [t0].[weight]
ORDER BY NEWID()
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926

看起来一点也不差 - 它有表格和范围条件,以及TOP 1,所以它做的非常相似;数据:

(total) 11000
abc     939
def     1893
ghi     1003
jkl     3104
mno     2048
pqr     2013

再说一遍,我们得到了正确的发行版,全部来自LINQ-to-SQL。排序

答案 2 :(得分:1)

从问题看来,您建议的解决方案与Linq / Linq2Sql绑定。

如果我理解正确,你的主要目标是从数据库获取最多X个记录,其权重大于0.如果数据库拥有超过X个记录,你想从中选择使用记录的重量,并随机得出结果。

如果到目前为止一切正确,我的解决方案是按重量克隆每条记录:如果记录的重量是5,请确保你有5次。这样随机选择考虑了重量。

然而,克隆记录会造成重复。所以你不能只记录X记录,你应该记录越来越多的记录,直到你有X个不同的记录。

到目前为止,我描述了一个与实现无关的通用解决方案。

我认为使用 Linq2Sql实施我的解决方案更难。如果数据库中的总记录数量不是很大,我建议读取整个表并在SQL Server外部进行克隆和随机。

如果总计数 很大,我建议你采取随机选择的100,000条记录(或更少)(通过Linq2Sql),并应用上述实现。我相信这是随机的。

答案 3 :(得分:1)

尝试使用RAND()sql函数 - 它会给你一个0到1的浮点数。

缺点是我不确定它是否会在sql服务器端引起全表扫描,即如果在sql上生成的查询+执行将以这样的方式进行优化:一旦你有前n条记录它就会忽略表的其余部分。

答案 4 :(得分:1)

var rand = new Random();

var ws = db.WorkTypes
.Where(e => e.HumanId != null && e.SeoPriority != 0)
.OrderByDescending(e => rand.Next() * e.SeoPriority)
.Select(e => new
{
   DescriptionText = e.DescriptionText,
   HumanId = e.HumanId
})
.Take(take).ToArray();

答案 5 :(得分:0)

在您正在查看的SQL示例中使用GUID(NEWID)函数的原因只是SQL Server RAND函数仅为每个语句计算一次。因此在随机选择中没用。

但是当你使用linq时,一个快速而肮脏的解决方案是创建一个Random对象并用语句替换你的订单。

随机rand = new Random(DateTime.Now.Millisecond);

var ws = db.WorkTypes     .Where(e =&gt; e.HumanId!= null&amp;&amp; e.SeoPriority!= 0)      .OrderByDescending(e =&gt; rand.Next(10)* e.SeoPriority)     .Select(e =&gt; new {DescriptionText = e.DescriptionText,HumanId = e.HumanId})     。取(取).ToArray();

rand.Next(10)假设您的SeoPriority从0到10缩放。

它不是100%精确,但它很接近,调整Next值可以调整它。