繁体   English   中英

使用选择概率优化 T-SQL 查询

[英]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 以确保我们不会收到重复项。

SQL小提琴

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM