SQL 数据层次结构

Posted

技术标签:

【中文标题】SQL 数据层次结构【英文标题】:SQL Data Hierarchy 【发布时间】:2011-08-27 14:34:23 【问题描述】:

我浏览了一些 SQL 层次结构教程,但没有一个对我的应用程序有多大意义。也许我只是没有正确理解它们。我正在编写一个 C# ASP.NET 应用程序,我想从 SQL 数据创建一个树视图层次结构。

这就是层次结构的工作方式:

SQL 表 身份证 |位置 ID |姓名 _______| __________ |_____________ 第1331章第1331章屋 第1321章第1331章房间 第2141章第1321章床 第1251章第2231章健身房

如果 ID 和位置 ID 相同,这将确定***父级。该父级的任何子级都将具有与父级相同的位置 ID。该子项的任何孙子项的位置 ID 都与子项的 ID 相同,依此类推。

对于上面的例子:

- 屋 - 房间 - - 床

任何帮助或指导易于遵循的教程将不胜感激。

编辑:

到目前为止我的代码,但它只得到父母和孩子,没有孙子。我似乎无法弄清楚如何让它递归地获取所有节点。

using System;
using System.Data;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Configuration;
using System.Data.SqlClient;

namespace TreeViewProject

public partial class _Default : System.Web.UI.Page

    protected void Page_Load(object sender, EventArgs e)
    
        PopulateTree(SampleTreeView);

    



    public void PopulateTree(Control ctl)
    

        // Data Connection
        SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["AssetWhereConnectionString1"].ConnectionString);
        connection.Open();

        // SQL Commands
        string getLocations = "SELECT ID, LocationID, Name FROM dbo.Locations";
        SqlDataAdapter adapter = new SqlDataAdapter(getLocations, connection);
        DataTable locations = new DataTable();
        // Fill Data Table with SQL Locations Table
        adapter.Fill(locations);
        // Setup a row index
        DataRow[] myRows;
        myRows = locations.Select();

        // Create an instance of the tree
        TreeView t1 = new TreeView();
        // Assign the tree to the control
        t1 = (TreeView)ctl;
        // Clear any exisiting nodes
        t1.Nodes.Clear();

        // BUILD THE TREE!
        for (int p = 0; p < myRows.Length; p++)
        
            // Get Parent Node
            if ((Guid)myRows[p]["ID"] == (Guid)myRows[p]["LocationID"])
            
                // Create Parent Node
                TreeNode parentNode = new TreeNode();
                parentNode.Text = (string)myRows[p]["Name"];
                t1.Nodes.Add(parentNode);

                // Get Child Node
                for (int c = 0; c < myRows.Length; c++)
                
                    if ((Guid)myRows[p]["LocationID"] == (Guid)myRows[c]["LocationID"] 
                        && (Guid)myRows[p]["LocationID"] != (Guid)myRows[c]["ID"] /* Exclude Parent */)
                    
                        // Create Child Node
                        TreeNode childNode = new TreeNode();
                        childNode.Text = (string)myRows[c]["Name"];
                        parentNode.ChildNodes.Add(childNode);
                    
                
            
        
        // ALL DONE BUILDING!

        // Close the Data Connection
        connection.Close();
    



这是来自实际 SQL 表的片段:位置

