在Access中找到一个随机的记录(真正的随机)

3
我想做的是每次程序打开时,表单上的图像都不同。因此,我有一个简单的带有2个列ID和ImagePath的表格,如何创建代码以选择随机记录(ImagePath),在表单加载事件或类似事件中?Rnd不好用,因为每次重新打开数据库时都会是相同的图像。
谢谢!

1
如果rnd每次打开数据库都返回相同的值,那么你可能在错误地使用它。你是如何使用它的? - Heinzi
只是一个简单的查询.... SELECT TOP 1 Images.ID, Images.Path, Rnd([ID]) AS Ran FROM Images ORDER BY Rnd([ID]); - Ben
5个回答

3

在第一次调用Rnd之前,尝试调用一次Randomize。正如Rnd的帮助主题所说,“在调用Rnd之前,使用不带参数的Randomize语句,以基于系统计时器的种子初始化随机数生成器。”


不知为何,我在这方面遇到了困难,所以我创建了一个模块:Public Function acbGetRandom(varFld As Variant)Randomize acbGetRandom = RndEnd Function然后我将该函数添加到我的查询中,使用:RandomID: acbGetRandom([ID]) 在字段中。希望这能帮助到某些人! - Ben
为了使Rnd使用的“种子”随机化,只需要调用一次Randomize。例如,您可以通过AutoExec宏或启动表单调用它。另一种方法是在生成伪随机数序列的开头调用它,就像ray023的答案中所示。至于您添加的“varFld”参数(但未使用)到函数中,这是一种强制函数在查询中每个记录中被评估(至少)一次的方法。但是,在像ray023的方法中不需要这样做,因为该函数在查询之外被调用。 - Brian Camire

2

“Rnd”不好吗?

Option Compare Database
Option Explicit

Sub Test()
    Randomize
    Dim x As Integer
    'Print the first field of a 100 random records
    For x = 0 To 100
        CallRandomRecord
    Next x
End Sub

Sub CallRandomRecord()
    Dim rs As DAO.Recordset
    Dim recordCount As Long
    Dim randomRecord As Long

    Set rs = CurrentDb.OpenRecordset("SELECT * FROM MyTable")
    rs.MoveLast 'To get the count
    rs.MoveFirst
    recordCount = rs.recordCount - 1
    randomRecord = CLng((recordCount) * Rnd)

     rs.Move randomRecord

     Debug.Print "Random Record No:" & randomRecord & "  Field 1:  " & rs.Fields(0)

End Sub

1
这是迄今为止发布的表现最佳的答案。请查看我的答案以获取基准测试结果。 - mwolfe02

2
我写了几个函数来返回一个随机记录,然后与这里提供的其他解决方案一起计时。我的两个函数都击败了Harkins方法,但是它们都无法接近@ray023的(稍作修改以进行基准测试)解决方案。@ray023的解决方案也可以说是最简单的。Susan Harkins看到这个就知难而退吧!
以下是代码。您可以将其复制并粘贴到标准模块中以在数据上进行测试。您只需要更改 TimeThem 模块顶部的三个常量即可:
Private Declare Function GetTickCount Lib "kernel32" () As Long

Sub TimeThem()
Const Loops As Integer = 10
Const TblName As String = "Batches"
Const FldName As String = "BatchName"
Const IndexFld As String = "BatchID"
Dim i As Integer, s As Long, dummy As Variant

    s = GetTickCount
    For i = 1 To Loops
        dummy = HarkinsRandom(TblName, FldName)
    Next i
    Debug.Print "Harkins:"; GetTickCount - s

    s = GetTickCount
    For i = 1 To Loops
        dummy = RandomRecord(TblName, FldName)
    Next i
    Debug.Print "RandomRecord:"; GetTickCount - s

    s = GetTickCount
    For i = 1 To Loops
        dummy = RandomRecordWithIndex(TblName, FldName, IndexFld)
    Next i
    Debug.Print "WithIndex:"; GetTickCount - s

    s = GetTickCount
    For i = 1 To Loops
        dummy = CallRandomRecord(TblName, FldName)
    Next i
    Debug.Print "CallRandom:"; GetTickCount - s


End Sub

