繁体   English   中英

具有多个级别的MS-Access SQL自连接

[英]MS-Access SQL Self Join with multiple levels

我有一个表“ People”,主键为“ PersonID”,字段为“ Supervisor”。 “ Supervisor”字段包含用于创建自我联接的“ PersonID”的外键。

我想创建一个sql查询,以返回所有以“ Me”(登录到数据库的PersonID)作为管理员的人,以及该列表上有人标记为管理员的任何人。 本质上,我想在命令链中列出提供的PersonID下面的任何人。

SQL在很多方面都很有用,但是分层数据是更大的挑战之一。 一些供应商提供了自定义扩展来解决此问题(例如Oracle的CONNECT语法或SQL Server的hierarchyid数据类型),但我们可能希望保留此标准SQL 1

您建模的模型称为“邻接表”-这非常简单明了,并且始终保持一致2 但是,正如您所发现的,这很麻烦查询,尤其是对于未知深度或子树,而不是从根节点查询。

因此,我们需要使用其他模型对此进行补充。 基本上,应该将3个其他模型与邻接表模型结合使用。

  • 套套
  • 物化路径
  • 祖先遍历关闭

为了深入研究它们,我们将使用以下图表: 员工图

在此讨论中,我们还假设这是一个简单的层次结构,没有循环。

Joe Celko的嵌套集。

基本上,您存储每个节点的“左”和“右”值,以指示其在树中的位置。 根节点的“左”将始终为1 ,“右”的<count of nodes * 2>将始终为1 用图更容易说明:

嵌套集

请注意,每个节点都分配有一对数字,一个代表“ Left”,另一个代表“ Right”。 利用这些信息,您可以进行一些逻辑推断。 查找所有子节点变得容易-在节点的“左”大于目标节点的“左”且相同节点的“右”小于目标节点的“右”的值中进行过滤。

该模型的最大缺点是,更改层次结构几乎总是需要更新整个树,这使得维护快速移动的图表非常困难。 如果您每年仅更新一次,则可以接受。

该模型的另一个问题是,如果需要多个层次结构,则在没有附加列来跟踪单独的层次结构的情况下,嵌套集将无法工作。

物化路径

您知道文件系统路径的工作原理,对吗? 这基本上是同一件事,除了我们将其存储在数据库3中 例如,物化路径的可能实现如下所示:

ID  Name      Path
1   Alice     1/
2   Bob       1/2/
3   Christina 1/3/
4   Dwayne    1/4/
5   Erin      1/2/5/
6   Frank     1/2/6/
7   Georgia   1/2/7/
8   Harry     1/2/7/8/
9   Isabella  1/3/9/
10  Jake      1/3/10/
11  Kirby     1/3/10/11/
12  Lana      1/3/12/
13  Mike      1/4/13/
14  Norma     1/4/13/14/
15  Opus      1/4/15/
16  Rianna    1/4/16/

这很直观,只要您编写SQL查询以使用WHERE Path LIKE '1/4/*'类的谓词,就可以执行OK。 引擎将能够使用path列上的索引。 请注意,如果您的查询涉及查询树的中部或自下而上,则意味着无法使用索引,并且性能会受到影响。 但是,针对物化路径进行编程非常容易理解。 更新树的一部分不会作为嵌套集传播到无关的节点,因此这对它也是有利的。

最大的缺点是,要建立索引,文本必须是一小段。 对于Access数据库,该数据库对您的path字段设置了255个字符的限制。 更糟糕的是,没有什么好方法可以预测何时将要达到极限,因为树太深或树太宽(例如,较大的数字占用太多的空间),您可能会达到极限。 因此,大树可能需要一些硬编码的限制来避免这种情况。

祖先遍历关闭

该模型涉及一个单独的表,该表在雇员表更新时进行更新。 我们列举了两个节点之间的所有祖先,而不是仅记录直接关系。 为了说明这一点,表的外观如下:

员工表:

ID  Name
1   Alice
2   Bob
3   Christina
4   Dwayne
5   Erin
6   Frank
7   Georgia
8   Harry
9   Isabella
10  Jake
11  Kirby
12  Lana
13  Mike
14  Norma
15  Opus
16  Rianna

员工祖先表:

Origin  Ancestor
1        1
2        1
2        2
3        1
3        3
4        1
4        4
5        1
5        2
5        5
6        1
6        2
6        6
7        1
7        2
7        7
8        1
8        2
8        7
8        8
9        1
9        3
9        9
10       1
10       3
10       10
11       1
11       3
11       10
11       11
12       1
12       3
12       12
13       1
13       4
14       1
14       4
14       13
14       14
15       1
15       4
15       15
16       1
16       4
16       16

