Mysql json 基于趋势标签的实施
Mysql json based trending tags implementation
我正在尝试使用 mysql json 功能识别时间序列上的趋势标签(基于最大点击数)。
下面是我的 table
CREATE TABLE TAG_COUNTER (
account varchar(36) NOT NULL,
time_id INT NOT NULL,
counters JSON,
PRIMARY KEY (account, time_id)
)
在每个网络 api 请求中,我将为每个帐户获取多个不同的标签,并且根据标签的数量,我将准备 INSERT ON DUPLICATE KEY UPDATE
查询。下面的示例显示了带有两个标签的插入。
INSERT INTO `TAG_COUNTER`
(`account`, `time_id`, `counters`)
VALUES
('google', '2018061023', '{"tag1": 1, "tag2": 1}')
ON DUPLICATE KEY UPDATE `counters` =
JSON_SET(`counters`,
'$."tag1"',
IFNULL(JSON_EXTRACT(`counters`,
'$."tag1"'), 0) + 1,
'$."tag2"',
IFNULL(JSON_EXTRACT(`counters`,
'$."tag2"'), 0) + 1
);
time_id 是 yyyyMMddhh,每行按小时聚合。
现在我的问题是检索 treding 标签。
下面的查询将为我提供 tag1 的聚合,但在进行此查询之前我们不会知道这些标签。
SELECT
SUBSTRING(time_id, 1, 6) AS month,
SUM(counters->>'$.tag1')
FROM TAG_COUNTER
WHERE counters->>'$.tag1' > 0
GROUP BY month;
所以我需要通用分组查询和排序依据来获取当时的趋势标签 hourly/daily/monthly。
预期的输出样本是
Time(hour/day/month) Tag_name Tag_count_value(total hits)
当我在网上搜索时,每个地方都提到如下
{"tag_name": "tag1", "tag_count": 1}
而不是直接 {"tag1" : 1}
他们在分组依据中使用 tag_name。
Q1) 那么,是否总是必须要有公知的 json 密钥来执行 group by ..?
Q2) 如果我必须采用这种方式,那么我的 INSERT ON DUPLICATE KEY UPDATE 查询对于这个新的 json [=74 有什么变化=]结构?由于计数器必须在不存在时创建,并且在存在时应加一。
Q3) 我必须维护对象数组吗
[
{"tag_name": "tag1", "tag_count": 2},
{"tag_name": "tag2", "tag_count": 3}
]
OR 对象的对象如下?
{
{"tag_name": "tag1", "tag_count": 2},
{"tag_name": "tag2", "tag_count": 3}
}
那么在 json 结构中,趋势计数的插入和检索哪个更好?
Q4) 我可以使用现有的 {"key" : "value"}
格式而不是 {"key_label" : key, "value_lable" : "value"}
并且可以提取趋势..?因为我认为 {"key" : "value"}
非常直截了当并且擅长表现。
Q5) 检索时我正在使用 SUBSTRING(time_id, 1, 6) AS month
。它可以使用索引吗?
或者我是否需要创建多个列,如 time_hour(2018061023)
、time_day(20180610)
、time_month(201806)
并在特定列上使用查询?
或者我可以使用 mysql date-time functions 吗?会使用索引来加快检索速度吗?
请帮忙。
我看不出你在这里使用 JSON 的充分理由。同样不清楚的是,为什么您认为 MySQL 中的“nosql 模式”会做得更好。
您可能需要的是这样的东西:
CREATE TABLE TAG_COUNTER (
account varchar(36) NOT NULL,
time_id INT NOT NULL,
tag_name varchar(50) NOT NULL,
counter INT UNSIGNED NOT NULL,
PRIMARY KEY (account, time_id, tag_name)
);
这将简化您的查询。 INSERT 语句如下所示:
INSERT INTO TAG_COUNTER
(account, time_id, tag_name, counter)
VALUES
('google', 2018061023, 'tag1', 1),
('google', 2018061023, 'tag2', 1)
ON DUPLICATE KEY UPDATE counter = counter + VALUES(counter);
SELECT 语句可能是这样的
SELECT
SUBSTRING(time_id, 1, 6) AS month,
tag_name,
SUM(counter) AS counter_agg
FROM TAG_COUNTER
GROUP BY month, tag_name
ORDER BY month, counter_agg DESC;
请注意,我没有尝试针对数据大小和性能优化 table/schema。那将是一个不同的问题。但是你必须看到,现在的查询要简单得多。
正如我在评论中所说,我认为远离 JSON 是可行的方法。但是,如果您想继续使用 JSON,此函数(我对 this question, see an explanation of what it does there 的回答中的函数的直接副本)和过程将执行您想要的操作。
DELIMITER //
DROP FUNCTION IF EXISTS json_merge_sum //
CREATE FUNCTION json_sum_merge(IN j1 JSON, IN total JSON) RETURNS JSON
BEGIN
DECLARE knum INT DEFAULT 0;
DECLARE jkeys JSON DEFAULT JSON_KEYS(j1);
DECLARE kpath VARCHAR(30);
DECLARE v INT;
DECLARE l INT DEFAULT JSON_LENGTH(jkeys);
kloop: LOOP
IF knum >= l THEN
LEAVE kloop;
END IF;
SET kpath = CONCAT('$.', JSON_EXTRACT(jkeys, CONCAT('$[', knum, ']')));
SET v = JSON_EXTRACT(j1, kpath);
IF JSON_CONTAINS_PATH(total, 'one', kpath) THEN
SET total = JSON_REPLACE(total, kpath, JSON_EXTRACT(total, kpath) + v);
ELSE
SET total = JSON_SET(total, kpath, v);
END IF;
SET knum = knum + 1;
END LOOP kloop;
RETURN total;
END //
该过程类似于我的另一个答案中的过程,因为它会找到与 time_id
(指定为参数)的给定子字符串关联的所有不同标签,并对与每个标签关联的值求和.然后将各个标签和计数写入临时 table,然后根据时间段和标签名称从中进行选择。
DELIMITER //
DROP PROCEDURE IF EXISTS count_tags //
CREATE PROCEDURE count_tags(IN period VARCHAR(50))
BEGIN
DECLARE finished INT DEFAULT 0;
DECLARE timeval VARCHAR(20);
DECLARE knum, l INT;
DECLARE jkeys JSON;
DECLARE time_cursor CURSOR FOR SELECT DISTINCT time_id FROM tag_counter;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished=1;
CREATE TEMPORARY TABLE tag_counts (Time VARCHAR(20), Tag_Name VARCHAR(30), Tag_count_value INT, INDEX(Time, Tag_Name));
OPEN time_cursor;
time_loop: LOOP
FETCH time_cursor INTO timeval;
IF finished=1 THEN
LEAVE time_loop;
END IF;
SET @total = '{}';
SET @query = CONCAT("SELECT MIN(@total:=json_sum_merge(counters, @total)) INTO @json FROM TAG_COUNTER WHERE time_id='", timeval, "'");
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @query = CONCAT('INSERT INTO tag_counts VALUES(', period, ', ?, ?)');
PREPARE stmt FROM @query;
SET @timeval = timeval;
SET l = JSON_LENGTH(@total);
SET jkeys = JSON_KEYS(@total);
SET knum = 0;
key_loop: LOOP
IF knum >= l THEN
LEAVE key_loop;
END IF;
SET @k = JSON_EXTRACT(jkeys, CONCAT('$[', knum, ']'));
SET @t = JSON_EXTRACT(@total, CONCAT('$.', @k));
EXECUTE stmt USING @k, @t;
SET knum = knum + 1;
END LOOP key_loop;
DEALLOCATE PREPARE stmt;
END LOOP time_loop;
SELECT Time, Tag_Name, SUM(Tag_count_value) AS Tag_count_value FROM tag_counts GROUP BY Time, Tag_Name;
DROP TABLE tag_counts;
END
基于您先前 的一些有限样本数据的几个示例。在这些示例中,@timeval
等同于 time_id
列。输入数据:
account time_id counters
google 20180510 {"gmail_page_viewed": 2, "search_page_viewed": 51}
google 20180511 {"gmail_page_viewed": 3, "search_page_viewed": 102}
apple 20180511 {"apple_page_viewed": 5, "search_page_viewed": 16}
呼叫 count_tags('@timeval')
:
Time Tag_Name Tag_count_value
20180510 "gmail_page_viewed" 2
20180510 "search_page_viewed" 51
20180511 "apple_page_viewed" 5
20180511 "gmail_page_viewed" 3
20180511 "search_page_viewed" 118
呼叫 count_tags('SUBSTRING(@timeval, 1, 6)')
:
Time Tag_Name Tag_count_value
201805 "apple_page_viewed" 5
201805 "gmail_page_viewed" 5
201805 "search_page_viewed" 169
请注意,您还可以使用 json_sum_merge
来简化您的 INSERT
查询,例如
INSERT INTO `TAG_COUNTER`
(`account`, `time_id`, `counters`)
VALUES
('apple', '20180511', '{"apple_page_viewed": 9, "itunes_page_viewed": 4}')
ON DUPLICATE KEY UPDATE `counters` = json_sum_merge(VALUES(counters), counters)
结果:
account time_id counters
apple 20180511 {"apple_page_viewed": 14, "itunes_page_viewed": 4, "search_page_viewed": 16}
就您回答的具体问题而言:
- 没有。这个答案表明它可以用你现有的数据格式来完成。
- 不适用。
- 不适用。
- 是的,您可以坚持使用现有的
{"key" : "value"}
格式
- 由于我们必须遍历
tag_counter
的每个条目才能获得标签列表,因此索引对该部分无益。对于临时 table,我在 Time
和 Tag_Name
列上包含了索引,因为它们直接在 GROUP BY
子句中使用,所以应该有利于速度。
如果您要维护一个键列表(例如,在单独的 table 中,由 insert/update/delete 到 tag_counter
上的触发器维护)此代码可以变得更简单更有效率。但这是另一个问题。
我正在尝试使用 mysql json 功能识别时间序列上的趋势标签(基于最大点击数)。 下面是我的 table
CREATE TABLE TAG_COUNTER (
account varchar(36) NOT NULL,
time_id INT NOT NULL,
counters JSON,
PRIMARY KEY (account, time_id)
)
在每个网络 api 请求中,我将为每个帐户获取多个不同的标签,并且根据标签的数量,我将准备 INSERT ON DUPLICATE KEY UPDATE
查询。下面的示例显示了带有两个标签的插入。
INSERT INTO `TAG_COUNTER`
(`account`, `time_id`, `counters`)
VALUES
('google', '2018061023', '{"tag1": 1, "tag2": 1}')
ON DUPLICATE KEY UPDATE `counters` =
JSON_SET(`counters`,
'$."tag1"',
IFNULL(JSON_EXTRACT(`counters`,
'$."tag1"'), 0) + 1,
'$."tag2"',
IFNULL(JSON_EXTRACT(`counters`,
'$."tag2"'), 0) + 1
);
time_id 是 yyyyMMddhh,每行按小时聚合。
现在我的问题是检索 treding 标签。 下面的查询将为我提供 tag1 的聚合,但在进行此查询之前我们不会知道这些标签。
SELECT
SUBSTRING(time_id, 1, 6) AS month,
SUM(counters->>'$.tag1')
FROM TAG_COUNTER
WHERE counters->>'$.tag1' > 0
GROUP BY month;
所以我需要通用分组查询和排序依据来获取当时的趋势标签 hourly/daily/monthly。
预期的输出样本是
Time(hour/day/month) Tag_name Tag_count_value(total hits)
当我在网上搜索时,每个地方都提到如下
{"tag_name": "tag1", "tag_count": 1}
而不是直接 {"tag1" : 1}
他们在分组依据中使用 tag_name。
Q1) 那么,是否总是必须要有公知的 json 密钥来执行 group by ..?
Q2) 如果我必须采用这种方式,那么我的 INSERT ON DUPLICATE KEY UPDATE 查询对于这个新的 json [=74 有什么变化=]结构?由于计数器必须在不存在时创建,并且在存在时应加一。
Q3) 我必须维护对象数组吗
[
{"tag_name": "tag1", "tag_count": 2},
{"tag_name": "tag2", "tag_count": 3}
]
OR 对象的对象如下?
{
{"tag_name": "tag1", "tag_count": 2},
{"tag_name": "tag2", "tag_count": 3}
}
那么在 json 结构中,趋势计数的插入和检索哪个更好?
Q4) 我可以使用现有的 {"key" : "value"}
格式而不是 {"key_label" : key, "value_lable" : "value"}
并且可以提取趋势..?因为我认为 {"key" : "value"}
非常直截了当并且擅长表现。
Q5) 检索时我正在使用 SUBSTRING(time_id, 1, 6) AS month
。它可以使用索引吗?
或者我是否需要创建多个列,如 time_hour(2018061023)
、time_day(20180610)
、time_month(201806)
并在特定列上使用查询?
或者我可以使用 mysql date-time functions 吗?会使用索引来加快检索速度吗?
请帮忙。
我看不出你在这里使用 JSON 的充分理由。同样不清楚的是,为什么您认为 MySQL 中的“nosql 模式”会做得更好。
您可能需要的是这样的东西:
CREATE TABLE TAG_COUNTER (
account varchar(36) NOT NULL,
time_id INT NOT NULL,
tag_name varchar(50) NOT NULL,
counter INT UNSIGNED NOT NULL,
PRIMARY KEY (account, time_id, tag_name)
);
这将简化您的查询。 INSERT 语句如下所示:
INSERT INTO TAG_COUNTER
(account, time_id, tag_name, counter)
VALUES
('google', 2018061023, 'tag1', 1),
('google', 2018061023, 'tag2', 1)
ON DUPLICATE KEY UPDATE counter = counter + VALUES(counter);
SELECT 语句可能是这样的
SELECT
SUBSTRING(time_id, 1, 6) AS month,
tag_name,
SUM(counter) AS counter_agg
FROM TAG_COUNTER
GROUP BY month, tag_name
ORDER BY month, counter_agg DESC;
请注意,我没有尝试针对数据大小和性能优化 table/schema。那将是一个不同的问题。但是你必须看到,现在的查询要简单得多。
正如我在评论中所说,我认为远离 JSON 是可行的方法。但是,如果您想继续使用 JSON,此函数(我对 this question, see an explanation of what it does there 的回答中的函数的直接副本)和过程将执行您想要的操作。
DELIMITER //
DROP FUNCTION IF EXISTS json_merge_sum //
CREATE FUNCTION json_sum_merge(IN j1 JSON, IN total JSON) RETURNS JSON
BEGIN
DECLARE knum INT DEFAULT 0;
DECLARE jkeys JSON DEFAULT JSON_KEYS(j1);
DECLARE kpath VARCHAR(30);
DECLARE v INT;
DECLARE l INT DEFAULT JSON_LENGTH(jkeys);
kloop: LOOP
IF knum >= l THEN
LEAVE kloop;
END IF;
SET kpath = CONCAT('$.', JSON_EXTRACT(jkeys, CONCAT('$[', knum, ']')));
SET v = JSON_EXTRACT(j1, kpath);
IF JSON_CONTAINS_PATH(total, 'one', kpath) THEN
SET total = JSON_REPLACE(total, kpath, JSON_EXTRACT(total, kpath) + v);
ELSE
SET total = JSON_SET(total, kpath, v);
END IF;
SET knum = knum + 1;
END LOOP kloop;
RETURN total;
END //
该过程类似于我的另一个答案中的过程,因为它会找到与 time_id
(指定为参数)的给定子字符串关联的所有不同标签,并对与每个标签关联的值求和.然后将各个标签和计数写入临时 table,然后根据时间段和标签名称从中进行选择。
DELIMITER //
DROP PROCEDURE IF EXISTS count_tags //
CREATE PROCEDURE count_tags(IN period VARCHAR(50))
BEGIN
DECLARE finished INT DEFAULT 0;
DECLARE timeval VARCHAR(20);
DECLARE knum, l INT;
DECLARE jkeys JSON;
DECLARE time_cursor CURSOR FOR SELECT DISTINCT time_id FROM tag_counter;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished=1;
CREATE TEMPORARY TABLE tag_counts (Time VARCHAR(20), Tag_Name VARCHAR(30), Tag_count_value INT, INDEX(Time, Tag_Name));
OPEN time_cursor;
time_loop: LOOP
FETCH time_cursor INTO timeval;
IF finished=1 THEN
LEAVE time_loop;
END IF;
SET @total = '{}';
SET @query = CONCAT("SELECT MIN(@total:=json_sum_merge(counters, @total)) INTO @json FROM TAG_COUNTER WHERE time_id='", timeval, "'");
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @query = CONCAT('INSERT INTO tag_counts VALUES(', period, ', ?, ?)');
PREPARE stmt FROM @query;
SET @timeval = timeval;
SET l = JSON_LENGTH(@total);
SET jkeys = JSON_KEYS(@total);
SET knum = 0;
key_loop: LOOP
IF knum >= l THEN
LEAVE key_loop;
END IF;
SET @k = JSON_EXTRACT(jkeys, CONCAT('$[', knum, ']'));
SET @t = JSON_EXTRACT(@total, CONCAT('$.', @k));
EXECUTE stmt USING @k, @t;
SET knum = knum + 1;
END LOOP key_loop;
DEALLOCATE PREPARE stmt;
END LOOP time_loop;
SELECT Time, Tag_Name, SUM(Tag_count_value) AS Tag_count_value FROM tag_counts GROUP BY Time, Tag_Name;
DROP TABLE tag_counts;
END
基于您先前 @timeval
等同于 time_id
列。输入数据:
account time_id counters
google 20180510 {"gmail_page_viewed": 2, "search_page_viewed": 51}
google 20180511 {"gmail_page_viewed": 3, "search_page_viewed": 102}
apple 20180511 {"apple_page_viewed": 5, "search_page_viewed": 16}
呼叫 count_tags('@timeval')
:
Time Tag_Name Tag_count_value
20180510 "gmail_page_viewed" 2
20180510 "search_page_viewed" 51
20180511 "apple_page_viewed" 5
20180511 "gmail_page_viewed" 3
20180511 "search_page_viewed" 118
呼叫 count_tags('SUBSTRING(@timeval, 1, 6)')
:
Time Tag_Name Tag_count_value
201805 "apple_page_viewed" 5
201805 "gmail_page_viewed" 5
201805 "search_page_viewed" 169
请注意,您还可以使用 json_sum_merge
来简化您的 INSERT
查询,例如
INSERT INTO `TAG_COUNTER`
(`account`, `time_id`, `counters`)
VALUES
('apple', '20180511', '{"apple_page_viewed": 9, "itunes_page_viewed": 4}')
ON DUPLICATE KEY UPDATE `counters` = json_sum_merge(VALUES(counters), counters)
结果:
account time_id counters
apple 20180511 {"apple_page_viewed": 14, "itunes_page_viewed": 4, "search_page_viewed": 16}
就您回答的具体问题而言:
- 没有。这个答案表明它可以用你现有的数据格式来完成。
- 不适用。
- 不适用。
- 是的,您可以坚持使用现有的
{"key" : "value"}
格式 - 由于我们必须遍历
tag_counter
的每个条目才能获得标签列表,因此索引对该部分无益。对于临时 table,我在Time
和Tag_Name
列上包含了索引,因为它们直接在GROUP BY
子句中使用,所以应该有利于速度。
如果您要维护一个键列表(例如,在单独的 table 中,由 insert/update/delete 到 tag_counter
上的触发器维护)此代码可以变得更简单更有效率。但这是另一个问题。