Function HarkinsRandom(TblName As String, FldName As String)
    Dim rs As DAO.Recordset
    Set rs = CurrentDb.OpenRecordset(" SELECT TOP 1 " & FldName & _
                                     " FROM " & TblName & _
                                     " ORDER BY GetRandomValue(" & FldName & ")", _
                                     dbOpenForwardOnly)
    HarkinsRandom = rs(0)
End Function

Public Function GetRandomValue(fld As Variant)
  Randomize
  GetRandomValue = Rnd(1)
End Function

Function RandomRecord(TblName As String, FldName As String)
Dim NumRecs As Long, RecNum As Long
Dim SQL As String, SubSQL As String, rs As DAO.Recordset
Dim IndexFld As String

    Randomize
    NumRecs = CurrentDb.OpenRecordset("SELECT Count(*) FROM " & TblName, dbOpenForwardOnly)(0)
    RecNum = Int(Rnd() * NumRecs + 1)
    SQL = " SELECT TOP 1 " & FldName & _
          " FROM (" & _
          "  SELECT TOP " & RecNum & " " & FldName & " " & _
          "  FROM " & TblName & _
          "  ORDER BY " & FldName & ")" & _
          " ORDER BY " & FldName & " DESC"
    Set rs = CurrentDb.OpenRecordset(SQL, dbOpenForwardOnly)
    RandomRecord = rs(0)
End Function

Function RandomRecordWithIndex(TblName As String, FldName As String, _
                               Optional IndexedFieldName As String)
Dim NumRecs As Long, RecNum As Long
Dim SQL As String, SubSQL As String, rs As DAO.Recordset
Dim IndexFld As String

    Randomize
    NumRecs = CurrentDb.OpenRecordset("SELECT Count(*) FROM " & TblName, dbOpenForwardOnly)(0)
    RecNum = Int(Rnd() * NumRecs + 1)
    If Len(IndexedFieldName) = 0 Or IndexedFieldName = FldName Then
        SQL = " SELECT TOP 1 " & FldName & _
              " FROM (" & _
              "  SELECT TOP " & RecNum & " " & FldName & " " & _
              "  FROM " & TblName & _
              "  ORDER BY " & FldName & ")" & _
              " ORDER BY " & FldName & " DESC"
    Else
        SQL = " SELECT TOP 1 " & FldName & _
              " FROM (" & _
              "  SELECT TOP " & RecNum & " " & FldName & ", " & IndexedFieldName & _
              "  FROM " & TblName & _
              "  ORDER BY " & IndexedFieldName & ")" & _
              " ORDER BY " & IndexedFieldName & " DESC"

    End If
    Set rs = CurrentDb.OpenRecordset(SQL, dbOpenForwardOnly)
    RandomRecordWithIndex = rs(0)
End Function

Function CallRandomRecord(TblName As String, FldName As String)
    Dim rs As DAO.Recordset
    Dim recordCount As Long
    Dim RandomRecord As Long

    Set rs = CurrentDb.OpenRecordset("SELECT " & FldName & " FROM " & TblName)
    rs.MoveLast 'To get the count
    rs.MoveFirst
    recordCount = rs.recordCount - 1
    RandomRecord = CLng((recordCount) * Rnd)

     rs.Move RandomRecord

    CallRandomRecord = rs(0)
'     Debug.Print "Random Record No:" & randomRecord & "  Field 1:  " & rs.Fields(0)

End Function

以下是针对一张约有50,000条记录的表格进行测试的结果(该表格是本地连接的Jet表格,即在我运行测试的同一台计算机上的.mdb文件中):

Harkins: 4461 
RandomRecord: 2528 
WithIndex: 1918 
CallRandom: 172 

Harkins: 4150 
RandomRecord: 2278 
WithIndex: 2043 
CallRandom: 47 

CallRandom: 63 
WithIndex: 2090 
RandomRecord: 2324 
Harkins: 4197 

CallRandom: 46 
WithIndex: 1997 
RandomRecord: 2169 
Harkins: 4150 

我运行了四次,第一次后颠倒顺序以考虑潜在的缓存优势。如您所见,我的两个函数的运行速度大约是Harkins解决方案的两倍,但@ray023的解决方案最慢时快了25倍(最快时快了近100倍)。

但无论如何,请根据自己的数据进行基准测试。


1

请参考Susan Harkins在TechRepublic上的这篇文章:http://www.techrepublic.com/blog/howdoi/how-do-i-retrieve-a-random-set-of-records-in-microsoft-access/149

