【问题标题】:Use RAND() in User Defined Function在用户定义函数中使用 RAND()
【发布时间】:2015-07-17 05:20:16
【问题描述】:

我正在尝试创建一个用户定义的函数,该函数在其中调用系统 RAND() 函数,当我尝试创建该函数时,它会出错并显示以下消息:

消息 443,级别 16,状态 1,过程 getNumber,第 10 行
在函数中无效使用副作用运算符“rand”。

我的功能码:

CREATE FUNCTION getNumber(@_id int)
RETURNS DECIMAL(18,4)
AS
BEGIN
   DECLARE @RtnValue DECIMAL(18,4);

   SELECT TOP 1 @RtnValue = EmployeeID 
   FROM dbo.Employees
   ORDER BY EmployeeID DESC

   SET @RtnValue = RAND() * @RtnValue * (1/100)

   RETURN @RtnValue;
END

请问我该如何解决这个问题?

【问题讨论】:

  • 嗨,你取消了接受,这让我回到了这个问题,我刚刚提出了一个新的答案,它提供了一种不同的方法。

标签: sql-server sql-server-2012


【解决方案1】:

问题是您不能从用户定义的函数内部调用非确定性函数。

我通过创建一个视图来解决这个限制,在视图中调用该函数并在您的函数中使用该视图,就像这样......

查看定义

CREATE VIEW vw_getRANDValue
AS
SELECT RAND() AS Value

函数定义

ALTER FUNCTION getNumber(@_id int )
RETURNS DECIMAL(18,4)
AS
BEGIN
   DECLARE @RtnValue DECIMAL(18,4);
   SELECT TOP 1 @RtnValue = EmployeeID 
   FROM dbo.Employees
   ORDER BY EmployeeID DESC

   SET @RtnValue = (SELECT Value FROM vw_getRANDValue) * @RtnValue * (1.0000/100.0000) --<-- to make sure its not converted to int
    RETURN @RtnValue;
END

【讨论】:

  • 我刚刚发布了一个答案,您可能应该看看。它涉及对此 RAND 视图的多次调用...
  • 这与 RAND 的不确定性无关。这是因为它有副作用,这就是 UDF 中不允许使用它的原因
【解决方案2】:

只需从外部传递 RAND() 值作为参数:

CREATE FUNCTION getNumber(@_id int, @RAND FLOAT)
RETURNS DECIMAL(18,4)
AS
BEGIN
   DECLARE @RtnValue DECIMAL(18,4);

   SELECT TOP 1 @RtnValue = EmployeeID 
   FROM dbo.Employees
   ORDER BY EmployeeID DESC

   SET @RtnValue = @RAND * @RtnValue * (1/100)

   RETURN @RtnValue;
END

并调用getNumber(10, RAND())

没有任何副作用。

【讨论】:

  • 我认为这是一个干净的解决方案 :),但不可扩展,如果您需要多个 Rand Value 怎么办?
  • @DavidNoreña 然后传递几个 Rand 值作为参数。
【解决方案3】:

小心兰德!

如果您检查这一点,您会发现多次调用此 VIEW 都返回相同的值。这与 NEWID() 不同。因此,如果您真的想要随机数,最好采用 NEWID() 并做一些“技巧”来从 - 比如说 - 第一个字节中获取一个数字......

CREATE VIEW vw_getRANDValue
AS
SELECT RAND() AS Value
GO
CREATE VIEW vw_getNEWID
AS
SELECT NEWID() AS Value
GO
CREATE FUNCTION dbo.Test() 
RETURNS TABLE AS
RETURN
WITH Numbers AS
(SELECT 1 AS x UNION SELECT 2 UNION SELECT 3) 
SELECT *
     ,(SELECT Value FROM vw_getRANDValue) AS myRandom
     ,(SELECT Value FROM vw_getNEWID) AS myNewid
