【问题标题】:Need MySQL optimization for complex search on EAV structured data需要 MySQL 优化以对 EAV 结构化数据进行复杂搜索
【发布时间】:2014-02-20 03:47:31
【问题描述】:

我有一个大型数据库,其中包含必须可搜索和可分页的 EAV 结构化数据。我尝试了书中的每一个技巧以足够快地完成它,但在某些情况下,它仍然无法在合理的时间内完成。

这是我的表格结构(仅相关部分,如果需要更多请询问):

CREATE TABLE IF NOT EXISTS `object` (
  `object_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `oid` varchar(32) CHARACTER SET utf8 NOT NULL,
  `status` varchar(100) CHARACTER SET utf8 DEFAULT NULL,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`object_id`),
  UNIQUE KEY `oid` (`oid`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `version` (
  `version_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `type_id` bigint(20) NOT NULL,
  `object_id` bigint(20) NOT NULL,
  `created` datetime NOT NULL,
  `status` varchar(100) CHARACTER SET utf8 DEFAULT NULL,
  PRIMARY KEY (`version_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `value` (
  `value_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `object_id` int(11) NOT NULL,
  `attribute_id` int(11) NOT NULL,
  `version_id` bigint(20) NOT NULL,
  `type_id` bigint(20) NOT NULL,
  `value` text NOT NULL,
  PRIMARY KEY (`value_id`),
  KEY `field_id` (`attribute_id`),
  KEY `action_id` (`version_id`),
  KEY `form_id` (`type_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

这是一个示例对象。我的数据库中有大约 100 万 个。每个对象可能有不同数量的具有不同attribute_id的属性

INSERT INTO `owner` (`owner_id`, `uid`, `status`, `created`, `updated`) VALUES (1, 'cwnzrdxs4dzxns47xs4tx', 'Green', NOW(), NOW());
INSERT INTO `object` (`object_id`, `type_id`, `owner_id`, `created`, `status`) VALUES (1, 1, 1, NOW(), NOW());
INSERT INTO `value` (`value_id`, `owner_id`, `attribute_id`, `object_id`, `type_id`, `value`) VALUES (1, 1, 1, 1, 1, 'Munich');
INSERT INTO `value` (`value_id`, `owner_id`, `attribute_id`, `object_id`, `type_id`, `value`) VALUES (2, 1, 2, 1, 1, 'Germany');
INSERT INTO `value` (`value_id`, `owner_id`, `attribute_id`, `object_id`, `type_id`, `value`) VALUES (3, 1, 3, 1, 1, '123');
INSERT INTO `value` (`value_id`, `owner_id`, `attribute_id`, `object_id`, `type_id`, `value`) VALUES (4, 1, 4, 1, 1, '2012-01-13');
INSERT INTO `value` (`value_id`, `owner_id`, `attribute_id`, `object_id`, `type_id`, `value`) VALUES (5, 1, 5, 1, 1, 'A cake!');

现在谈谈我目前的机制。我的第一次尝试是 Mysql 的典型方法。对我需要的任何内容执行一个带有大量连接的大型 SQL。彻底的灾难!由于 RAM 耗尽,加载时间过长,甚至导致 PHP 和 MySQL 服务器崩溃。

所以我将查询分为几个步骤:

1 确定所有需要的attribute_id。

我可以在另一个引用对象的 type_id 的表中查找它们。结果是一个attribute_ids 列表。 (此表与性能关系不大,所以我的示例中没有包含。)

:type_id 包含我想要包含在搜索中的任何对象的所有 type_id。我已经在我的应用程序中获得了这些信息。所以这很便宜。

SELECT * FROM attribute WHERE form_id IN (:type_id)

结果是一个 type_id 整数数组。

2 搜索匹配的对象 编译一个大 SQL 查询,为我想要的每个条件添加一个 INNER JOIN。这听起来很可怕,但最终,这是最快的方法:(

典型的生成查询可能如下所示。遗憾的是 LIMIT 是必要的,否则我可能会得到太多的 ID,结果数组会使 PHP 爆炸或破坏下一个查询中的 IN 语句:

SELECT DISTINCT `version`.object_id FROM `version`
INNER JOIN `version` AS condition1 
        ON `version`.version_id = condition1.version_id 
       AND condition1.created = '2012-03-04' -- Filter by version date
INNER JOIN `value` AS condition2 
        ON `version`.version_id = condition2.version_id
       AND condition2.type_id IN (:type_id) -- try to limit joins to object types we need
       AND condition2.attribute_id = :field_id2 -- searching for a value in a specific attribute
       AND condition2.value = 'Munich' -- searching for the value 'Munich'
INNER JOIN `value` AS condition3 
        ON `version`.version_id = condition3.version_id
       AND condition3.type_id IN (:type_id) -- try to limit joins to object types we need
       AND condition3.attribute_id = :field_id3 -- searching for a value in a specific attribute
       AND condition3.value = 'Green' -- searching for the value 'Green'
WHERE `version`.type_id IN (:type_id) ORDER BY `version`.version_id DESC LIMIT 10000

结果将包含我可能需要的任何对象的所有 object_id。我选择 object_ids 而不是 version_ids,因为我需要匹配对象的所有版本,无论哪个版本匹配。

3 排序和分页结果 接下来,我将创建一个查询,按某个属性对对象进行排序,然后对结果数组进行分页。

SELECT DISTINCT object_id
FROM value
WHERE object_id IN (:foundObjects)
AND attribute_id = :attribute_id_to_sort
AND value > ''
ORDER BY value ASC LIMIT :limit OFFSET :offset

结果是来自先前搜索的对象 ID 的排序和分页列表

4 获取我们完整的对象、版本和属性 在最后一步中,我将为以前的查询找到的任何对象和版本选择所有值。

SELECT `value`.*, `object`.*, `version`.*, `type`.*
`object`.status AS `object.status`,
`object`.flag AS `object.flag`,
`version`.created AS `version.created`,
`version`.status AS `version.status`,
FROM version
INNER JOIN `type` ON `version`.form_id = `type`.type_id
INNER JOIN `object` ON `version`.object_id = `object`.object_id
LEFT JOIN value ON `version`.version_id = `value`.version_id
WHERE version.object_id IN (:sortedObjectIds) AND `version.type_id IN (:typeIds)
ORDER BY version.created DESC

然后将通过 PHP 将结果编译成漂亮的 object->version->value 数组结构。


现在的问题

  • 能否以任何方式加速这整个混乱局面?
  • 我能否以某种方式从我的搜索查询中移除 LIMIT 10000 限制?

如果一切都失败了,也许切换数据库技术?请参阅我的另一个问题:Database optimized for searching in large number of objects with different attributes


现实生活样本

表格大小:对象 - 193801 行,版本 - 193841 行,值 - 1053928 行

SELECT * FROM attribute WHERE attribute_id IN (30)

SELECT DISTINCT `version`.object_id
FROM version  
INNER JOIN value AS condition_d4e328e33813 
     ON version.version_id = condition_d4e328e33813.version_id
    AND condition_d4e328e33813.type_id IN (30)
    AND condition_d4e328e33813.attribute_id IN (377) 
    AND condition_d4e328e33813.value LIKE '%e%'  
INNER JOIN value AS condition_2c870b0a429f 
     ON version.version_id = condition_2c870b0a429f.version_id
    AND condition_2c870b0a429f.type_id IN (30)
    AND condition_2c870b0a429f.attribute_id IN (376) 
    AND condition_2c870b0a429f.value LIKE '%s%' 
WHERE version.type_id IN (30) 
ORDER BY version.version_id DESC LIMIT 10000 -- limit to 10000 or it breaks!

解释:

id  select_type  table                   type      possible_keys                key         key_len ref                               rows      Extra   
1   SIMPLE       condition_2c870b0a429f  ref       field_id,action_id,form_id   field_id    4       const                             178639    Using where; Using temporary; Using filesort
1   SIMPLE       action                  eq_ref    PRIMARY                      PRIMARY     8       condition_2c870b0a429f.action_id  1         Using where
1   SIMPLE       condition_d4e328e33813  ref       field_id,action_id,form_id   action_id   8       action.action_id                  11        Using where; Distinct

对象搜索完成(峰值 RAM:5.91MB,时间:4.64s)

SELECT DISTINCT object_id
FROM version
WHERE object_id IN (193793,193789, ... ,135326,135324) -- 10000 ids in here!
ORDER BY created ASC
LIMIT 50 OFFSET 0                                                  

对象排序完成(峰值 RAM:6.68MB,时间:0.352s)

SELECT `value`.*, object.*, version.*, type.*,
    object.status AS `object.status`,
    object.flag AS `object.flag`,
    version.created AS `version.created`,
    version.status AS `version.status`,
    version.flag AS `version.flag`
FROM version
INNER JOIN type ON version.type_id = type.type_id
INNER JOIN object ON version.object_id = object.object_id
LEFT JOIN value ON version.version_id = `value`.version_id
WHERE version.object_id IN (135324,135326,...,135658,135661) AND version.type_id IN (30)
ORDER BY quality DESC, version.created DESC 

对象加载查询完成(峰值 RAM:6.68MB,时间:0.083s)
对象编译成数组完成(峰值内存:6.68MB,时间:0.007s)

【问题讨论】:

  • 大概value_id 没有任何意义——您可以像使用PK 一样轻松地使用(object_id,attribute_id) 吗?对于给定的对象,owner_id 和 type 总是相同的(在value 表中如此冗余?
  • value_id 没有意义。总是添加一个 id 列只是一种习惯。它会减慢任何速度吗?
  • 要明确一点,以目前的机制,速度已经足够公平了。问题是我将搜索结果限制为 10000 个返回的 ID。如果我构建了一个多合一查询,那么速度只是一个问题,但是由于 MySQL 不能做 EAV 数据立方体,所以我无论如何也做不到。至少据我所知。

标签: php mysql sql database-design entity-attribute-value


【解决方案1】:

尝试在您的搜索查询前添加一个 EXPLAIN :

EXPLAIN SELECT DISTINCT `version`.object_id FROM `version`, etc ...

然后检查“Extra”列中的结果,它将为您提供一些加快查询速度的线索,例如在正确的字段中添加 INDEX。

有时您还可以移除INNER JOIN,在您的Mysql 响应中获得更多结果,并通过使用PHP 循环处理来过滤大数组。

【讨论】:

  • 稍后我将添加一些现实生活中的 Selects with Explain。
【解决方案2】:

我会首先尝试覆盖索引(即:所有列与您查询的条件相匹配,甚至作为结果提取)。这样引擎就不必返回原始页面数据。

因为您需要版本中的“object_id”,并使用“version_id”作为其他表的连接基础。您的版本表在 TYPE_ID 上还有一个 WHERE 子句,所以我会有一个索引

版本表 -- (object_id, version_id, type_id)

对于你的“价值”表,也匹配那里的标准

值表——(version_id、type_id、attribute_id、value、created)

【讨论】:

  • 我以为我添加了所有需要的索引,我错过了什么吗? (解释如下shorty...)无论如何,当前机制的问题是速度不如10000个结果的限制。
猜你喜欢
  • 1970-01-01
  • 2013-12-26
  • 2012-04-16
  • 2011-05-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-12
  • 1970-01-01
相关资源
最近更新 更多