我在这个查询中使用了她的GetRandomValue函数,它每次返回不同的记录。

SELECT TOP 1 f.id, GetRandomValue(f.id) AS rnd_value
FROM tblFoo AS f
ORDER BY 2;

这个函数:

Public Function GetRandomValue(fld As Variant)

  Randomize

  GetRandomValue = Rnd(1)

End Function

注意:这种方法需要对表中的每一行运行一个函数。对于小到中等大小的表可能还能容忍,但是不应该在非常大的表上使用。


对于相对较小的表,这个方法可以很好地工作。如果性能是一个问题或者表格更大(10,000+条记录),请查看@ray023的解决方案。 - mwolfe02
实际上,如果你仔细看基准测试,你会发现我只调用了每个函数10次。字符串连接可能占据了这4000多毫秒中的4或5个,但其余的更多与在表的每一行上调用VBA函数有关。这就是问题所在。 - mwolfe02
我倾向于认同这不是他使用情况的问题。我假设他有一个带有几十个图像文件名的表格,性能应该是完全可以接受的。我的原始评论确实是为了未来可能需要检索随机记录但具有完全不同用例的读者而撰写的。 - mwolfe02
@mwolfe02 我在我的回答中添加了那个警告。我放弃了关于连接的评论;那是我没有看到树木而看不到森林。一旦你提到“对表中每行执行函数”,森林就对我可见了。 :-) - HansUp

0
我可能太简单,无法理解这个问题,但是在我看来,如果您想检索单个随机图像,那么您需要做的就是生成一个单一的随机数字,以某种方式键入您可用的图像表中。 如果有100张图片可供选择,则需要从1到100获取随机数字。
因此,您要生成该数字:
  Round(100 * Rnd(), 0)

...然后使用它来检索图像。如果图像表具有自动编号的 PK,您可以直接使用它,这将非常快速。如果您的图像位于子窗体中,您可以将 LinkMaster 设置为字面 PK 值,这将为您检索图像。

关于 Randomize(),我似乎无法在即时窗口中调用 Rnd() 时重复它,所以我不确定是否需要它。

但对我来说,这似乎是一个非常简单的操作,可能不需要任何 SQL 或记录集的使用。如果您选择记录集路线,我建议仅打开一次并保持其持久性,然后每次需要时导航它,而不是每次需要新图像时重复打开它。但如果我在做这件事,我会尽可能简单地为自己铺平道路,并选择使用自动编号 PK 路线来处理图像。如果您想在 SQL 中执行此操作,则为:

  SELECT Images.ID, Images.Path
  FROM Images 
  WHERE Images.ID = Round(100 * Rnd(), 0)

显然,您需要将100更改为适当的数字。如果您需要Randomize(),则用调用Randomize()并返回Round(100 * Rnd(), 0)的函数替换直接的Round(100 * Rnd(), 0)

但也许我错过了一些重要的细节,这使得它比我所认为的要复杂得多。


自动编号主键序列中由于记录删除和插入失败而产生的间隙怎么办?如果 Round(100 * Rnd(), 0) 的计算结果为57,但是没有ID = 57的行... - HansUp
你需要处理它。也就是说,如果没有记录返回,你将得到下一个随机数。如果你正在追求性能最大化,那么你会想要确保主键字段中没有间隙。也就是说,如果你要以这种方式使用数据,你需要确保数据符合某些要求,以尽可能高效地工作。虽然这违反了无意义主键的通常原则,但在这种情况下我对此没有问题,因为你赋予了它一定的含义。 - David-W-Fenton
你在评论中写的都是真的。我的评论是针对你回答中的最后一句话而来的。这种方法并不像你的答案所暗示的那么简单。 - HansUp
我不知道。如果我有一堆想要随机排列的图像,我不明白将“业务规则”编码到数据存储中会比没有它更复杂。对我来说,获得的效率值得在代理主键字段中使用非标准的含义编码。 - David-W-Fenton
我更加不喜欢给自动编号的主键赋予含义。如果没有其他方法,我可能会这样做,但这里不适用。我建议的方法可以适用于较小的记录源。对于大型记录源,我不会使用你的方法或我的方法。相反,我会像@ray023建议的那样做些其他事情。 - HansUp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接