[英]Optimization of a T-SQL Query with probability for selection
我有一个条目的表,应该从中选择两个条目。 某些条目被选中的概率应该高于其他条目的概率。
目前我用UNION ALL
解决了这个问题,因此我选择一次所有条目,然后再次选择应该具有更高概率的条目。 从这个合并的表中,我选择在ORDER BY NEWID()
调用后与TOP 2
两个条目混合。
SELECT TOP 2 EMail
FROM (
SELECT EMail
FROM dbo.Benutzer
UNION ALL
SELECT EMail
FROM dbo.Benutzer
WHERE param1 = 1
UNION ALL
SELECT EMail
FROM dbo.Benutzer
WHERE param2 = 1
) AS EMail
ORDER BY NEWID();
示例表:
EMail param1 param2
______________|_________|___________
Test@test.com |0 | 0 -> probability is 1 (normal)
Test1@test.com|1 | 0 -> probability is 2 (higher than 1)
Test2@test.com|1 | 0 -> probability is 2 (higher than 1)
Test3@test.com|1 | 1 -> probability is 3 (higher than 1 and 2)
Test4@test.com|1 | 0 -> probability is 2 (higher than 1)
Test5@test.com|1 | 0 -> probability is 2 (higher than 1)
因此,如果我从该表中进行选择并在此之前对其进行洗牌,那么每个新选择的不同数据都会出现,概率应该基于该表。 那么这个条目出现的可能性有多大。
总是两个不同的用户应该出来。 例如Test3 和Test4。 但是,也测试 unf Test3 所以它应该只是一个概率。
但是,此查询未执行。 如何以高性能的方式解决这个问题?
更新的答案:
似乎您想以更高的概率随机选择每封电子邮件的一行。 在这种情况下,您需要对每个不同的电子邮件的行进行编号:
SELECT *
INTO Benutzer
FROM (VALUES
('Test@test.com', 0, 0),
('Test1@test.com', 1, 0),
('Test2@test.com', 1, 0),
('Test3@test.com', 1, 1),
('Test4@test.com', 1, 0),
('Test5@test.com', 1, 0)
) v (EMail, param1, param2)
SELECT TOP 2 Email
FROM (
SELECT b.*, ROW_NUMBER() OVER (PARTITION BY b.Email ORDER BY a.probability DESC) AS rn
FROM Benutzer b
CROSS APPLY (VALUES
(1), -- normal probability
(
1 +
CASE WHEN param1 = 1 THEN 1 ELSE 0 END +
CASE WHEN param2 = 1 THEN 1 ELSE 0 END
) -- calculated probability
) a (probability)
) t
WHERE t.rn = 1
ORDER BY NEWID()
最后,您可以尝试对行进行随机编号:
ROW_NUMBER() OVER (PARTITION BY b.Email ORDER BY NEWID()) AS rn
原答案:
如果我正确理解了这个问题,您只需要根据所需条件计算每一行的概率:
SELECT TOP 2 EMail
FROM (VALUES
('Test@test.com', 0, 0),
('Test1@test.com', 1, 0),
('Test2@test.com', 1, 0),
('Test3@test.com', 1, 1),
('Test4@test.com', 1, 0),
('Test5@test.com', 1, 0)
) Benutzer (EMail, param1, param2)
ORDER BY
(
1 +
CASE WHEN param1 = 1 THEN 1 ELSE 0 END +
CASE WHEN param2 = 1 THEN 1 ELSE 0 END
) DESC,
NEWID()
一个重要的说明(基于@GordonLinoff 的评论) - 如果NEWID()
是一阶表达式,则忽略二阶表达式,因此我将NEWID()
作为二阶表达式(这是答案的第一个版本)。 但是,在这种情况下,行不会随机返回。
您可以尝试使用计数表和 ROW_NUMBER() 函数。 Tally 表用于根据给定的概率为每行创建正确数量的重复行。 ROW_NUMBER() 函数根据 NewId() 的排序顺序创建行号 - 这是 RN,另一个基于电子邮件地址中的排序 - 这是 RNK。
我们想选择由 RN 排序的 2 行,但使用 RNK = 1 以确保我们不会收到重复项。
MS SQL Server 2017 架构设置:
CREATE TABLE Benutzer
(
Email VARCHAR(30),
Param1 Int,
Param2 Int
);
INSERT INTO Benutzer
VALUES
('Test@test.com', 0, 0), -- probability 1
('Test1@test.com', 1, 0), -- probability 2
('Test2@test.com', 1, 0), -- probability 2
('Test3@test.com', 1, 1), -- probability 3
('Test4@test.com', 1, 0), -- probability 2
('Test5@test.com', 1, 0) -- probability 2
查询 1 :
WITH Tally As
(
SELECT *
FROM (VALUES
(1),
(2),
(3)
) Tally (Num)
),
Results AS
(
SELECT Email, ROW_NUMBER() OVER (ORDER By NewId()) AS RN,
ROW_NUMBER() OVER (PArtition BY Email ORDER BY PAram1) As Rnk
FROM Benutzer
INNER JOIN Tally
ON TAlly.Num <= 1 + Param1 + Param2
)
SELECT TOP 2 Email
FROM Results
WHERE Results.Rnk =1
ORDER BY RN
结果:
| Email |
|----------------|
| Test2@test.com |
| Test5@test.com |
考虑到概率。 使用递归,因为下一步取决于前一步的结果,如果它指向相同的范围,则忽略新的随机索引。
with weighted as (
-- weight = f (param1, param2) , using 1 + param1 + param2 here
select Email, param1, param2, count(*) * (1 + param1 + param2) n
from Benutzer
group by Email, param1, param2
), ranges as (
-- a range width = weight
select Email, sum(n) over(order by Email)- n + 1 n1, sum(n) over(order by Email) n2, sum(n) over() sn
from weighted
), h as (
select 1 level, t2.*, ABS(CHECKSUM(NewId())) r
-- , @rnd % t2.sn + 1 k
from ranges t2
where (@rnd % t2.sn + 1) between t2.n1 and t2.n2
union all
-- try next random index till the next row is a new one
select case when t2.Email = h.Email then level else level + 1 end
, t2.* , ABS(CHECKSUM(NewId()))
-- , r % t2.sn + 1
from ranges t2
join h on r % t2.sn + 1 between t2.n1 and t2.n2
where level <=1
)
-- take one row from every level
select top(1) with ties Email, @cnt
from h
order by row_number() over(partition by level order by newid());
见测试数据库<>小提琴
500 次运行统计:
Email n p
Test@test.com 68 6.800000000000
Test1@test.com 179 17.900000000000
Test2@test.com 159 15.900000000000
Test3@test.com 246 24.600000000000
Test4@test.com 180 18.000000000000
Test5@test.com 168 16.800000000000
不理想但接近。
主要问题是什么?
当使用ORDER BY NEWID()
方法时, ORDER BY
子句将导致结果集中的所有记录被排序,当基表有大量记录时,这可能是一项非常昂贵的操作(可能使用大量磁盘 I/O) .
我建议的解决方案:
如果我们像这样向基表添加一个 ID 列:
CREATE TABLE #T
(
ID int IDENTITY(1, 3) PRIMARY KEY,
Email varchar(100),
Param1 int,
Param2 int
);
然后我们可以使用这个查询来得到想要的结果(没有重复的值):
WITH RndRange AS
(
SELECT MIN(ID) RangeStart, MAX(ID) + 2 RangeEnd FROM #T
)
,RndSelector AS
(
SELECT NULL AS HitID, 0 AS HitCounter, CAST('' AS varchar(100)) AS HitIDList
UNION ALL
SELECT
NewHitID,
HitCounter + IIF(HitID IS NULL, 0, 1),
CAST(HitIDList + IIF(NewHitID IS NULL, '', CONCAT(NewHitID, ',')) AS varchar(100))
FROM
RndSelector
CROSS APPLY
(
SELECT ABS(CHECKSUM(NEWID()) % (RangeEnd - RangeStart + 1)) + RangeStart AS RndNum FROM RndRange
) C
OUTER APPLY
(
SELECT
ID AS NewHitID
FROM
#T
WHERE
(ID BETWEEN RndNum - 2 AND RndNum) --Added just for performance
AND
(RndNum BETWEEN ID AND ID + Param1 + Param2)
AND
ID NOT IN (SELECT Value FROM string_split(HitIDList, ','))
) O
WHERE
HitCounter + IIF(HitID IS NULL, 0, 1) < 2
)
SELECT * FROM #T WHERE ID IN (SELECT HitID FROM RndSelector)
它是如何工作的?
假设我们在表中有这些记录:
ID Email Param1 Param2
1 User1 1 1
4 User2 1 0
7 User3 0 0
递归执行的每一轮都会生成一个介于 1 和 9(最小 ID 和最大 ID + 2)之间的随机整数。 当随机数介于 1 和 3 之间时,则选择(命中)User1 的 ID。 如果随机数为 4 或 5,则选择 User2,当随机数为 6 时,则不选择任何内容,因此进入下一轮。 递归的执行将继续直到选择了 2 个不同的 ID (HitCounter = 2)。
并且在 HitIDList 的帮助下,不会再次选择重复的 ID。
如您所见,使用此解决方案不需要表排序。 我们只是生成一些随机数并找到它们的相关记录,其中 dbms 将使用索引扫描来查找记录。 由于减少了专门针对大表的 I/O 操作,因此我预计性能会得到显着提高。
答案更新:
我在查询中添加了一个条件ID BETWEEN RndNum - 2 AND RndNum
,现在它非常快。 具有 1000000 条记录的表的速度测试结果:
应用查询 | 测试1 | 测试2 | 测试3 |
---|---|---|---|
按 NEWID() 排序 | 477ms | 400ms | 510ms |
随机发生器 | 2ms | 3ms | 2ms |
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.