如何正确预热 MySQL FULLTEXT 索引?

How to properly warm a MySQL FULLTEXT index?

我在 Amazon RDS 上安装了 MySQL V5.6.23 运行ning。其中有一个名为 product_details 的 InnoDB table,它包含大约 10 列,这些列都针对精确匹配(日期、数字、文本等)进行了索引。然后我有一个 product_name 字段,我已经在上面放置了 FULLTEXT 索引。我还有很多我们不搜索的其他字段。

table目前有150M行,我们每晚添加大约3-5M,每晚还会更新另外10-20M。在晚上 运行 处理这些 inserts/updates 之后,全文索引似乎从内存中删除(不确定到底发生了什么)。

当我第一次 运行 查询 'blue ford taurus' 时,查询最多可能需要几分钟。第二次我运行它,不是几百毫秒就是几秒。如果我 运行 OPTIMIZE TABLE product_details; 在新数据处理完成后,那么几乎我测试的每个搜索都尽可能快。这需要几个小时才能 运行 OPTIMIZE TABLE (因为我认为它正在重写整个 table (和索引?)?!?!

我曾考虑过创建一个 "warming" 脚本,该脚本将针对用户的常见查询发送 table,但我对正在发生的事情没有很好的心理模型,所以我不知道这会热身什么。搜索 'blue ford taurus' 似乎不仅加快了该查询的速度,但我不明白为什么。

问题

  1. 每天晚上加载新数据后,应该如何适当地预热这些索引?此 table 支持最终用户每天早上搜索它的网络应用程序。

  2. 我如何知道保存索引所需的内存要求?

评论

  1. 我打算将这一切转移到 Elasticsearch(或类似的),我在其中有很多搜索经验。我不熟悉 MySQL 作为 FULLTEXT "search engine," 但目前我坚持使用它。

普通查询

SELECT * FROM product_details as pd 
WHERE
    MATCH (pd.product_name) AGAINST ('+ipod +nano' IN BOOLEAN MODE)
    and pd.city_id IN (577,528,567,614,615,616,618) 
ORDER BY(pd.timestamp) DESC
LIMIT 1000;

Table

CREATE TABLE `product_details` (
  `product_name` text NOT NULL,
  `category_name` varchar(100) NOT NULL,
  `product_description` text NOT NULL,
  `price` int(11) NOT NULL,
  `address` varchar(200) NOT NULL,
  `zip_code` varchar(30) NOT NULL DEFAULT '',
  `phone` bigint(10) DEFAULT NULL,
  `email` varchar(50) NOT NULL,
  `state` varchar(20) NOT NULL,
  `city` varchar(30) NOT NULL,
  `post_id` bigint(11) NOT NULL,
  `post_date` date DEFAULT NULL,
  `post_time` time NOT NULL,
  `updated_date` varchar(10) NOT NULL,
  `updated_time` time NOT NULL,
  `status` tinyint(4) NOT NULL,
  `timestamp` date NOT NULL,
  `new_field` tinyint(4) DEFAULT NULL,
  `multiple_items` tinyint(1) NOT NULL,
  `city_id` int(4) NOT NULL,
  `date_changed` date DEFAULT NULL,
  `latlong` varchar(100) NOT NULL,
  PRIMARY KEY (`post_id`),
  KEY `city_id` (`city_id`),
  KEY `post_date` (`post_date`),
  KEY `price` (`price`),
  KEY `category_name` (`category_name`),
  KEY `state` (`state`),
  KEY `multiple_items` (`multiple_items`),
  KEY `new_field` (`new_field`),
  KEY `phone` (`phone`),
  KEY `timestamp` (`timestamp`),
  KEY `date_changed` (`date_changed`),
  FULLTEXT KEY `product_name` (`product_name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Table状态

上面的 table 状态数据实际上是我的开发 table 的,其中只有 18M 行。当我加载所有生产数据时,它的数据量将增加约 8 倍,这意味着 data_length 将约为 70GB,而 index_length 将约为 32GB。

优化(或不优化)。是的 OPTIMIZE TABLE 复制 table 并重建所有索引,因此需要很长时间。不要运行OPTIMIZE;它几乎没有帮助。 (或者您看到显着变化了吗?)

调整。你有多少内存?索引有多大? SHOW TABLE STATUS.

innodb_buffer_pool_size 应该是 可用 RAM 的大约 70%。

缩小架构 会有一点帮助:

  • DATETIME 分成两个字段很少见好
  • 为什么这个table里面同时有citycity_id。也许您应该将 citystate 以及 zip_code 标准化为另一个 table(一个,而不是另外两个 table)。
  • id 的大小应该适当——city_id 可以是 SMALLINT UNSIGNED(2 个字节:0..65535)而不是 INT SIGNED(4 个字节)。
  • 标准化 category_name 和任何其他重复的列?
  • updated_date 是一个 VARCHAR??

您查询的步骤

  1. 找到同时具有 ipad 和 nano 的产品的所有 ID。假设有 5555 行这样的行。
  2. 去所有 5555 行,收集所需的信息,因为 *,这是所有列。听起来 table 比 RAM 大很多,所以这意味着大约 5555 次磁盘读取——可能是最慢的部分。
  3. 根据city_id过滤掉不需要的行。假设我们减少到 3210 行。
  4. 将所有 3210 行的所有列写入 tmp table。由于有一个 TEXT 列,它将是一个 MyISAM table,而不是更快的 MEMORY table.
  5. 排序 timestamp
  6. 交付前 1000 个。

我希望您能看到,大行意味着 tmp 中的内容大 table。减少 * and/or 收缩列。

这里有一个技巧 可以减少 tmp table 大小(步骤 4、5、6):

SELECT  ...
    FROM  product_details as pd
    JOIN  
      ( SELECT  post_id
            FROM  product_details
            WHERE  MATCH (product_name) AGAINST ('+ipod +nano' IN BOOLEAN MODE)
              and  city_id IN (577,528,567,614,615,616,618)
            ORDER BY timestamp DESC
            LIMIT  1000
      ) x USING (post_id)
    ORDER BY  pd.timestamp;

然而,tmp table 并不是最糟糕的部分,这需要进行第二次排序。所以,你可以试试这个,但不要屏住呼吸。

请注意,当您运行一个测试时,很可能会I/O-bound,运行它两次。第二个 运行 将是一个更公平的比较,因为它大概没有 I/O.

再加一层应该会更快:

SELECT  pd...
    FROM  
      ( SELECT  post_id
            FROM  product_details
            WHERE  MATCH (product_name) AGAINST ('+ipod +nano' IN BOOLEAN MODE) 
      ) AS a
    JOIN  product_details AS b ON b.post_id = a.post_id
    WHERE  b.city_id IN (577,528,567,614,615,616,618)
    ORDER BY  b.timestamp DESC
    LIMIT  1000 ) x
    JOIN  product_details as pd ON pd.post_id = b.post_id
    ORDER BY  pd.timestamp;

INDEX(post_id, city_id, timestamp) -- also required for this formulation

这个公式的希望是

  • 二次过滤(city_id)是在更小的 BTree(那个索引)上完成的,因此更有可能存在于 RAM 中,从而避免了 I/O.
  • 大 table 只需要 1000 个探针。这应该大获全胜。

步骤:

  1. 从 FULLTEXT 索引中获取 5555 个 id。
  2. 使用 希望 成为内存操作的方式过滤到 3210 个 ID。
  3. 排序 3210 'narrow' 行(仅 3 列,不是全部)。这次可以是 MEMORY tmp table.
  4. JOIN回原table仅1000次。 (大赢。)(我在这里可能是错的;它可能是 3210,但仍然比 5555 好。)
  5. 交付结果。