ID LocationID 姓名 ____________________________________ ____________________________________ ______________ DEAF3FFF-FD33-4ECF-910B-1B07DF192074 48700BC6-D422-4B26-B123-31A7CB704B97 下降 F 48700BC6-D422-4B26-B123-31A7CB704B97 7EBDF61C-3425-46DB-A4D5-686E91FD0832 奥尔威 06B49351-6D18-4595-8228-356253CF45FF 6E8C65AC-CB22-42DA-89EB-D81C5ED0BBD0 下降 E 5 E98BC1F6-4BAE-4022-86A5-43BBEE2BA6CD DEAF3FFF-FD33-4ECF-910B-1B07DF192074 下降 F 6 F6A2CF99-F708-4C61-8154-4C04A38ADDC6 7EBDF61C-3425-46DB-A4D5-686E91FD0832 Pree 0EC89A67-D74A-4A3B-8E03-4E7AAAFEBE51 6E8C65AC-CB22-42DA-89EB-D81C5ED0BBD0 下降 E 4 35540B7A-62F9-487F-B65B-4EA5F42AD88A 48700BC6-D422-4B26-B123-31A7CB704B97 奥尔威故障 5000AB9D-EB95-48E3-B5C0-547F5DA06FC6 6E8C65AC-CB22-42DA-89EB-D81C5ED0BBD0 输出 1 53CDD540-19BC-4BC2-8612-5C0663B7FDA5 6E8C65AC-CB22-42DA-89EB-D81C5ED0BBD0 下降 E 3 7EBDF61C-3425-46DB-A4D5-686E91FD0821 B46C7305-18B1-4499-9E1C-7B6FDE786CD6 测试 1 7EBDF61C-3425-46DB-A4D5-686E91FD0832 7EBDF61C-3425-46DB-A4D5-686E91FD0832 HMN

谢谢。

【问题讨论】:

这些名字令人困惑。我会将Location ID 更改为Parent_ID 纯 SQL 答案可能需要知道您使用的是哪种 SQL。你能指定你有哪个数据库吗?也许是 SQL Server 2008? 不幸的是,我无法更改名称,太多其他事情取决于它们。我正在使用 SQL Server 2008。 您使用的是 HierarchyID 数据类型? 【参考方案1】:

您正在寻找使用公用表表达式或简称 CTE 的递归查询。在 SQL Server 2008 中对此进行详细说明可以是found on MSDN。

一般来说,它们的结构类似于以下:

WITH cte_name ( column_name [,...n] )
AS (
    –- Anchor
    CTE_query_definition

    UNION ALL

    –- Recursive portion
    CTE_query_definition
)
-- Statement using the CTE
SELECT * FROM cte_name

执行此操作时,SQL Server 将执行类似于以下的操作(从 MSDN 解释为更简单的语言):

    将 CTE 表达式拆分为锚和递归成员。 运行锚点,创建第一个结果集。 运行递归部分,将前面的步骤作为输入。 重复第 3 步,直到返回一个空集。 返回结果集。这是锚点和所有递归步骤的 UNION ALL。

对于这个具体的例子,试试这样的:

With hierarchy (id, [location id], name, depth)
As (
    -- selects the "root" level items.
    Select ID, [LocationID], Name, 1 As depth
    From dbo.Locations
    Where ID = [LocationID]

    Union All

    -- selects the descendant items.
    Select child.id, child.[LocationID], child.name,
        parent.depth + 1 As depth
    From dbo.Locations As child
    Inner Join hierarchy As parent
        On child.[LocationID] = parent.ID
    Where child.ID != parent.[Location ID])
-- invokes the above expression.
Select *
From hierarchy

根据您的示例数据,您应该得到如下信息:

ID     | Location ID | Name  | Depth
_______| __________  |______ | _____
1331   | 1331        | House |     1
1321   | 1331        | Room  |     2
2141   | 1321        | Bed   |     3

请注意,“健身房”不包括在内。根据您的示例数据,它的 ID 与其 [Location ID] 不匹配,因此它不是根级项目。它的位置 ID 2231 未出现在有效父 ID 列表中。


编辑 1:

您曾询问过如何将其放入 C# 数据结构中。在 C# 中表示层次结构的方式有很多种。这是一个例子,选择它是为了简单。一个真正的代码示例无疑会更广泛。

第一步是定义层次结构中每个节点的外观。除了包含节点中每个数据的属性外,我还包含了ParentChildren 属性,以及Add 子节点和Get 子节点的方法。 Get 方法将搜索节点的整个后代轴,而不仅仅是节点自己的子节点。

public class LocationNode 
    public LocationNode Parent  get; set; 
    public List<LocationNode> Children = new List<LocationNode>();
    
    public int ID  get; set; 
    public int LocationID  get; set; 
    public string Name  get; set; 
    
    public void Add(LocationNode child) 
        child.Parent = this;
        this.Children.Add(child);
    
    
    public LocationNode Get(int id) 
        LocationNode result;
        foreach (LocationNode child in this.Children) 
            if (child.ID == id) 
                return child;
            
            result = child.Get(id);
            if (result != null) 
                return result;
            
        
        return null;
    

