【问题标题】:Count number of descendants in the same table计算同一张表中的后代数量
【发布时间】:2015-11-18 08:32:04
【问题描述】:

我有一个带有 ID 的对象表,其中一些基于其他对象。
为此,我使用了一个名为 path 的字段,它列出了父母 ID 的字符串

对象D (path="A,B,C") 是基于对象C 是基于B 是基于A。

现在我想从所有对象中选择 *,加上一个额外的列:count(descendants)
(A 有 3 个(B、C 和 D)B 有 2 个(C 和 D)而 C 只有一个(D)- D 有 0 个

“我的”后代是路径 = myPath+myID(+more?)
的对象的数量 - 这是否可能仅在 SQL 中(没有在 PHP 中循环)?

O id=a .... path="" .......... a 有 5 个后代
O id=b .... path="a" ........ b 有 3
O id=c .... path="a,b" ..... c 有 1
O id=d .... path="a,b,c" .. d 有 0
O id=n .... path="a,b" ..... n 有 0
O id=x .... path="a" ........ x 有 0

【问题讨论】:

  • 请澄清。您是否尝试根据最长链推断后代的数量,或者例如表中的Object C 是否已经将D 作为其path 值?如果我理解的话,由于多值列,如果所有行都存在path,那将非常困难。但是,如果没有在整个链条中填写这些内容,那么在 SQL 中将很难做到。
  • 请张贴表格中的行样本以消除歧义。
  • 它们被填写-所有前辈在路径字段中按顺序排列(字符串)
  • 鉴于您刚刚包含的示例,我想我仍然误解了。根据我的阅读,a 应该有 5 个后代而不是 4 个,因为 b、c、d、n、x 在它们的路径中都有 a*。这是一个错误吗?

标签: php mysql sql inner-join


【解决方案1】:

这种表结构如果需要频繁查询,很可能会出问题。很少建议在单个列中存储多个值,尽管 MySQL 有一种基本的读取方法。

不过,鉴于您现有的要求,查询结果并不难产生您想要的结果。使用LEFT JOIN 与具有不同别名的自身 连接表,您可以使用MySQL 的FIND_IN_SET() string functionpath 中定位object 作为连接条件。

加入后,您可以COUNT() 匹配来自FIND_IN_SET() 的匹配项,并且由于您使用了LEFT JOIN,它将为没有后代的人返回0

SELECT
  o.*,
  -- Count matches from the joined table
  COUNT(odesc.object) AS num_descendants
FROM
  paths o
  -- Self join with FIND_IN_SET()
  LEFT JOIN paths odesc ON FIND_IN_SET(o.object, odesc.path)
GROUP BY o.object

鉴于您的示例行,这里有一个演示,如果它工作并产生您的预期结果。 http://sqlfiddle.com/#!9/1fae7/1

现在,如果您的数据不像您的样本那样规则,那可能仍然允许不完全遵循的路径,而只是将对象作为成员。添加额外的LIKE 条件可以强制LEFT JOIN 两侧的路径以相同的方式开始,这意味着一条路径延伸另一条路径。

 LEFT JOIN paths odesc ON
   FIND_IN_SET(o.object, odesc.path)
   -- Additional condition to ensure paths start the same
   AND odesc.path LIKE CONCAT(COALESCE(o.path, ''), '%')

只是为了验证结果是否相同,http://sqlfiddle.com/#!9/1fae7/15

注意,使用FIND_IN_SET() 永远不会很快。这就是让这件事变得困难的原因——MySQL 没有很好的分割字符串的本机功能,也无法很好地利用索引。

附录:

我针对FIND_IN_SET() 查询运行了EXPLAIN,两列中的每一列都有一个索引:

+------+-------------+-------+-------+---------------+------+---------+------+------+--------------------------------------------------------------+
| id   | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra                                                        |
+------+-------------+-------+-------+---------------+------+---------+------+------+--------------------------------------------------------------+
|    1 | SIMPLE      | o     | index | NULL          | path | 20      | NULL |    6 | Using index; Using temporary; Using filesort                 |
|    1 | SIMPLE      | ox    | index | NULL          | path | 20      | NULL |    6 | Using where; Using index; Using join buffer (flat, BNL join) |
+------+-------------+-------+-------+---------------+------+---------+------+------+--------------------------------------------------------------+

这是来自 cmets 的依赖子查询的解释,在更正源数据以使用尾随逗号和空字符串而不是 NULL 之后:

EXPLAIN select    paths.*,   (select count(object)     from paths ox    where LEFT(ox.path,char_length( concat( paths.path, paths.object))) = concat(paths.path, paths.object ) )as descendants from paths;
+------+--------------------+-------+-------+---------------+------+---------+------+------+--------------------------+
| id   | select_type        | table | type  | possible_keys | key  | key_len | ref  | rows | Extra                    |
+------+--------------------+-------+-------+---------------+------+---------+------+------+--------------------------+
|    1 | PRIMARY            | paths | index | NULL          | path | 20      | NULL |    6 | Using index              |
|    2 | DEPENDENT SUBQUERY | ox    | index | NULL          | path | 20      | NULL |    6 | Using where; Using index |
+------+--------------------+-------+-------+---------------+------+---------+------+------+--------------------------+

最后,用子选择表示的修改数据改为LEFT JOIN,MySQL 可能能够更好地优化:

EXPLAIN SELECT
   paths.*,
   COUNT(ox.object)
FROM
  paths
  LEFT JOIN paths ox
     ON LEFT(ox.path,char_length(concat(paths.path, paths.object))) = concat(paths.path, paths.object)
GROUP BY paths.object;

+------+-------------+-------+-------+---------------+------+---------+------+------+--------------------------------------------------------------+
| id   | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra                                                        |
+------+-------------+-------+-------+---------------+------+---------+------+------+--------------------------------------------------------------+
|    1 | SIMPLE      | paths | index | NULL          | path | 20      | NULL |    6 | Using index; Using temporary; Using filesort                 |
|    1 | SIMPLE      | ox    | index | NULL          | path | 20      | NULL |    6 | Using where; Using index; Using join buffer (flat, BNL join) |
+------+-------------+-------+-------+---------------+------+---------+------+------+--------------------------------------------------------------+

这三个似乎都可以使用索引,但您需要根据实际行集对它们进行基准测试,以找到最有效的。重要的是,这些都是针对最近的 MariaDB 版本运行的。如果您使用的是较旧的 MySQL,您的结果可能会有很大差异。

我发现修改原始数据以满足尾随逗号的要求有点令人反感。

【讨论】:

  • 真棒 - 使用 FIND_IN_SET,即使是一棵有洞的树也可以工作(以及多行父母......肯定是可憎的)......但如果它很慢......我不能数数路径完全匹配 myPath+","+myID.. 的对象,使用 SUBSTRING 和 CHAR_LENGTH(更原始,但更快?)
  • @T4NK3R 任何这些字符串操作都可能不会比FIND_IN_SET() 快很多,除非它可以通过严格的左锚比较来完成,因此可以使用索引。我现在必须运行,但是如果我想到一个解决方案(我尝试过的 2 或 3 个都没有奏效),我会编辑以稍后添加它。
  • 我想得越多,避免 FIND_IN_SET() 变得越复杂 - 因为您需要特殊的 CASE 处理空路径和逗号等。
  • 我无法使用你的左连接和分组...但是这个匹配对我来说看起来更友好:left(ox.path, char_length(concat(o.路径,o.object))) = concat(o.path,o.object)
  • 那行不通,因为连接会产生像A,X 这样的值,当来自 A....X 的完整路径不存在时,这些值永远不会存在。只要您的示例中存在XN 之类的实例,其中路径不是之前所有内容的精确构建,我认为您将无法依赖子字符串。我使用通配符 LIKE、CONCAT() 和 SUBSTR() 进行了多次尝试,但这些行总是导致不正确的结果。我还有几件事要尝试使用通配符 LIKE...
【解决方案2】:

我假设您有一个表,其中包含 keyy 列的 id 和父母与父母。我还假设父母列中的每个父母都以“,”结尾。 那么:

select t.*, 
      (select count(*) 
       from t tt 
       where tt.parents between concat( t.parents , t.keyy ,',' )
         and  concat(t.parents , t.keyy ,',zzzzzzzzzz' ) )as descendants
    from t

如果您在父列上有索引,则可以使用它。 也许你应该把 zz 换成更合理的东西。

见: http://sqlfiddle.com/#!9/a2d5e/1

【讨论】:

  • 也可以。这(显然?) WHERE LEFT(tt.parents,char_length(concat( t.parents , t.keyy))) = concat( t.parents , t.keyy) - 单个匹配项(带有索引友好的 LEFT)
  • 这实际上不适用于路径为AX。它不会被识别为A 的后代。
  • 而 LEFT(CHAR_INDEX()) 版本将在该测试集中为 c 返回一个不正确的值,2 个后代而不是 1 个。这些方法只能用于完全连续的路径 A,B,C,D....N....X
  • 在 Rolans 的数据中,X 不是 A 的后代。X 在 A 旁边独立存在:对于他的数据,这个 LEFTY 版本有效:select t.*, (select count(*) from t tt where LEFT(tt.parents,char_length(concat( t.parents , t.keyy, ","))) = concat( t.parents , t.keyy,",") ) 作为 t 的后代
  • 上面的额外逗号仅用于区分 C 和 C2 - 如果所有 ID 的长度相同,则不需要
猜你喜欢
  • 1970-01-01
  • 2023-03-12
  • 1970-01-01
  • 2021-02-02
  • 1970-01-01
  • 1970-01-01
  • 2019-06-03
  • 2017-07-19
  • 1970-01-01
相关资源
最近更新 更多