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();
答案 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值可以调整它。