现在您需要填充您的树。这里有一个问题:很难以错误的顺序填充树。在添加子节点之前,您确实需要对父节点的引用。如果您不得不不按顺序执行此操作,则可以通过两次传递(一次创建所有节点,另一次创建树)来缓解问题。但是,在这种情况下,这是不必要的。

如果您使用我上面提供的 SQL 查询并按depth 列排序,您可以在数学上确定在遇到父节点之前永远不会遇到子节点。因此,您可以一次性完成。

您仍然需要一个节点作为树的“根”。您可以决定这是否是“House”(来自您的示例),或者它是否是您为此目的创建的虚构占位符节点。我建议后者。

所以,代码!同样,这针对简单性和可读性进行了优化。您可能希望在生产代码中解决一些性能问题(例如,没有必要经常查找“父”节点)。我在这里避免了这些优化,因为它们会增加复杂性。

// Create the root of the tree.
LocationNode root = new LocationNode();

using (SqlCommand cmd = new SqlCommand()) 
    cmd.Connection = conn; // your connection object, not shown here.
    cmd.CommandText = "The above query, ordered by [Depth] ascending";
    cmd.CommandType = CommandType.Text;
    using (SqlDataReader rs = cmd.ExecuteReader()) 
        while (rs.Read()) 
            int id = rs.GetInt32(0); // ID column
            var parent = root.Get(id) ?? root;
            parent.Add(new LocationNode 
                ID = id,
                LocationID = rs.GetInt32(1),
                Name = rs.GetString(2)
            );
        
    

哒哒! root LocationNode 现在包含您的整个层次结构。顺便说一句,我还没有真正执行过这段代码,所以如果你发现任何明显的问题,请告诉我。


编辑 2

要修复您的示例代码,请进行以下更改:

删除此行:

// Create an instance of the tree
TreeView t1 = new TreeView();

这一行实际上不是问题,但应该删除它。你这里的cmets不准确;您并没有真正将树分配给控件。相反,您正在创建一个新的 TreeView,将其分配给 t1,然后立即将不同的对象分配给 t1。一旦执行下一行,您创建的 TreeView 就会丢失。

修复您的 SQL 语句

// SQL Commands
string getLocations = "SELECT ID, LocationID, Name FROM dbo.Locations";

用我之前建议的 SQL 语句替换这个 SQL 语句,并带有一个 ORDER BY 子句。阅读我之前的编辑,它解释了为什么“深度”很重要:您确实希望以特定顺序添加节点。在拥有父节点之前,您无法添加子节点。

(可选)我认为您在这里不需要 SqlDataAdapter 和 DataTable 的开销。我最初建议的 DataReader 解决方案更简单,更易于使用,并且在资源方面更有效。

此外,大多数 C# SQL 对象都实现了IDisposable,因此您需要确保正确使用它们。如果某些东西实现了IDisposable,请确保将其包装在using 语句中(请参阅我之前的C# 代码示例)。

修复你的树构建循环

您只获得父节点和子节点,因为您有一个用于父节点的循环和一个用于子节点的内部循环。您必须已经知道,您没有得到孙子,因为您没有添加它们的代码。

您可以添加一个内部循环来获取孙子,但显然您是在寻求帮助,因为您已经意识到这样做只会导致疯狂。如果你那时想要曾孙,会发生什么?内-内-内循环?这种技术不可行。

您可能在这里想到了递归。这是一个完美的地方,如果你正在处理树状结构,它最终会出现。现在您已经编辑了您的问题,很明显您的问题与 SQL 几乎没有关系。你真正的问题是递归。最终可能有人会出现并为此设计一个递归解决方案。这将是一个完全有效且可能更可取的方法。

但是,我的回答已经涵盖了递归部分——它只是将它移到了 SQL 层。因此,我将保留我以前的代码,因为我觉得它是对这个问题的一个合适的通用答案。根据您的具体情况,您需要进行更多修改。

首先,您不需要我建议的 LocationNode 类。您正在使用 TreeNode 代替,这样可以正常工作。

