这种表结构如果需要频繁查询,很可能会出问题。很少建议在单个列中存储多个值,尽管 MySQL 有一种基本的读取方法。
不过,鉴于您现有的要求,查询结果并不难产生您想要的结果。使用LEFT JOIN 与具有不同别名的自身 连接表,您可以使用MySQL 的FIND_IN_SET() string function 在path 中定位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,您的结果可能会有很大差异。
我发现修改原始数据以满足尾随逗号的要求有点令人反感。