如何为海量表设置复杂的多列索引

n0n*_*g0n 5 mysql myisam index index-tuning

我已经搜索了我可以在 Google、SO 等上搜索的内容,但没有找到似乎适合我正在寻找的答案。我很犹豫要不要就此发表一篇文章,因为我确定答案就在某个地方,我只是找不到它:-\

我已经在 MySQL 中设置了一个表,我真的可以使用一些见解。我有一个现有的索引,它在某些情况下运行良好,而在其他情况下则需要 20 多秒才能运行。让我给你一些背景。

这是一个带有固定行的 MyISAM 表(每列都是一个 INT(11) ),该表目前有 150,000,000 行(总共 10.2 GB)。我用它来跟踪在我们使用的网络软件上运行的分析,因为其他开源替代品(如 Piwik)简直是矫枉过正,我们需要直接访问“东西”。除此之外,还有表格的基本结构。(再次所有 INT(11) ,因为我阅读并理解具有相同类型和长度的索引最有效)。

id    region_id location_id action_id visitor_id ts        url_id employee_id
Run Code Online (Sandbox Code Playgroud)

这是每列的以下值范围。

150M  <20       <3000      <10       <40M        unix_time <20k   <200k
Run Code Online (Sandbox Code Playgroud)

我尝试运行的查询基本上是获取在特定时间对某个位置执行特定操作的所有访问者的不同计数。(换句话说,查询中必须有位置、动作和 ts)

SELECT COUNT(DISTINCT visitor_id) FROM table WHERE location_id = # AND region_id = # AND action_id = # AND ts BETWEEN x AND y
Run Code Online (Sandbox Code Playgroud)

我有一个关于位置、区域、操作、ts 的多列索引,执行此查询可能需要 20 到 30 秒。我还有另外一个索引,它是简单的visitor_id,ts,我认为它没有显示任何问题,因为visitor_id 具有如此高的基数。EXPLAIN SELECT 显示我正在点击索引,它似乎做得和它所能做的一样好。

id select_type table   type  possible_keys       key      key_len ref  rows  Extra
1  SIMPLE      table   range visitor_id,location location 13      NULL 23964 Using where
Run Code Online (Sandbox Code Playgroud)

几天前,该索引曾经是区域、位置、ts(无操作),我自己做了一些实验。我用最多 55,000,000 行的虚拟数据填充了一个表,只是为了看看什么样的索引会给我带来改进,尽管我刚刚尝试的索引在 55,000,000 行时工作得很好,但在 150,000,000 行时并没有真正提高多少。

我尝试过的另一件事是,此查询实际上是为报告提取日期,如“获取第 x 天和第 y 天或过去 90 天之间的所有访问者”。前段时间我尝试制作另一列,仅存储相当于仅表示日期的 unix 时间戳(因此索引的基数会小得多)。奇怪的是,唯一似乎真正做的是通过添加一列来增加表的大小,并没有真正帮助其他任何事情(但这也是几个月前的可能大约 50,000,000 行标记,也许差异不是很大交易回来)

我知道您通常应该让索引从高基数变为低基数。我也知道 MySQL 在范围查询上会崩溃,如果我将 ts 放在索引的开头,那么索引的其余部分就没有用了。

我向您寻求帮助的问题是我该怎么做才能缩短运行此查询所需的时间?我真的到了需要每 50M 行左右拆分这张表的地步吗?是否没有可以保存该表的索引,或者我真的不知道如何设置索引?我也愿意接受其他看似非正统的替代方案,但至少可以缩短查询时间。

更新

自发布此问题以来,该表现在有近 1.59 亿行。看起来目前它正在以每月大约 1000 万的速度增长,这当然会呈指数增长。在这一点上,随着这种疯狂的增长,我最好将表格分成几个月或类似的内容吗?

更新更新 10/5/17

表现在有 8 亿,并且仍然可以通过非常快速的查询来保持强劲。表80GB左右,数据30GB,索引50GB

Tho*_*ser 4

回答你的第二个问题:

MySQL没有并行查询执行引擎,因此即使对查询进行分区,仍然是单线程的。这最终会杀死你的体重秤。

但是,您可以按 分区表visitor_id。这将允许您并行运行多个查询(每个分区一个),所有查询都形成:

SELECT COUNT(DISTINCT visitor_id) 
FROM table WHERE location_id = # 
AND region_id = # 
AND action_id = # AND ts BETWEEN x AND y
AND visitor_id BETWEEN <partition_start> and <partition_end>
Run Code Online (Sandbox Code Playgroud)

这些并行查询的输出(您可以在运行时将其存储在临时表中)只需将不同的计数添加在一起即可轻松组合到最终结果中。

这与分片非常相似,但不是跨机器进行,而是在同一个表上进行。通过选择一个好的散列函数来生成 guest_id(例如,如果原始 id 是使用 AUTO_INCRMENT 生成的,则为模数或位反转),您可以确保所有分区的大小大致相等。

visitor_id您想要按而不是其他列之一进行分区的原因是它使 DISTINCT 跨分区相加。例如,考虑一个具有两个分区的表。一次保持visitor_id0-99,一次保持100-199。您现在可以表达两个可以并行运行的查询:

INSERT INTO TempResult(visitor_id)
SELECT COUNT(DISTINCT visitor_id) 
    FROM table WHERE location_id = # 
    AND region_id = # 
    AND action_id = # AND ts BETWEEN x AND y
    AND visitor_id BETWEEN 0 and 99
Run Code Online (Sandbox Code Playgroud)

与此并行的是:

INSERT INTO TempResults (visitor_id)
SELECT COUNT(DISTINCT visitor_id) 
    FROM table WHERE location_id = # 
    AND region_id = # 
    AND action_id = # AND ts BETWEEN x AND y
    AND visitor_id BETWEEN 100 and 199
Run Code Online (Sandbox Code Playgroud)

因为你知道visitor_id分区之间不重叠,所以最终结果是:

SELECT SUM(visitor_id) FROM TempResults
Run Code Online (Sandbox Code Playgroud)

当然,您需要以分区大小大致相同的方式选择分区边界。

我会让 ypercube 提交索引问题的答案,因为这是值得奖励的答案。