其次,TreeView.FindNode 类似于我建议的LocationNode.Get 方法,只是FindNode 需要节点的完整路径。要使用FindNode,您必须修改 SQL 以提供此信息。

因此,您的整个 PopulateTree 函数应如下所示:

public void PopulateTree(TreeView t1) 

    // Clear any exisiting nodes
    t1.Nodes.Clear();

    using (SqlConnection connection = new SqlConnection()) 
        connection.ConnectionString = "((replace this string))";
        connection.Open();

        string getLocations = @"
            With hierarchy (id, [location id], name, depth, [path])
            As (

                Select ID, [LocationID], Name, 1 As depth,
                    Cast(Null as varChar(max)) As [path]
                From dbo.Locations
                Where ID = [LocationID]

                Union All

                Select child.id, child.[LocationID], child.name,
                    parent.depth + 1 As depth,
                    IsNull(
                        parent.[path] + '/' + Cast(parent.id As varChar(max)),
                        Cast(parent.id As varChar(max))
                    ) As [path]
                From dbo.Locations As child
                Inner Join hierarchy As parent
                    On child.[LocationID] = parent.ID
                Where child.ID != parent.[Location ID])

            Select *
            From hierarchy
            Order By [depth] Asc";

        using (SqlCommand cmd = new SqlCommand(getLocations, connection)) 
            cmd.CommandType = CommandType.Text;
            using (SqlDataReader rs = cmd.ExecuteReader()) 
                while (rs.Read()) 
                    // I guess you actually have GUIDs here, huh?
                    int id = rs.GetInt32(0);
                    int locationID = rs.GetInt32(1);
                    TreeNode node = new TreeNode();
                    node.Text = rs.GetString(2);
                    node.Value = id.ToString();

                    if (id == locationID) 
                        t1.Nodes.Add(node);
                     else 
                        t1.FindNode(rs.GetString(4)).ChildNodes.Add(node);
                    
                
            
        
    

如果您发现任何其他错误,请告诉我!

【讨论】:

太好了,这就是我想要的。谢谢!不过还有一个问题。我怎么知道哪个孩子属于哪个父母?为了开发这些数据的树形视图,我需要知道关系。 有没有办法使用 ASP.NET C# 完成同样的任务,这样我就可以在查询数据的同时构建树? 我已经包含了一些示例 C# 代码,这些代码利用 CTE 查询来构建 C# 树状结构。这是你要找的吗? 这就是我要找的。我在编译时遇到了一些问题。此代码是否适用于 ASP.NET C# 项目? 是的,我认为它应该可以工作。我没有执行它,但我确实验证了它在语法上是正确的。如果您愿意,请尝试使用更多详细信息来编辑您的问题,我可以尝试为您提供帮助。或者,如果你觉得你了解层次结构的 SQL 方面,并且你被困在 C# 方面,那么你可能会认为这是一个单独的问题,并发布一个新的 SO 帖子。【参考方案2】:

我建议您还看看 SQL Server 2008 中引入的HierarchyId 数据类型,它为您提供了许多遍历和操作树结构的功能。这是一个教程:

Working With SQL Server HierarchyId Data Type In .NET Application

【讨论】:

【参考方案3】:

抱歉,我只是在浏览 ***。我看到了你的问题,感觉就像三年前写了一篇回答你问题的文章。如果有帮助,请告诉我。http://www.simple-talk.com/dotnet/asp.net/rendering-hierarchical-data-with-the-treeview/

【讨论】:

【参考方案4】:

SQl 2008 中有一个新功能。即hierarchyid。这个功能让我的生活更轻松。

有好用的method for hierarchyid datatype、GetAncestor()、GetRoot()... 一旦我处理层次结构,它将降低查询的复杂性。

【讨论】:

以上是关于SQL 数据层次结构的主要内容,如果未能解决你的问题,请参考以下文章

设计 SQL 数据库来表示 OO 类层次结构

从 spark 数据框或 sql 中选择具有偏好层次结构的多个记录

在SQL层次结构数据中移动父AND子项

SQL - 在 PARTITION BY 语句中创建层次结构

SQL Server 中层次结构和规范化的建议

确定 PL/SQL 过程的调用层次结构