何时应该使用复合索引?

160
  1. 何时应该在数据库中使用复合索引?
  2. 使用复合索引会有哪些性能影响?
  3. 为什么应该使用复合索引?

例如,我有一个名为homes的表:

CREATE TABLE IF NOT EXISTS `homes` (
  `home_id` int(10) unsigned NOT NULL auto_increment,
  `sqft` smallint(5) unsigned NOT NULL,
  `year_built` smallint(5) unsigned NOT NULL,
  `geolat` decimal(10,6) default NULL,
  `geolng` decimal(10,6) default NULL,
  PRIMARY KEY  (`home_id`),
  KEY `geolat` (`geolat`),
  KEY `geolng` (`geolng`),
) ENGINE=InnoDB  ;

对于我是否使用一个包含geolatgeolng的组合索引有意义,例如:

我将其替换为:

  KEY `geolat` (`geolat`),
  KEY `geolng` (`geolng`),

用于:

KEY `geolat_geolng` (`geolat`, `geolng`)

如果是这样的话:

  • 为什么?
  • 使用组合索引会有什么性能影响?

更新:

由于许多人已经说到它完全取决于我执行的查询,下面是最常见的查询:

SELECT * FROM homes
WHERE geolat BETWEEN ??? AND ???
AND geolng BETWEEN ??? AND ???

更新2:

给定以下数据库模式:

CREATE TABLE IF NOT EXISTS `homes` (
  `home_id` int(10) unsigned NOT NULL auto_increment,
  `primary_photo_group_id` int(10) unsigned NOT NULL default '0',
  `customer_id` bigint(20) unsigned NOT NULL,
  `account_type_id` int(11) NOT NULL,
  `address` varchar(128) collate utf8_unicode_ci NOT NULL,
  `city` varchar(64) collate utf8_unicode_ci NOT NULL,
  `state` varchar(2) collate utf8_unicode_ci NOT NULL,
  `zip` mediumint(8) unsigned NOT NULL,
  `price` mediumint(8) unsigned NOT NULL,
  `sqft` smallint(5) unsigned NOT NULL,
  `year_built` smallint(5) unsigned NOT NULL,
  `num_of_beds` tinyint(3) unsigned NOT NULL,
  `num_of_baths` decimal(3,1) unsigned NOT NULL,
  `num_of_floors` tinyint(3) unsigned NOT NULL,
  `description` text collate utf8_unicode_ci,
  `geolat` decimal(10,6) default NULL,
  `geolng` decimal(10,6) default NULL,
  `display_status` tinyint(1) NOT NULL,
  `date_listed` timestamp NOT NULL default CURRENT_TIMESTAMP,
  `contact_email` varchar(100) collate utf8_unicode_ci NOT NULL,
  `contact_phone_number` varchar(15) collate utf8_unicode_ci NOT NULL,
  PRIMARY KEY  (`home_id`),
  KEY `customer_id` (`customer_id`),
  KEY `city` (`city`),
  KEY `num_of_beds` (`num_of_beds`),
  KEY `num_of_baths` (`num_of_baths`),
  KEY `geolat` (`geolat`),
  KEY `geolng` (`geolng`),
  KEY `account_type_id` (`account_type_id`),
  KEY `display_status` (`display_status`),
  KEY `sqft` (`sqft`),
  KEY `price` (`price`),
  KEY `primary_photo_group_id` (`primary_photo_group_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=8 ;

使用以下SQL语句:

EXPLAIN SELECT  homes.home_id,
                    address,
                    city,
                    state,
                    zip,
                    price,
                    sqft,
                    year_built,
                    account_type_id,
                    num_of_beds,
                    num_of_baths,
                    geolat,
                    geolng,
                    photo_id,
                    photo_url_dir
            FROM homes
            LEFT OUTER JOIN home_photos ON homes.home_id = home_photos.home_id
                AND homes.primary_photo_group_id = home_photos.home_photo_group_id
                AND home_photos.home_photo_type_id = 2
            WHERE homes.display_status = true
            AND homes.geolat BETWEEN -100 AND 100
            AND homes.geolng BETWEEN -100 AND 100

EXPLAIN的作用是返回查询的执行计划:

id  select_type  table        type  possible_keys                                    key                  key_len  ref     rows  Extra
----------------------------------------------------------------------------------------------------------
1   SIMPLE       homes        ref   geolat,geolng,display_status                     display_status       1        const   2     Using where
1  SIMPLE        home_photos  ref   home_id,home_photo_type_id,home_photo_group_id   home_photo_group_id  4        homes.primary_photo_group_id   4  

我不太明白如何阅读EXPLAIN命令。这个看起来好还是坏?目前,我没有使用geolat和geolng的组合索引。我应该使用吗?

9个回答

130

当您使用可以从中受益的查询时,应使用复合索引。复合索引看起来像这样:

index( column_A, column_B, column_C )

使用这些字段进行连接、过滤和有时选择的查询将获益。它还将使使用该复合列左侧子集的查询受益。因此,上述索引也将满足需要的查询。

index( column_A, column_B, column_C )
index( column_A, column_B )
index( column_A )

但它不会(至少不是直接的,如果没有更好的索引可能会在一定程度上有所帮助)对需要查询的内容有帮助。

index( column_A, column_C )

请注意,column_B 丢失了。

在你原来的例子中,针对两个维度的复合索引将主要有利于同时查询这两个维度或仅查询左侧维度而非仅查询右侧维度的查询。如果你总是查询两个维度,那么使用复合索引是正确的选择,不太重要哪个先出现(最可能的情况)。


2
马克,我更新了我的原始帖子(更新2)。这是我的实际查询。我的实际数据库模式。以及EXPLAIN指令返回的内容。因此,有了这些信息 - 我应该使用复合索引吗?我还是不清楚。先感谢您。 - Teddy
1
-1 是因为复合索引对于 WHERE geolat BETWEEN ??? AND ??? AND geolng BETWEEN ??? AND ??? 这种查询条件没有帮助。它只会在第一个字段上停止。"Question Overflow" 上的答案解释了原因。 - Rick James
密钥通常具有支持它们的索引。因此,很可能不需要,但我仍然会检查您特定的SQL引擎如何工作以及哪些索引显示出来(作为PK规范的结果)。 - Mark Canlas
1
我真正想知道的是:相对于每个列上的单独索引,复合索引有什么好处? - felwithe
2
@felwithe MySQL在查询中每个表只能使用一个索引(有例外,例如索引合并)。这意味着查询中的表必须为所有where子句、表连接、group-by和order-by使用单个索引。因此,每个列上的单独索引可能并不总是有效,但组合索引可以发挥魔力。 - Akhil Mathew
显示剩余4条评论

67

假设你有以下三个查询:

查询 I:

SELECT * FROM homes WHERE `geolat`=42.9 AND `geolng`=36.4

查询 II:

SELECT * FROM homes WHERE `geolat`=42.9

查询 III:

SELECT * FROM homes WHERE `geolng`=36.4

如果每列都有单独的索引,那么三个查询都会使用索引。在MySQL中,如果你有一个复合索引(geolat, geolng),只有查询I和查询II(使用复合索引的第一部分)使用索引。在这种情况下,查询III需要全表搜索。
在手册的多列索引章节中,清楚地解释了多列索引的工作原理,因此我不想重复手册内容。
MySQL参考手册页面
多列索引可以被视为包含通过连接索引列的值创建的值的排序数组。
如果您为geolat和geolng列使用单独的索引,则在表中有两个不同的索引,您可以独立搜索它们。
INDEX geolat
-----------
VALUE RRN
36.4  1
36.4  8
36.6  2
37.8  3
37.8  12
41.4  4

INDEX geolng
-----------
VALUE RRN
26.1  1
26.1  8
29.6  2
29.6  3
30.1  12
34.7  4

如果您使用复合索引,您只需要一个索引即可涵盖两个列:

INDEX (geolat, geolng)
-----------
VALUE      RRN
36.4,26.1  1
36.4,26.1  8
36.6,29.6  2
37.8,29.6  3
37.8,30.1  12
41.4,34.7  4

RRN指相对记录号(简单来说,可以称为ID)。前两个索引是分开生成的,第三个索引是复合索引。如您所见,可以基于复合索引的geolat进行搜索,因为它被geolat索引,但也可以通过geolat或“geolat AND geolng”进行搜索(因为geolng是第二级索引)。
另外,请查看MySQL如何使用索引手册部分。

2
实际上,我没有那些查询。我的查询在原始帖子中列出。我的查询是返回一个正方形网格内的房屋。我知道空间索引,我不想计算距离。我只想知道当我尝试显示特定地理网格(例如邻里/城市/县)内的所有房屋时,使用复合索引是否有意义。 - Teddy
1
Eyazici,我已经更新了我的原始帖子(更新2)。这是我的实际查询。我实际的数据库架构。以及EXPLAIN命令的返回结果。所以,根据这些信息,我应该使用复合索引吗?我还不太清楚。提前感谢你的帮助。 - Teddy
1
"实际上,我没有任何这些查询。" 实际上,你有,我已经使用简单的 WHERE 条件来解释基本逻辑。当在列上使用条件语句(如WHERE)时,MySQL 会尽可能地使用索引。"x BETWEEN a AND b" 类似于 "x>a AND x<b"。你在查询条件中使用了 geolng 和 geolat 两个列。如果你使用复合索引 "(geolat, geolng)",你的 "AND geolng BETWEEN ??? AND ???" 条件就无法获得索引的优势(这是针对 MySQL 的)。所以,你应该为你的场景使用每个列的单独索引。 - Emre Yazici
是的,如果你的条件只是"geolat BETWEEN ??? AND ??? AND geolng BETWEEN ??? AND ???",那么你应该使用复合索引。 - Emre Yazici
1
不。当遇到“范围”(如BETWEEN)时,将不会考虑索引的其他字段!因此,复合索引也不会更好。 - Rick James
显示剩余7条评论

24
可能有一个关于复合索引的误解。许多人认为只要“where”子句涵盖了索引列,例如您的情况中的“geolat”和“geolng”,就可以使用复合索引来优化搜索查询。让我们深入探讨一下:
我相信您的房屋坐标数据将是随机小数,例如:
home_id  geolat  geolng
   1    20.1243  50.4521
   2    22.6456  51.1564
   3    13.5464  45.4562
   4    55.5642 166.5756
   5    24.2624  27.4564
   6    62.1564  24.2542
...

由于geolatgeolng的值几乎不会重复。在geolatgeolng上创建一个组合索引看起来像这样:

index_id  geolat  geolng
   1     20.1243  50.4521
   2     20.1244  61.1564
   3     20.1251  55.4562
   4     20.1293  66.5756
   5     20.1302  57.4564
   6     20.1311  54.2542
...

因此,复合索引的第二列基本上是无用的!使用复合索引进行查询的速度可能与仅在geolat列上建立索引的速度相似。
正如Will所提到的,MySQL提供了空间扩展支持。空间点存储在单个列中,而不是两个分开的lat和lng列中。空间索引可以应用于这样的列。然而,根据我的个人经验,效率可能被高估了。可能是空间索引并没有解决二维问题,而只是加速了使用带有二次分裂的R-Trees进行搜索。
权衡的是,空间点消耗更多的内存,因为它使用八字节的双精度数字来存储坐标。如果我错了,请纠正我。

8

复合索引在IT技术中非常有用,可用于:

  • 0个或多个“=”子句,加上
  • 最多一个范围子句。

复合索引无法处理两个范围。我在我的索引手册中进一步讨论了这个问题。

查找最近的 - 如果问题确实是关于优化的,则可以使用此方法。

WHERE geolat BETWEEN ??? AND ???
  AND geolng BETWEEN ??? AND ???

那么,没有一个索引能真正处理两个维度。

相反,必须要有“跳出思维定式”的想法。如果一个维度通过分区实现,另一个维度通过仔细选择PRIMARY KEY实现,则可以为非常大的lat / lng查找表获得显着更高的效率。我的latlng博客详细介绍了如何在全球范围内实现“查找最近的位置”。它包括代码。

PARTITIONs是纬度范围的条纹。 PRIMARY KEY故意以经度开头,以便有用的行可能在同一块中。存储过程编排混乱的代码来执行order by ... limit ...以及在目标周围增加“正方形”直到具有足够的咖啡店(或其他内容)。 它还负责计算大圆并处理日期线和极点。

更多信息

我写了另一篇博客;它比较了5种进行lat / lng搜索的方法:http://mysql.rjweb.org/doc.php/latlng#representation_choices(它将上面提供的链接作为其中5种之一的参考)。其中另一种方法是这样的,并指出它们对于特定情况来说是最佳的

INDEX(geolat, geolng),
INDEX(geolng, geolat)

那就是,两个索引中都有这两列,并且没有在geolat和geolng上建立单列索引是很重要的。

7

组合索引非常强大,因为它们可以:

  • 强制执行结构完整性
  • 在经过筛选的 ID 上启用排序

强制执行结构完整性

组合索引不仅仅是另一种类型的索引;它们可以通过将主键作为强制执行来为表提供必要的结构完整性。

Mysql 的 Innodb 支持聚集,并且以下示例说明了为什么可能需要组合索引。

为了创建一个朋友表(即针对社交网络),我们需要 2 列:user_id, friend_id

表结构:

user_id (medium_int)
friend_id (medium_int)

Primary Key -> (user_id, friend_id)

按照惯例,主键(PK)是唯一的。通过创建一个组合主键,Innodb会在添加新记录时自动检查user_id, friend_id是否存在重复。这是期望的行为,因为没有用户应该有超过1条与friend_id = 2的记录(关系链)。

如果没有组合主键,我们可以使用代理键来创建这个模式:

user_friend_id
user_id
friend_id

Primary Key -> (user_friend_id)

现在,每当添加一条新记录时,我们都需要检查是否已经存在具有组合“用户ID、好友ID”的先前记录。因此,复合索引可以强制执行结构完整性。
启用对过滤ID的排序非常常见,通常是按照帖子的时间戳或日期时间对一组记录进行排序。以下是一个示例:
用户墙帖子表(类似Facebook的墙帖子)。
user_id (medium_int)
timestamp (timestamp)
author_id (medium_int)
comment_post (text)

Primary Key -> (user_id, timestamp, author_id)

我们希望查询并找到所有user_id = 10的帖子,并按评论帖子的时间戳(日期)排序。
SQL查询
SELECT * FROM User_Wall_Posts WHERE user_id = 10 ORDER BY timestamp DES

复合主键使得Mysql可以使用索引来过滤和排序结果;Mysql不需要使用临时文件或文件排序来获取结果。如果没有复合主键,这将是不可能的,并且会导致查询非常低效。
因此,复合主键非常强大,适用于不仅仅是简单的问题,“我想搜索column_a,column_b,所以我将使用复合主键。” 对于我的当前数据库架构,我有与单个键一样多的复合键。不要忽视复合键的用处!

PRIMARY KEY 强制完整性,因为它是 UNIQUE 的;而复合键则是次要的。 - Rick James

1

当您想要优化group by子句时,复合索引可能非常有用(请查看此文章http://dev.mysql.com/doc/refman/5.0/en/group-by-optimization.html)。 请注意:

使用索引进行GROUP BY的最重要前提条件是,所有GROUP BY列都引用同一索引中的属性,并且索引按顺序存储其键(例如,这是BTREE索引而不是HASH索引)。


没有提到GROUP BY - Rick James
这个问题没有提到在哪里?:) 很明显,在我所参考的文章中有提到。它回答了被问到的问题:何时应该在数据库中使用复合索引?使用复合索引会有什么性能影响?为什么应该使用复合索引? - Alexander
更正:OP没有提到GROUP BY - Rick James
当然,那就是答案——在数据库中使用复合索引的情况之一。 - Alexander

1

没有绝对的答案,每个问题都不同。

当查询工作量受益于使用复合(或多列)索引时,应该使用它。

您需要分析您的查询工作负载,以确定是否需要使用它。

当查询可以完全从索引中满足时,即查询所需的所有列都由索引覆盖时,就会使用复合索引。

更新(针对发布的问题进行编辑):如果从表中选择*,则可能使用复合索引,也可能不使用。您需要运行EXPLAIN PLAN以确保。


在地理位置数据(纬度和经度)中使用复合索引是否有意义? - Teddy
1
这完全取决于针对该表进行的查询。 - Mitch Wheat
我已经更新了原始帖子,包括最常见的查询。请参见上文。 - Teddy
@Teddy - 请查看http://mysql.rjweb.org/doc.php/find_nearest_in_mysql#bounding_box以获取有关地理位置的信息。 - Rick James
@RickJames:我完全知道什么是覆盖索引。 - Mitch Wheat
显示剩余3条评论

1

要进行空间搜索,您需要使用R-Tree算法,它可以快速地搜索地理区域。这正是您在此工作中所需要的。

一些数据库已经内置了空间索引。通过快速的谷歌搜索,我们可以发现MySQL 5拥有这些功能(根据您的SQL代码,我猜测您正在使用MySQL)。


0

我同意 @Mitch 的观点,这完全取决于你的查询。幸运的是,你可以随时创建和删除索引,并且你可以在查询前加上 EXPLAIN 关键字,以查看查询分析器是否使用了索引。

如果你要查找一个精确的纬度/经度对,那么这个索引可能是有意义的。但是你可能会寻找某个特定地点附近的房屋,所以你的查询将类似于这样(参见 source):

select *, sqrt(  pow(h2.geolat - h1.geolat,  2) 
               + pow(h2.geolng - h1.geolng, 2) ) as distance
from homes h1, homes h2
where h1.home_id = 12345 and h2.home_id != h1.home_id
order by distance

索引很可能毫无帮助。对于地理空间查询,您需要类似this的东西。

更新:使用此查询:

SELECT * FROM homes
WHERE geolat BETWEEN ??? AND ???
AND geolng BETWEEN ??? AND ???

查询分析器可以仅使用geolat索引,或仅使用geolng索引,或可能同时使用两个索引。我认为它不会使用复合索引。但是,在真实数据集上尝试这些排列组合很容易,然后(a)查看EXPLAIN告诉您的内容,以及(b)测量查询实际花费的时间。


我只是想在一个正方形网格内返回房屋。我知道空间索引,所以我不想计算距离。我只想快速返回正方形网格内的房屋。因此,我想确保我的索引设置正确。这有帮助吗? - Teddy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接