我有一个表“ People”,主键为“ PersonID”,字段为“ Supervisor”。 “ Supervisor”字段包含用于创建自我联接的“ PersonID”的外键。
我想创建一个sql查询,该查询返回所有以“ Me”(登录到数据库的PersonID)作为管理员的人,以及该列表上标记为管理员的任何人。本质上,我想在命令链中列出提供的PersonID下面的任何人。
答案 0 :(得分:3)
SQL在很多方面都很有用,但是分层数据是更大的挑战之一。一些供应商提供了自定义扩展来解决此问题(例如Oracle的CONNECT
语法或SQL Server的hierarchyid
数据类型),但我们可能希望保留此标准SQL 1 。
您建模的内容称为“邻接表”-这非常简单明了,并且始终保持一致。 2 。但是,正如您所发现的,这很麻烦查询,尤其是对于未知深度或子树,而不是从根节点开始查询。
因此,我们需要使用其他模型对此进行补充。基本上,应该将3种其他模型与邻接表模型结合使用。
在此讨论中,我们还假设这是一个简单的层次结构,没有循环。
Joe Celko的嵌套集。
基本上,您存储每个节点的“左”和“右”值,以指示其在树中的位置。根节点将始终具有1
的“左”和<count of nodes * 2>
的“右”。用图更容易说明:
请注意,为每个节点分配了一对数字,一个代表“ 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过程,该过程将使用您喜欢的模型截断并重新构建整个图表。当图表较小或不经常更改时,这可以很好地工作。
另一方面,您可以考虑在employee表上使用 Data Macros 来执行将更新传播到层次结构所需的维护。需要注意的是,如果使用数据宏,则将数据移植到另一个RDBMS系统更加困难,因为这些都不支持数据宏。 (为公平起见,如果您从SQL Server的存储过程/触发器移植到Oracle的存储过程/触发器,问题仍然存在,因为在供应商方言中,移植非常困难,因为移植是一个挑战。)使用数据宏或trigger +存储过程意味着您可以依靠引擎来维护层次结构,而无需对表单进行任何编程。
一个常见的诱惑是使用表单的AfterUpdate
事件来维护更改,并且该更改将起作用。...除非有人在表单外部对其进行更新。因此,我实际上更希望我们使用数据宏,而不是依赖于每个人都始终使用表单。
请注意,在所有讨论中,我们应该不丢弃邻接列表模型。正如我之前评论的那样,这是对层次结构进行建模的最标准化和一致的方法。实际上,用它创建一个荒谬的等级是不可能的。仅出于这个原因,您应该将其保留为“权威性事实”,然后可以在其上构建模型以帮助提高查询性能。
继续使用邻接表模型的另一个很好的理由是,不管您使用的是哪种模型,它们都会引入附加的列或附加的表,这些表或表并不打算由用户直接编辑,但其目的在某种程度上等同于计算字段因此不应该修改。如果只允许用户编辑SupervisorID
字段,则围绕该字段编码数据宏/触发器/ VBA过程变得很容易,并更新其他字段/表的“计算”以确保正确性取决于此类模型的查询。
1。 SQL Standard确实描述了创建递归查询的方法。但是,该特定功能的合规性似乎很差。此外,性能可能不会那么好。 (在SQL Server的特定实现中就是这种情况)在大多数RDBMS中,很容易实现所讨论的3个模型,并且可以轻松编写和移植用于查询层次结构的查询。但是,自动管理层次结构更改的实现始终需要使用特定于供应商的方言,使用触发器或存储过程不是很方便。
2。当我说“一致”时,我仅表示该模型无法创建无意义的输出。仍然有可能提供错误的数据并建立一个怪异的层次结构,例如员工的主管向员工报告,但不会给出不确定的结果。但是,它仍然是一个层次结构(即使最终以循环图的形式出现)。对于其他模型,如果无法正确维护派生数据,则意味着查询将开始返回未定义的结果。
3。 SQL Server的hierarchyid
数据类型实际上是此模型的实现。
答案 1 :(得分:1)
由于您可能会有一个有限的计数,比如说六层,所以您可以对子查询和子查询使用查询 ...等。非常简单
对于无限个级别,我发现的最快方法是创建一个查找功能,为每个记录遍历树。这可以输出记录的级别,也可以输出由记录的键和上面所有键组成的复合键。
由于查找函数将为每个调用使用相同的记录集,因此可以将其设置为静态,并且(对于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的顶级记录(未使用)。
现在,使用这样的查询几乎可以立即很好地显示您的树,其中“帐户”是可见的复合键:
SELECT
*, RecursiveLookup([ID]) AS Account
FROM
tblAccount
WHERE
(AccountID > 0)
ORDER BY
RecursiveLookup([ID]);
如果希望使用此方法将记录添加到另一个表中,则不要为每个表进行SQL调用,因为这非常慢,但请先打开一个记录集,然后使用 AddNew-Update 追加每个记录,最后关闭该记录集。
答案 2 :(得分:0)
请考虑以下功能:
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代码以获得{{1 }}用于主管的所有下属。
因此可以评估该函数以构造保存的查询,例如对于具有PersonID
的主管,创建一个名为PersonID = 5
的查询:
Subordinates
或者根据您的应用程序的要求,也许可以对SQL进行评估以打开结果的RecordSet。
请注意,该函数将构造一个Sub test()
CurrentDb.CreateQueryDef "Subordinates", BuildQuerySQL(5)
End Sub
查询,并将每个嵌套级别与上一个查询结合在一起。
答案 3 :(得分:0)
在考虑了此处介绍的选项之后,我决定我将以错误的方式进行操作。我在“人”表“ 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)