计算同一 table 中的后代数
Count number of descendants in the same table
我有 table 个带有 ID 的 object 个,其中一些是基于其他 object 个。
为此,我使用了一个名为路径的字段,其中列出了 parents' ID
的字符串
ObjectD(path="A,B,C")是基于objectC是基于B是基于A.
现在我想 select * 来自所有 objects,加上一个额外的列:count(descendants)
(A 有 3 个 (B,C 和 D) B 有 2 个 (C 和 D) 而 C 只有一个 (D) - D 有 0
"my"个后代是路径=myPath+myID(+more?)
的object个数
- 这是否仅在 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
这个table结构如果需要经常查询的话很可能会出问题。很少建议在单个列中存储多个值,尽管 MySQL 有一种基本的读取方法。
不过鉴于您现有的要求,查询结果并不难产生您想要的结果。使用 LEFT JOIN
将 table 与 自身 加入不同的别名,您可以使用 MySQL 的 FIND_IN_SET()
string function 来定位object
里面的path
作为加入条件。
加入后,您可以 COUNT()
来自 FIND_IN_SET()
的匹配项,并且由于您使用了 LEFT JOIN
,它将 return 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 没有很好的拆分字符串的本机功能,并且不能很好地利用索引。
附录:
I 运行 EXPLAIN
针对 FIND_IN_SET()
查询,两列各有一个索引:
+------+-------------+-------+-------+---------------+------+---------+------+------+--------------------------------------------------------------+
| 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) |
+------+-------------+-------+-------+---------------+------+---------+------+------+--------------------------------------------------------------+
这是在更正源数据以使用尾随逗号和空字符串而不是 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 |
+------+--------------------+-------+-------+---------------+------+---------+------+------+--------------------------+
最后,用subselect修改后的数据表示为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,您的结果可能会有很大差异。
我发现修改原始数据以满足尾随逗号的要求有点令人反感。
我假设您有一个 table,id 列为 keyy,parents 为 parents。我还假设 parents 列中的每个 parent 都以“,”结尾。
那么:
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
如果您在 parents 列上有索引,则可以使用它。
也许你应该把 zz 换成更合理的东西。
我有 table 个带有 ID 的 object 个,其中一些是基于其他 object 个。
为此,我使用了一个名为路径的字段,其中列出了 parents' ID
ObjectD(path="A,B,C")是基于objectC是基于B是基于A.
现在我想 select * 来自所有 objects,加上一个额外的列:count(descendants)
(A 有 3 个 (B,C 和 D) B 有 2 个 (C 和 D) 而 C 只有一个 (D) - D 有 0
"my"个后代是路径=myPath+myID(+more?)
的object个数
- 这是否仅在 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
这个table结构如果需要经常查询的话很可能会出问题。很少建议在单个列中存储多个值,尽管 MySQL 有一种基本的读取方法。
不过鉴于您现有的要求,查询结果并不难产生您想要的结果。使用 LEFT JOIN
将 table 与 自身 加入不同的别名,您可以使用 MySQL 的 FIND_IN_SET()
string function 来定位object
里面的path
作为加入条件。
加入后,您可以 COUNT()
来自 FIND_IN_SET()
的匹配项,并且由于您使用了 LEFT JOIN
,它将 return 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 没有很好的拆分字符串的本机功能,并且不能很好地利用索引。
附录:
I 运行 EXPLAIN
针对 FIND_IN_SET()
查询,两列各有一个索引:
+------+-------------+-------+-------+---------------+------+---------+------+------+--------------------------------------------------------------+
| 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) |
+------+-------------+-------+-------+---------------+------+---------+------+------+--------------------------------------------------------------+
这是在更正源数据以使用尾随逗号和空字符串而不是 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 |
+------+--------------------+-------+-------+---------------+------+---------+------+------+--------------------------+
最后,用subselect修改后的数据表示为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,您的结果可能会有很大差异。
我发现修改原始数据以满足尾随逗号的要求有点令人反感。
我假设您有一个 table,id 列为 keyy,parents 为 parents。我还假设 parents 列中的每个 parent 都以“,”结尾。 那么:
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
如果您在 parents 列上有索引,则可以使用它。 也许你应该把 zz 换成更合理的东西。