【问题标题】:Selecting count of JSON type values in MySQL table选择 MySQL 表中 JSON 类型值的计数
【发布时间】:2019-12-02 16:44:09
【问题描述】:

假设我们有下表

+-----+------------+
| id  | categories |
+-----+------------+
| id1 | [20,25]    |
| id2 | [25]       |
| id3 | [20,25,28] |
| id4 | [28,25]    |
| id5 | [20,25]    |
+-----+------------+

字段categories 是JSON 类型。它仅包含已知且有限的整数列表 - 例如 20、25、28。 所以,我需要以某种方式计算所有这些值的所有包含项,如下所示:

+-------+--------+
| count | number |
+-------+--------+
|    20 |      3 |
|    25 |      5 |
|    28 |      2 |
+-------+--------+

主要问题是使用单个请求来完成此操作,而无需在服务器代码或过程调用中遍历类别编号

正面决定如下

SELECT 
    COUNT(id) AS 'count', '20' AS number
FROM
    ml_categories
WHERE
    JSON_CONTAINS(categories, '20') 
UNION SELECT 
    COUNT(id) AS 'count', '25' AS number
FROM
    ml_categories
WHERE
    JSON_CONTAINS(categories, '25') 
UNION SELECT 
    COUNT(id) AS 'count', '28' AS number
FROM
    ml_categories
WHERE
    JSON_CONTAINS(categories, '28')

但是这个解决方案的复杂度为 O(n),而且代码本身还不够好。例如,对于我的硬件上的一个类别,循环约 500K 记录大约需要 1 秒,因此计算 10 个类别大约需要 10 秒。不好。 有没有办法优化这样的查询?

提前致谢,伙计们

【问题讨论】:

  • 您的表格不是标准的,如果您可以查看表格,每行输入一个值

标签: mysql sql json


【解决方案1】:

但是这个解决方案有 O(n) 复杂度

我不确定您的情况是什么n。但我敢肯定,您不会找到比 O(n) 更好的解决方案。

假设以下数字:

  • n:项目数(“ml_categories”表中的行)
  • m:所有类别的数量
  • a:每个项目的平均类别数

您的查询的复杂度为 O(n*m)。即使是 Bill Karwins 解决方案(我认为是最佳的)也具有 O(n*a) 的复杂性(假设 GROUP BY 子句在 category_id 上的索引)。

如果您有一个包含所有类别的 categories 表,您可以使用以下查询:

select c.id as category, count(*)
from ml_categories i
join categories c on json_contains(i.categories, cast(c.id as json))
group by c.id

它将返回与您的 UNION 查询相同的结果:

| category | count(*) |
| -------- | -------- |
| 20       | 3        |
| 25       | 5        |
| 28       | 2        |

View on DB Fiddle

而且因为没有索引可以用于 JOIN,它可能和您的查询一样快或一样慢(最好的情况是索引可以用于 GROUP BY 避免 filesort) .

如果你使用 MySQL 8(至少 8.0.4),你可以使用JSON_TABLE()

select c.category, count(*)
from ml_categories i
join json_table(
  i.categories,
  '$[*]' columns (category int path '$')
) c
group by c.category;

View on DB Fiddle

使用 JSON_TABLE 的 JOIN 会将 JSON 列中的类别“解包”成行。如果您删除 GROUP BY 子句,那么您将获得一个(动态)规范化表。这应该与 O(n*a) 一起扩展。但是由于表是动态创建的,所以不会有索引来支持 GROUP BY 子句。所以必须先对结果进行排序,最终的复杂度为O(n*a * log(n*a))。这比 O(n*m) 更好地扩展(如果 m 增长而 a 没有)。但是,如果 m(类别的数量)足够小,您的查询可能仍然是使用给定模式所能做的最好的查询。

