基于Mysql json的趋势标签实现

时间:2018-05-11 17:45:41

标签: mysql json time-series analytics

我正在尝试使用mysql json功能识别时间序列中的趋势标签(基于最大命中数)。 下面是我的表

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;

所以我需要逐个查询的通用组以及按顺序获取每小时/每日/每月的趋势标签。

预期的输出样本是

Time(hour/day/month)  Tag_name  Tag_count_value(total hits)

当我搜索网络时,每个提到的地方都如下所示 {"tag_name": "tag1", "tag_count": 1}代替直接{"tag1" : 1} 他们在组中使用tag_name。

Q1)因此,总是必须使用公知的json密钥来执行分组..?

Q2)如果我必须采用这种方式,那么对于这个新的json标签/值struture,我的INSERT ON DUPLICATE KEY UPDATE查询的变化是什么?由于计数器必须在不存在时创建,并且在存在时应该增加1。

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}
}

那么在趋势计数的INSERT和RETRIEVAL的json结构中间哪个更好?

问题4)我可以使用现有的{"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吗?会使用索引来加快检索吗?

请帮忙。

2 个答案:

答案 0 :(得分:4)

我没有看到一个很好的理由,为什么你在这里使用JSON。还不清楚,为什么你认为MySQL中的“ nosql schema ”可以做得更好。

你可能需要的是这样的东西:

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;

请注意,我没有尝试针对数据大小和性能优化表/架构。那将是一个不同的问题。但是你必须看到,现在查询要简单得多。

答案 1 :(得分:1)

正如我在评论中所说,我认为远离JSON是要走的路。但是,如果你想继续使用JSON,这个函数(我对this question的答案的直接副本,看到它的作用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的给定子字符串(指定为参数)关联的所有不同标记,并对与每个标记关联的值求和。然后将各个标签和计数写入临时表,然后根据时间段和标签名称对其进行分组。

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

基于先前question的一些有限样本数据的几个示例。在这些示例中,@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}

就答案中的具体问题而言:

  1. 否。这个答案显示可以使用现有的数据格式完成。
  2. 不适用。
  3. 不适用。
  4. 是的,您可以坚持使用现有的{"key" : "value"}格式
  5. 由于我们必须遍历tag_counter的每个条目才能获取标记列表,因此索引对该部分无益。对于临时表,我在TimeTag_Name列中包含了索引,这些索引应该有利于速度,因为它们直接用在GROUP BY子句中。
  6. 如果你要维护一个密钥列表(例如,在一个单独的表中,由插入/更新/删除到tag_counter的触发器维护),这个代码可以变得更简单,更有效。但这是另一个问题。