如您所见,我们在两个节点之间生成了价值几行的所有可能关系。 作为一张表的额外奖励,我们可以使用外键和级联删除来帮助保持一致。 但是,我们仍然必须手动管理插入和更新。 由于表也很窄,因此创建查询很容易,该查询可以利用键,源和祖先上的索引来查找子树,子级和父级。 这是最灵活的系统,但以维护方面的额外复杂性为代价。

维护模型

讨论的所有3个模型基本上都对数据进行了一定程度的归一化,以简化查询并支持任意深度搜索。 结果是,当雇员表以某种方式修改后,我们必须手动管理更改。

最简单的方法就是简单地编写一个VBA过程,该过程将使用您喜欢的模型截断并重新构建整个图表。 当图表较小或不经常更改时,这可以很好地工作。

在另一方面,您可以考虑使用雇员表上的数据宏来执行将更新传播到层次结构所需的维护。 需要注意的是,如果使用数据宏,则将数据移植到另一个RDBMS系统更加困难,因为这些都不支持数据宏。 (为公平起见,如果您从SQL Server的存储过程/触发器移植到Oracle的存储过程/触发器,问题仍然存在,因为在供应商方言中,移植非常困难,因为移植是一个挑战。) 使用数据宏或触发器+存储过程意味着您可以依靠引擎为您维护层次结构,而无需在表单中进行任何编程。

常见的诱惑是使用表单的AfterUpdate事件来维护更改,并且该更改将起作用....除非有人在表单外部对其进行更新。 因此,我实际上更希望我们使用数据宏,而不是依赖于每个人都始终使用表单。

请注意,在所有这些讨论,我们应该放弃邻接表模型。 正如我之前评论的那样,这是对层次结构进行建模的最标准化和一致的方法。 实际上,用它创建一个荒谬的等级是不可能的。 仅出于这个原因,您应该将其保留为“权威性事实”,然后可以在其上构建模型以帮助提高查询性能。

继续使用邻接列表模型的另一个很好的理由是,不管您使用的是哪种模型,它们都会引入其​​他列或其他表,这些表或表并不打算由用户直接编辑,而其目的在某种程度上等同于计算字段,因此应该不修补。 如果只允许用户编辑SupervisorID字段,则围绕该字段编码数据宏/触发器/ VBA过程变得很容易,并更新其他字段/表的“计算”以确保查询的正确性,具体取决于在这样的模型上。


1. SQL Standard确实描述了一种创建递归查询的方法。 但是,该特定功能的合规性似乎很差。 此外,性能可能不会那么好。 (在SQL Server的特定实现中就是这种情况)在大多数RDBMS中,很容易实现所讨论的3个模型,并且可以轻松编写和移植用于查询层次结构的查询。 但是,自动管理层次结构更改的实现始终需要使用特定于供应商的方言,使用触发器或存储过程不是很方便。

2.当我说一致时,我仅表示该模型无法创建无意义的输出。 仍然有可能提供错误的数据并建立一个怪异的层次结构,例如员工的主管向员工报告,但不会给出不确定的结果。 但是,它仍然是一个层次结构(即使最终以循环图的形式出现)。 对于其他模型,无法正确维护派生数据意味着查询将开始返回未定义的结果。

3. SQL Server的hierarchyid数据类型实际上是此模型的实现。

因为您可能只有有限的数量(例如6个)级别的深度,所以您可以将查询与带有子查询的子查询一起使用 ...等等。非常简单。

对于无限数量的级别,我发现的最快方法是创建一个查找功能,该功能可以遍历每条记录。 这可以输出记录的级别,也可以输出由记录的键和上面所有键构成的复合键。

由于查找函数将为每个调用使用相同的记录集,因此可以使其变为静态,并且(对于JET)可以通过使用Seek查找记录来进一步改善。

这是一个示例,它将为您提供一个想法:

