【问题标题】:Creating hierarchical data (tree) structures in Neo4j using "tree keys"使用“树键”在 Neo4j 中创建分层数据(树)结构
【发布时间】:2016-09-15 20:59:48
【问题描述】:

我从 CSV 文件导入数据并创建了很多节点,所有这些节点都基于“树”在同一数据集中与其他节点相关编号”等级体系:

例如,具有树号 A01.111的节点是节点A01的直接子节点,而具有的节点树编号 A01.111.230 是节点A01.111 的直接子节点。

我想要做的是在作为其他节点的直接子节点的节点之间创建独特的关系。例如,节点 A01.111.230 应该与节点 A01.111 有一个“IS_CHILD_OF”关系。

我已经尝试了几件事,例如:

MATCH (n:Node), (n2:Node)
WHERE (n2.treeNumber STARTS WITH n.treeNumber)
AND (n <> n2)
AND NOT ((n2)-[:IS_CHILD_OF]->())
CREATE UNIQUE (n2)-[:IS_CHILD_OF]->(n);

此示例导致创建唯一的“IS_CHILD_OF”关系,但不是与节点的直接父级。相反,节点 A01.111.230 将与节点 A01 相关。

【问题讨论】:

    标签: database neo4j tree


    【解决方案1】:

    我想建议另一种通用解决方案,同时避免使用@InverseFalcon 指出的笛卡尔积。

    让我们首先创建一个索引以加快查找速度,并插入一些测试数据:

    CREATE CONSTRAINT ON (n:Node) ASSERT n.treeNumber IS UNIQUE;
    CREATE (n:Node {treeNumber: 'A01.111.230'})
    CREATE (n:Node {treeNumber: 'A01.111'})
    CREATE (n:Node {treeNumber: 'A01'})
    

    然后我们需要扫描所有节点作为潜在的父母,并寻找以父母的treeNumber开头的孩子(STARTS WITH可以使用索引)的“剩余”中没有点treeNumber(即直接孩子),而不是拆分、加入等:

    MATCH (p:Node), (c:Node)
    WHERE c.treeNumber STARTS WITH p.treeNumber
      AND p <> c
      AND NOT substring(c.treeNumber, length(p.treeNumber) + 1) CONTAINS '.'
    RETURN p, c
    

    出于分析目的,我用简单的RETURN 替换了关系的创建,但您可以简单地用CREATE UNIQUEMERGE 替换它。

    实际上,我们可以通过预先计算应该匹配的实际前缀来摆脱p &lt;&gt; c谓词长度上的+ 1

    MATCH (p:Node)
    WITH p, p.treeNumber + '.' AS parentNumber
    MATCH (c:Node)
    WHERE c.treeNumber STARTS WITH parentNumber
      AND NOT substring(c.treeNumber, length(parentNumber)) CONTAINS '.'
    RETURN p, c
    

    但是,分析该查询表明该索引未使用,并且存在一个笛卡尔积(所以我们有一个O(n^2) 算法):

    Compiler CYPHER 3.0
    
    Planner COST
    
    Runtime INTERPRETED
    
    +--------------------+----------------+------+---------+----------------------+------------------------------------------------------------------------------------------------------------------------------------+
    | Operator           | Estimated Rows | Rows | DB Hits | Variables            | Other                                                                                                                              |
    +--------------------+----------------+------+---------+----------------------+------------------------------------------------------------------------------------------------------------------------------------+
    | +ProduceResults    |              2 |    2 |       0 | c, p                 | p, c                                                                                                                               |
    | |                  +----------------+------+---------+----------------------+------------------------------------------------------------------------------------------------------------------------------------+
    | +Filter            |              2 |    2 |      26 | c, p, parentNumber   | NOT(Contains(SubstringFunction(c.treeNumber,length(parentNumber),None),{  AUTOSTRING1})) AND StartsWith(c.treeNumber,parentNumber) |
    | |                  +----------------+------+---------+----------------------+------------------------------------------------------------------------------------------------------------------------------------+
    | +Apply             |              2 |    9 |       0 | p, parentNumber -- c |                                                                                                                                    |
    | |\                 +----------------+------+---------+----------------------+------------------------------------------------------------------------------------------------------------------------------------+
    | | +NodeByLabelScan |              9 |    9 |      12 | c                    | :Node                                                                                                                              |
    | |                  +----------------+------+---------+----------------------+------------------------------------------------------------------------------------------------------------------------------------+
    | +Projection        |              3 |    3 |       3 | parentNumber -- p    | p; Add(p.treeNumber,{  AUTOSTRING0})                                                                                               |
    | |                  +----------------+------+---------+----------------------+------------------------------------------------------------------------------------------------------------------------------------+
    | +NodeByLabelScan   |              3 |    3 |       4 | p                    | :Node                                                                                                                              |
    +--------------------+----------------+------+---------+----------------------+------------------------------------------------------------------------------------------------------------------------------------+
    
    Total database accesses: 45
    

    但是,如果我们像这样简单地添加一个提示

    MATCH (p:Node)
    WITH p, p.treeNumber + '.' AS parentNumber
    MATCH (c:Node)
    USING INDEX c:Node(treeNumber)
    WHERE c.treeNumber STARTS WITH parentNumber
      AND NOT substring(c.treeNumber, length(parentNumber)) CONTAINS '.'
    RETURN p, c
    

    它确实使用了索引,我们有类似 O(n*log(n)) 算法(log(n) 用于索引查找):

    Compiler CYPHER 3.0
    
    Planner COST
    
    Runtime INTERPRETED
    
    +-------------------------------+----------------+------+---------+----------------------+------------------------------------------------------------------------------------------+
    | Operator                      | Estimated Rows | Rows | DB Hits | Variables            | Other                                                                                    |
    +-------------------------------+----------------+------+---------+----------------------+------------------------------------------------------------------------------------------+
    | +ProduceResults               |              2 |    2 |       0 | c, p                 | p, c                                                                                     |
    | |                             +----------------+------+---------+----------------------+------------------------------------------------------------------------------------------+
    | +Filter                       |              2 |    2 |       6 | c, p, parentNumber   | NOT(Contains(SubstringFunction(c.treeNumber,length(parentNumber),None),{  AUTOSTRING1})) |
    | |                             +----------------+------+---------+----------------------+------------------------------------------------------------------------------------------+
    | +Apply                        |              2 |    3 |       0 | p, parentNumber -- c |                                                                                          |
    | |\                            +----------------+------+---------+----------------------+------------------------------------------------------------------------------------------+
    | | +NodeUniqueIndexSeekByRange |              9 |    3 |       6 | c                    | :Node(treeNumber STARTS WITH parentNumber)                                               |
    | |                             +----------------+------+---------+----------------------+------------------------------------------------------------------------------------------+
    | +Projection                   |              3 |    3 |       3 | parentNumber -- p    | p; Add(p.treeNumber,{  AUTOSTRING0})                                                     |
    | |                             +----------------+------+---------+----------------------+------------------------------------------------------------------------------------------+
    | +NodeByLabelScan              |              3 |    3 |       4 | p                    | :Node                                                                                    |
    +-------------------------------+----------------+------+---------+----------------------+------------------------------------------------------------------------------------------+
    
    Total database accesses: 19
    

    请注意,我在前面介绍创建前缀的WITH 步骤时确实有点作弊,因为我注意到它改进了执行计划和数据库访问

    MATCH (p:Node), (c:Node)
    USING INDEX c:Node(treeNumber)
    WHERE c.treeNumber STARTS WITH p.treeNumber
      AND p <> c
      AND NOT substring(c.treeNumber, length(p.treeNumber) + 1) CONTAINS '.'
    RETURN p, c
    

    执行计划如下:

    Compiler CYPHER 3.0
    
    Planner RULE
    
    Runtime INTERPRETED
    
    +--------------+------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------+
    | Operator     | Rows | DB Hits | Variables | Other                                                                                                                      |
    +--------------+------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------+
    | +Filter      |    2 |       9 | c, p      | NOT(p == c) AND NOT(Contains(SubstringFunction(c.treeNumber,Add(length(p.treeNumber),{  AUTOINT0}),None),{  AUTOSTRING1})) |
    | |            +------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------+
    | +SchemaIndex |    6 |      12 | c -- p    | PrefixSeekRangeExpression(p.treeNumber); :Node(treeNumber)                                                                 |
    | |            +------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------+
    | +NodeByLabel |    3 |       4 | p         | :Node                                                                                                                      |
    +--------------+------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------+
    
    Total database accesses: 25
    

    最后,记录一下,我写的原始查询的执行计划(即没有提示)是:

    Compiler CYPHER 3.0
    
    Planner COST
    
    Runtime INTERPRETED
    
    +--------------------+----------------+------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | Operator           | Estimated Rows | Rows | DB Hits | Variables | Other                                                                                                                                                                |
    +--------------------+----------------+------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | +ProduceResults    |              2 |    2 |       0 | c, p      | p, c                                                                                                                                                                 |
    | |                  +----------------+------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | +Filter            |              2 |    2 |      21 | c, p      | NOT(p == c) AND StartsWith(c.treeNumber,p.treeNumber) AND NOT(Contains(SubstringFunction(c.treeNumber,Add(length(p.treeNumber),{  AUTOINT0}),None),{  AUTOSTRING1})) |
    | |                  +----------------+------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | +CartesianProduct  |              9 |    9 |       0 | p -- c    |                                                                                                                                                                      |
    | |\                 +----------------+------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | | +NodeByLabelScan |              3 |    9 |      12 | c         | :Node                                                                                                                                                                |
    | |                  +----------------+------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | +NodeByLabelScan   |              3 |    3 |       4 | p         | :Node                                                                                                                                                                |
    +--------------------+----------------+------+---------+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    
    Total database accesses: 37
    

    这不是最糟糕的一个:没有提示但带有预先计算的前缀的是!这就是为什么您应该始终测量的原因。

    【讨论】:

      【解决方案2】:

      我认为我们可以对查询进行一些改进。首先,确保您在 :Node.treeNumber 上具有唯一约束或索引,因为您需要它来改进此查询中的父节点查找。

      接下来,我们在子节点上进行匹配,不包括根节点(假设根的 treeNumber 中没有 .')和已经处理过并且已经有关系的节点。

      然后我们将使用我们的索引通过 treeNumber 找到每个节点的父节点,并创建关系。这假定子 treeNumber 总是有 4 个以上的字符,包括点。

      MATCH (child:Node)
      WHERE child.treeNumber CONTAINS '.'
      AND NOT EXISTS( (child)-[:IS_CHILD_OF]->() )
      WITH child, SUBSTRING(child.treeNumber, 0, SIZE(child.treeNumber)-4) as parentNumber
      MATCH (parent:Node)
      WHERE parent.treeNumber = parentNumber
      CREATE UNIQUE (child)-[:IS_CHILD_OF]->(parent)
      

      我认为这个查询避免了笛卡尔积,因为您可能从其他答案中得到,并且应该在 O(n) 左右(如果我错了,请有人纠正我)。

      编辑

      如果 treeNumbers 中的每个数字子集不限于 3(如您的描述中,实际上是 'A01.111.23'),那么您需要一种不同的方法来派生 parentNumber。 Neo4j 在这里有点弱,因为它既缺少 indexOf() 函数,也缺少 join() 函数来反转 split()。您可能需要安装 APOC Procedures library 以允许访问 join() 函数。

      处理treeNumber的数字子集中位数可变的情况的查询变为:

      MATCH (child:Node)
      WHERE child.treeNumber CONTAINS '.'
      AND NOT EXISTS( (child)-[:IS_CHILD_OF]->() )
      WITH child, SPLIT(child.treeNumber, '.') as splitNumber
      CALL apoc.text.join(splitNumber[0..-1], '.') YIELD value AS parentNumber
      WITH child, parentNumber
      MATCH (parent:Node)
      WHERE parent.treeNumber = parentNumber
      CREATE UNIQUE (child)-[:IS_CHILD_OF]->(parent)
      

      【讨论】:

        【解决方案3】:

        我想我只是想出了一个解决方案! (如果有人有更优雅的请发帖)

        我刚刚意识到“树号”编码系统总是在点之间使用 3 位数字,即 A01.111.230C02.100,因此,如果节点是另一个节点的直接子节点,它的“树号”应该不仅以父节点的树号开头,还应该长 4 个字符(点 '.' 一个字符)和 3 个字符的数值)。

        因此,我似乎可以完成这项工作的解决方案是:

        MATCH (n:Node), (n2:Node)
        WHERE (n2.treeNumber STARTS WITH n.treeNumber)
        AND (length(n2.treeNumber) = (length(n.treeNumber) + 4)) 
        CREATE UNIQUE (n2)-[:IS_CHILD_OF]->(n);
        

        【讨论】:

        • 这可能会执行笛卡尔积,使其成为 n^2 操作。此外,您的描述有一个示例“A01.111.23”,它打破了您的假设,即 treeNumbers 的每个数字子集中都有 3 位数字。不过,这个问题还不错。
        【解决方案4】:

        对于您的要求 STARTS WITH 将不起作用,因为除了以 A01.111 开头之外,A01.111.23 确实以 A01 开头时间>。

        treeNumber 由带有“.”的几个部分组成。作为分隔符。我们不要对各个部分的最大/最小可能字符长度做出任何假设。我们需要将每个节点的treeNumber 的最后一部分以外的所有部分与被测试的潜在子节点的部分进行比较。您可以使用 Cypher 的 split() 函数来实现这一点,如下所示:

        MATCH (n1:Node), (n2:Node)
        WHERE split(n2.treeNumber,'.')[0..-1] = split(n1.treeNumber,'.')
        CREATE UNIQUE (n2)-[:IS_CHILD_OF]->(n1);
        

        split() 函数在每次出现给定分隔符时将字符串拆分为字符串列表(部分)。在这种情况下,分隔符是“。”拆分任何treeNumber。我们可以使用语法list[{startIndex}..{endIndex}] 选择密码列表的子集。允许反向查找的负索引,例如上述查询中使用的索引。

        此解决方案应推广到所有可能的treeNumber 值,采用手头的格式,无论零件数量和单个零件长度如何。

        【讨论】:

        • 虽然我同意通用解决方案更可取,但我认为您的查询可能不会利用 treeNumber 上的索引或约束,并且可能正在执行笛卡尔积。小数据集不是问题,但可能是大数据集的问题,或者如果它要运行多次。
        猜你喜欢
        • 2017-04-18
        • 2011-03-10
        • 1970-01-01
        • 1970-01-01
        • 2013-12-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多