【讨论】:

  • 点,它通常是一个表扫描无论如何,除非你限制你正在搜索的 category_id 的值。如果 category_id 上的索引包括每个 category_id 的不同值的条目计数,理论上它可能是 O(a)
  • 完全正确。如果您没有 WHERE(或同等条件)条件,则无法击败 O(n)。但是,如果您想获取特定类别的所有项目,规范化模式是优越的(O(log(n))O(n))。我虽然想不出一个可以用O(a)解决的问题。如果您希望结果包含所有类别,则不能比 O(m) 更好(例如:从“缓存”表中读取计数)。
  • 不管怎样,我的印象是 OP 除了具有最佳计算复杂度之外,还在寻找更易于编码的查询。
  • 太棒了!这就是我一直在寻找的。还开始使用cast 切换类型以构建此类查询。也可以测量性能的一点点增益。堆栈溢出很棒!一如既往:)
【解决方案2】:

存储逗号分隔的列表不是一种关系策略。它是非规范化的。与所有优化一样,非规范化以牺牲其他类型的查询为代价来优化一种类型的查询。

因此,您在优化其他类型的查询时遇到问题也就不足为奇了。

优化此查询的方法是避免将多值属性存储在逗号分隔的列表(或 JSON,逻辑上等效)中。相反,将多值属性存储在行中,每行一个值,而不是在逗号分隔的列表或 JSON 对象中。换句话说,规范化你的数据。

为您的“事物”(无论它们是什么)和类别之间的多对多关系创建一个表:

CREATE TABLE things_have_categories (
  thing_id VARCHAR(10), 
  category_id INT, 
  PRIMARY KEY (category_id, thing_id)
);
INSERT INTO things_have_categories VALUES
('id1', 20),
('id1', 25),
('id2', 25),
('id3', 20),
('id3', 25),
('id3', 28),
('id4', 28),
('id4', 25),
('id5', 20),
('id5', 25);

然后你可以像这样编写一个更简单和优化的查询:

SELECT category_id, COUNT(*) as count
FROM things_have_categories
GROUP BY category_id

输出:

+-------------+-------+
| category_id | count |
+-------------+-------+
|          20 |     3 |
|          25 |     5 |
|          28 |     2 |
+-------------+-------+

你可能也喜欢my answer to "Is storing a delimited list in a database column really that bad?"

您可能会回复,“但我无法更改此表的存储方式。”

我以前听说过。如果这是约束,那么您将无法优化查询。它必然是 O(n)。

【讨论】:

  • 谢谢。我知道您的解决方案在设计本身上更可行和更好,但使用 JSON 类型是一种实验 - 包括性能问题。这就是为什么我的问题对我自己来说也有点奇怪——知道桌子本身应该设计得更好。
  • 好吧,你的实验结果是:JSON 使得插入复杂数据变得更容易(在这种情况下,每个事物的类别 id 的列表),但是 更难查询该数据。
【解决方案3】:

您可以对数组的所有三个组件的每个值(202528)使用JSON_EXTRACT() 函数,然后使用Conditional Aggregation,然后应用UNION ALL 来组合所有这些查询:

SELECT 20 as count, sum(case when 20 in (comp1,comp2,comp3) then 1 end) as number
  FROM
  (SELECT JSON_EXTRACT(categories, '$[0]') as comp1, 
          JSON_EXTRACT(categories, '$[1]') as comp2,
          JSON_EXTRACT(categories, '$[2]') as comp3
     FROM ml_categories ) q1 
UNION ALL
SELECT 25 as count, sum(case when 25 in (comp1,comp2,comp3) then 1 end) as number
  FROM
  (SELECT JSON_EXTRACT(categories, '$[0]') as comp1, 
          JSON_EXTRACT(categories, '$[1]') as comp2,
          JSON_EXTRACT(categories, '$[2]') as comp3
     FROM ml_categories ) q2
UNION ALL
SELECT 28 as count, sum(case when 28 in (comp1,comp2,comp3) then 1 end) as number
  FROM
  (SELECT JSON_EXTRACT(categories, '$[0]') as comp1, 
          JSON_EXTRACT(categories, '$[1]') as comp2,
          JSON_EXTRACT(categories, '$[2]') as comp3
     FROM ml_categories ) q3 

Demo

【讨论】:

  • 不。我的问题是如何摆脱多个语句并可能提高性能。 Paul Spiegel 认为是最好的。
  • 是的,我刚刚看到@Paul Spiegel 的案例是最好的。
猜你喜欢
  • 2020-12-29
  • 1970-01-01
  • 2011-11-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-07
  • 1970-01-01
相关资源
最近更新 更多