FROM Numbers
GO
SELECT * FROM dbo.Test();
GO
DROP FUNCTION dbo.Test;
GO
DROP VIEW vw_getRANDValue;
GO
DROP VIEW  vw_getNEWID;
GO      

这是一个结果:

  1. 0,684530884058892 D1809581-BBD1-4D23-A7F9-BC697E869BB0
  2. 0,684530884058892 A4BAECDE-E993-46C1-B571-7440A713C371
  3. 0,684530884058892 D7A1CB65-D2BC-41B2-990D-C3BC52B056A2

随机 BIGINT 的视图可能如下所示:

CREATE VIEW vw_getRandomBigInt
AS
SELECT CONVERT(BIGINT,CONVERT(VARBINARY(16),NEWID(),1)) * (-1) AS Value
GO

提示:我检查了很多行,似乎(仅凭视觉)这种方法并不是真正随机的(所有 BIGINT 都具有相同的宽度......)。这似乎工作正常:

CREATE VIEW vw_getRandomInt
AS
SELECT sys.fn_replvarbintoint(sys.fn_cdc_hexstrtobin(LEFT(REPLACE(CONVERT(VARCHAR(100),NEWID()),'-',''),4))) AS Value
GO

【讨论】:

    【解决方案4】:

    还有一个想法:使用该函数只是为您的业务逻辑进行计算并提交非确定性部分。在您的情况下,您似乎选择了一个介于零和最高员工 ID 之间的随机数(缺少 ID 怎么办?)

    如前所述,RAND() 很难使用。它将在多次调用中返回相同的值。因此我使用NEWID,将其转换为VARBINARY(8),并将其转换为BIGINT

    看看这个:

    此函数将采用GUID 并在给定边界之间缩放

    CREATE FUNCTION dbo.GetRandomNumber(@lowerLimit BIGINT, @upperLimit BIGINT, @GuidValue UNIQUEIDENTIFIER)
    RETURNS BIGINT
    AS
    BEGIN
        RETURN
        (
        SELECT ABS(CAST(CAST(@GuidValue AS VARBINARY(8)) AS BIGINT)) % (@upperLimit-@lowerLimit)+@lowerLimit
        )
    END
    GO
    

    --该表将被随机值填充

    CREATE TABLE testTable(RndNumber BIGINT,Tile INT);
    

    --CTE 创建超过 6 个 mio 虚拟行

    WITH manyRows AS
    (
        SELECT 1 AS nr FROM master..spt_values CROSS JOIN master..spt_values AS x
    )
    INSERT INTO testTable(RndNumber) 
    SELECT dbo.GetRandomNumber(-300,700,NEWID()) --<-- Here I pass in the non-deterministic part
    FROM manyRows;
    

    --现在表格被平铺成10个相等的片段

    WITH UpdateableCTE AS
    (
        SELECT Tile
              ,NTILE(10) OVER(ORDER BY RndNumber) AS tileValue
        FROM testTable
    )  
    UPDATE UpdateableCTE SET Tile=tileValue;
    

    --检查随机结果

    SELECT * FROM testTable
    ORDER BY Tile,RndNumber;
    

    --这清楚地表明,每个图块的覆盖范围几乎相同,这证明了相当好的随机分布

    SELECT Tile
          ,COUNT(*) CountOfNumbers
          ,MAX(RndNumber)-MIN(RndNumber) CoveredRange
    FROM testTable
    GROUP BY Tile
    ORDER BY Tile;
    GO
    

    --清理

    DROP TABLE dbo.testTable;
    DROP FUNCTION dbo.GetRandomNumber;
    

    结果

    T   Counts  min     max     CoveredRange
    1   636553  -300    -201      99
    2   636553  -201    -101     100
    3   636553  -101       0     101
    4   636553     0      99      99
    5   636553    99     199     100
    6   636553   199     299     100
    7   636553   299     399     100
    8   636553   399     499     100
    9   636553   499     599     100
    10  636552   599     699     100
    

    您可以看到,每个图块覆盖的元素数量大致相同。里面的元素几乎覆盖了相同的范围。这表明,数字在表格中分布均匀。

    【讨论】:

    • 需要加1,否则达不到上限。 (@upperLimit-@lowerLimit + 1) 使用 1 作为上限和 0 作为下限进行测试
    • @Matt 这是故意的......但你是对的,必须意识到这一点。这是 fence-pole-question(多少极?多少中间体?)我倾向于避免完美地达到目标?
    • 这是一种基于零和基于 1 的问题。或者是否要包括在这种情况下的限制。 BETWEEN,大于 = 等 :) 我实际上将使用你的技术来处理刚刚注意到的事情,因为我需要一些日期、位等。
    【解决方案5】:

    您不能在函数内部使用RAND 函数,而是可以创建Rand 函数的简单视图并在function 内部使用它。这只是一种解决方法

    查看:

    CREATE VIEW random_val_view
    AS
    SELECT RAND() as  random_value
    

    功能:

    CREATE FUNCTION getNumber(@_id int )
    RETURNS DECIMAL(18,4)
    AS
    BEGIN
       DECLARE @RtnValue DECIMAL(18,4);
       SELECT TOP 1 @RtnValue = EmployeeID 
       FROM dbo.Employees
       ORDER BY EmployeeID DESC
    
       SET @RtnValue = (select random_value from random_val_view) * @RtnValue * (1/100.0)
        RETURN @RtnValue;
    END
    

    【讨论】:

    • 此函数将始终返回0.000,因为返回值将隐式转换为INT
    • @M.Ali - 我确定不会,如果我错了,请展示一个演示
    • @Fireblade:我刚刚发布了一个答案,您可能应该看看。它涉及对此 RAND 视图的多次调用...
    • " 返回值将被隐式转换为 INT" 哦。这很微妙。你解决了我的问题。
    • "此函数将始终返回 0.000" - 我相信在这种情况下不会因为 100.0.虽然我不知道背后的理论,但根据我的经验,如果在计算中使用整数 (/100),SQL 服务器将返回整数,如果使用小数,则返回小数 (/100.0)。当然,CAST转十进制是最安全的方法。
    【解决方案6】:

    您可以使用这些脚本从视图(来自 SQL 2017)中获取随机数或字符:

    -- FLOAT BETWEEN 0 AND 1
    CREATE VIEW [dbo].[GetRandomFloat]
    AS
        SELECT  CAST('0.'+REPLACE(TRANSLATE(NEWID(), 'ABCDEF-', '#######'),'#','') AS FLOAT) AS RND
    GO
    
    -- INT FROM 0 TO 9
    CREATE VIEW [dbo].[GetRandomFrom0To9]
    AS
        SELECT  CAST(COALESCE(LEFT(REPLACE(TRANSLATE(NEWID(), 'ABCDEF-', '#######'), '#', ''), 1), '0') AS TINYINT) AS RND
    GO
    
    -- CHAR FROM 'A' TO 'Z'
    CREATE VIEW [dbo].[GetRandomFromAToZ]
    AS
        SELECT CHAR(CAST(ROUND(CAST('0.'+REPLACE(TRANSLATE(NEWID(), 'ABCDEF-', '#######'),'#','') AS FLOAT) * 25 + 65, 0) AS TINYINT)) AS RND
    GO
    

    选择:

    SELECT [RND] FROM [GetRandomFloat]
    SELECT [RND] FROM [GetRandomFrom0To9]
    SELECT [RND] FROM [GetRandomFromAToZ]
    

    【讨论】:

      猜你喜欢
      • 2016-11-19
      • 1970-01-01
      • 2018-04-30
      • 1970-01-01
      • 2022-09-23
      • 2023-03-11
      • 2013-12-19
      • 1970-01-01
      相关资源
      最近更新 更多