Public Function RecursiveLookup(ByVal lngID As Long) As String

  Static dbs      As Database
  Static tbl      As TableDef
  Static rst      As Recordset

  Dim lngLevel    As Long
  Dim strAccount  As String

  If dbs Is Nothing Then
    ' For testing only.
    ' Replace with OpenDatabase of backend database file.
    Set dbs = CurrentDb()
    Set tbl = dbs.TableDefs("tblAccount")
    Set rst = dbs.OpenRecordset(tbl.Name, dbOpenTable)
  End If

  With rst
    .Index = "PrimaryKey"
    While lngID > 0
      .Seek "=", lngID
      If Not .NoMatch Then
        lngLevel = lngLevel + 1
        lngID = !MasterAccountFK.Value
        If lngID > 0 Then
          strAccount = str(!AccountID) & strAccount
        End If
      Else
        lngID = 0
      End If
    Wend
    ' Leave recordset open.
    ' .Close
  End With

'  Don't terminate static objects.
'  Set rst = Nothing
'  Set tbl = Nothing
'  Set dbs = Nothing

'  Alternative expression for returning the level.
'  (Adjust vartype of return value of function.) '  RecursiveLookup = lngLevel ' As Long
  RecursiveLookup = strAccount

End Function

这假定一个表具有一个主键ID和一个指向父记录的外(主)键-以及一个可见键(AccountID)为0的顶级记录(未使用)。

现在,使用这样的查询几乎可以立即很好地显示您的树,其中Account将是可见的复合键:

  SELECT
    *, RecursiveLookup([ID]) AS Account
  FROM
    tblAccount
  WHERE
    (AccountID > 0)
  ORDER BY
    RecursiveLookup([ID]);

如果希望使用此方法将记录添加到另一个表中,则不要为每个表进行SQL调用,因为这非常慢,但是请先打开一个记录集,然后使用AddNew-Update追加每个记录,最后关闭此记录记录。

考虑以下功能集:

Function BuildQuerySQL(lngsid As Long) As String
    Dim intlvl As Integer
    Dim strsel As String: strsel = selsql(intlvl)
    Dim strfrm As String: strfrm = "people as p0 "
    Dim strwhr As String: strwhr = "where p0.supervisor = " & lngsid

    While HasRecordsP(strsel & strfrm & strwhr)
        intlvl = intlvl + 1
        BuildQuerySQL = BuildQuerySQL & " union " & strsel & strfrm & strwhr
        strsel = selsql(intlvl)
        If intlvl > 1 Then
            strfrm = "(" & strfrm & ")" & frmsql(intlvl)
        Else
            strfrm = strfrm & frmsql(intlvl)
        End If
    Wend
    BuildQuerySQL = Mid(BuildQuerySQL, 8)
End Function

Function HasRecordsP(strSQL As String) As Boolean
    Dim dbs As DAO.Database
    Set dbs = CurrentDb
    With dbs.OpenRecordset(strSQL)
        HasRecordsP = Not .EOF
        .Close
    End With
    Set dbs = Nothing
End Function

Function selsql(intlvl As Integer) As String
    selsql = "select p" & intlvl & ".personid from "
End Function

Function frmsql(intlvl As Integer) As String
    frmsql = " inner join people as p" & intlvl & " on p" & intlvl - 1 & ".personid = p" & intlvl & ".supervisor "
End Function

这里, BuildQuerySQL功能可以与被提供PersonID对应于Supervisor和该函数将返回“递归”的SQL代码用于适当的查询以得到PersonID对于主管的所有下属。

因此,可以评估该函数以构造一个保存的查询,例如,对于PersonID = 5的主管,创建一个名为Subordinates的查询:

Sub test()
    CurrentDb.CreateQueryDef "Subordinates", BuildQuerySQL(5)
End Sub

或者根据您的应用程序的要求,对SQL进行评估以打开结果的RecordSet。

请注意,该函数构造一个UNION查询,并将每个嵌套级别与上一个查询结合在一起。

在考虑了此处介绍的选项之后,我决定以错误的方式进行此操作。 我在“人”表“ PermissionsLevel”中添加了一个字段,该字段是从另一个具有简单“ PermissionNumber”和“ PermissionDescription”的表中查找的。 然后,我在Form_load()事件中为登录用户的权限级别使用一个选择大小写。

Select Case userPermissionLevel

    Case Creator
        'Queries everyone in the database

    Case Administrator    
        'Queries everyone in the "Department" they are a member of

    Case Supervisor
        'Queries all people WHERE supervisor = userID OR _
            supervisor IN (Select PersonID From People WHERE supervisor = userID)

    Case Custodian '(Person in charge of maintaining the HAZMAT Cabinet and SDS)
        'Queries WHERE supervisor = DLookup("Supervisor", "People", "PersonID = " & userID)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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