mysql(六)多列索引之索引顺序问题

2023-11-09

使用索引常见的错误是

  • 为每列创建单独的索引,
  • 或者按照错误的顺序创建多列索引

多列索引

多列索引,是指在创建索引时所关联的字段不是一个字段,而是多个字段,虽然可以通过所关联的字段进行查询,但是只有查询条件中使用了所关联字段中的第一个字段,多列索引才会被使用

多列索引时顺序问题

平时我们遇到的比较容易引起困惑的问题就是索引列的顺序。正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好的满足排序和分组的需要(针对B-Tree索引

在一个多列B-Tree索引中,索引列的顺序意味着
索引首先按照最左列进行排序,其次是第二列,等等。所以,索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的 ORDER BY,GROUP BY和DISTINCT等字句的查询需求。

所以多列索引的顺序至关重要。在Lahdenmaki和Leach的“三星索引”系统中,列顺序也决定了一个索引是否能够成为一个真正的“三星索引”

对于如何选择索引的列顺序有一个经验法则:将选择性最高的列放到索引的最前列。这个建议有用吗?在某些场景可能有帮助,但通常不如避免随机IO和排序那么重要,考略问题需要更全面(场景不同则选择不同,没有一个放之四海皆准的法则。这里只能说明,这个经验法则没有你想的那么重要)

当不需要考虑排序和分组时,将选择性最高的列放在前面。这个时候索引的作用只是用于优化WHERE条件的查找。在这种情况下,这样的设计的索引确实能够最快的过滤出需要的行,对于在WHERE子句中只使用了索引部分前缀的列的查询来说选择性也更高。然而,性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布有关。这和选择前桌的长度需要考虑的地方一样。可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下的索引的选择性最高

演示示例

新建表

CREATE TABLE `payment` (
  `id` int NOT NULL AUTO_INCREMENT,
  `staff_id` int NOT NULL,
  `customer_id` int NOT NULL,
  `money` decimal(20,0) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=169682 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

随机插入数据

此时有如下查询:

SELECT * FROM payment WHERE staff_id = 2 AND customer_id = 584

此时是应该创建一个(staff_id,customer_id)索引还是应该颠倒一下顺序?

此时我们可以跑一下查询来确定在这个表中值的分布情况,并确定哪个列的选择性更高。

先使用以下查询预测一下,看看各个WHERE条件分支应对的数据基数有多大

mysql> select SUM(staff_id=2),SUM(customer_id=584) from payment;
+-----------------+----------------------+
| SUM(staff_id=2) | SUM(customer_id=584) |
+-----------------+----------------------+
|          219555 |                   30 |
+-----------------+----------------------+
1 row in set (0.08 sec)

根据前面的经验法则,应该将索引列customer_id放在前面,因为对应的条件值的customer_id数量更小。我们再来看看对于这个customer_id的条件值,对应的staff_id列的选择性如何

mysql> select sum(staff_id = 2) from payment where customer_id = 584;
+-------------------+
| sum(staff_id = 2) |
+-------------------+
|                17 |
+-------------------+
1 row in set (0.05 sec)

这样做有一个地方需要注意,查询的结果非常依赖于选定的值。如果按照上述办法优化,可能对其他的值的查询不公平,服务器的整体性可能变得更糟糕,或者其他某些查询的运行变得不如预期

如果是从pt-query-digest这样的工具的报告中提取“最差”查询,那么再按照上述的办法选定的索引顺序往往是非常高效的。如果没有类似的具体查询来运行,那么最好还是按经验法则来做,因为经验法则考虑的是全局基数和选择性,而不是某个具体的查询

mysql> select count(distinct staff_id)/count(*) as staff_id_selectivity,count(distinct customer_id)/count(*) as customer_id_selectivity,count(*) from payment;
+----------------------+-------------------------+----------+
| staff_id_selectivity | customer_id_selectivity | count(*) |
+----------------------+-------------------------+----------+
|               0.0001 |                  0.0373| 15543109 |
+----------------------+-------------------------+----------+
1 row in set (1 min 27.19 sec)

customer_id的选择性更高,所以答案是将其作为索引列的第一列:

ALTER TABLE payment ADD KEY(customer_id,staff_id);

当使用前缀索引的时候,在某些条件值的基数比正常值高的时候,问题就来了。例如,在某些应用程序中,对于没有登录的用户,都将其用户名记录为“guest”,在记录用户行为的会话表和其他记录用户活动的表中“guest”就成为了一个特殊用户ID,一旦涉及这个用户,那么和对于正常用户的查询就大不同了,因为通常有很多会话都是没有登录的。系统账号也会导致类似的问题。一个应用通常都有一个特殊的管理员账号,和普通账号不同,他并不是一个具体的用户,系统中所有的其他用户都是这个用户的好友,所以系统往往通过他向网站的所有用户发送状态通知和其他消息。这个账号的巨大好友列表很容易导致网站出现服务器性能问题。

这实际上是一个非常典型的问题。任何的异常用户,不仅仅是那些用于管理应用的设计糟糕的账号会有同样的问题,那些拥有大量好友,图片,状态,收藏的用户,也会有前面提到的系统账号同样的问题

下面举个例子:
在一个用户分享购买商品和购买经验的论坛上,这个特殊表上的查询运行的非常慢:

select count(distinct threadId) as count_value from Message where (group_id = 10137) and (userId = 1288826) and (anonymous = 0) order by priority desc,modifiedDate desc

这个查询看似没有建立合适的索引,所以客户咨询我们是否可以优化。EXPLAIN的结果如下:

         id:1
select_type:SIMPLE
      table:Message
       type:ref
        key:ix_groupId_userId
    key_len:18
        ref:const,const
       rows1251162
      Extra:Using where

MySQL 为这个查询选择了索引(groupId,userId),如果不考虑列的基数,这看起来是一个非常合理的选择。但如果考虑一下 user ID 和 group ID 条配的行数,可能就会有不同的想法了 :

mysql> SELECT COUNT(*),SUM(groupId = 10137),SUM(userId = 1288826),SUM(anonymous = 0) FROM Messagel
+----------------------+-------------------------+----------+----------+----------+
|             count(*) |     sum(groupId =10137) | sum(userId=1288826) |sum(anonymous = 0) |
+----------------------+-------------------------+----------+----------+----------+
|              4142217 |                 4092654 |             1288496 |           4141934 |
+----------------------+-------------------------+----------+----------+----------+
1 row in set (1 min 27.19 sec)

从上面的结果来看符合组 (groupId) 条件几乎满足表中的所有行,符合用户(userId)条件的有 130 万条记录一一也就是说索引基本上没什么用。因为这些数据是从其他应用中迁移过来的,迁移的时候把所有的消息都赋予了管理员组的用户。这个案例的解决办法是修改应用程序代码,区分这类特殊用户和组,禁止针对这类用户和组执行这个查询

从这个小案例可以看到经验法则和推论在多数情况是有用的,但要注意不要假设平均情况下的性能也能代表特殊情况下的性能,特殊情况可能会摧毁整个应用的性能。

最后,尽管关于选择性和基数的经验法则值得去研究和分析,但一定要记住别忘了WHERE子句中的排序、分组和范围条件等其他因素,这些因素可能对查询的性能造成非常大的影响。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

mysql(六)多列索引之索引顺序问题 的相关文章

  • 复杂的sql树行

    表结构 id message reply id 1 help me 0 434 love to 1 852 didn t work 434 0110 try this 852 2200 this wont 0 5465 done 0110
  • MySQL小写自动转换

    我有多个在数据库表中写入数据的 Web 服务 我想针对特定字段自动将大写字符串转换为小写字符串 mysql 有没有执行此任务的函数 假设这是表 id name language 有时 在语言字段内 Web 服务会写入大写字符串 IT 我想直
  • 在 BIRT 中输入参数后更新数据集查询

    在 BIRT 报告设计中传递参数后 如何更改或更新数据集的查询 详细说明 我有一个如下所示的查询 WHERE 该参数标记可以保存不同的值 在用户输入参数后 它看起来像这样 例如 WHERE column name 1 or WHERE co
  • 在 LINQ 中执行 FirstOrDefault 的替代方法

    我有一个成员资格表 用于记录用户是否是列表的成员 当用户的成员资格发生更新时 会写入新记录 并且先前的记录保持原样 从而可以维护其成员资格的历史记录 要获取用户的会员身份 需要选择他们最近的条目 下面是一些用户列表成员资格数据的示例 目的是
  • dayname(curdate()) 不适用于 codeigniter php

    此 sql 在 phpmyadmin 中有效 但在 codeigniter php 中无效 function getProgramsHomepage data array this gt db gt select p name p star
  • 从 CSV 到 MySQL 的换行问题

    我正在将 csv 文件导入 MySQL 除了文件中的换行符之外 一切正常 我的 csv 行之一如下所示 42 E A R Classic Earplugs ear images ear classic jpg 5 Proven size s
  • 用教义 2 DBAL 连接子查询

    我正在重构 Zend 框架2应用程序使用学说 2 5 DBAL 而不是 Zend DB ZF1 我有以下 Zend Db 查询 subSelect db gt select gt from user survey status entrie
  • 使用 php 和 mysql 计算日期差(以小时为单位)

    我如何使用 php 和 mysql 找到以小时为单位的日期差异 Use TIMEDIFF http dev mysql com doc refman 5 1 en date and time functions html function
  • MySQL 5:我的 GROUP BY 字段的顺序重要吗?

    Peeps 我的 MySQL 查询中有一些聚合 计算字段 我的 GROUP BY 子句是动态生成的 具体取决于用户在 Web 表单中选择的选项 很好奇 GROUP BY 子句中列出的字段顺序是否会对计算产生任何影响 例如 SUM AVERA
  • 用于全文搜索和 2 亿多条记录的数据库

    我即将创建一个包含至少 2 亿个条目的庞大数据库 数据库需要可使用全文进行搜索 并且速度应该很快 我的数据库从许多不同的数据源获取数据 我需要定期导入新的或更新的数据 将我的所有数据存储在像 mysql 这样的关系数据库中 然后创建一个 n
  • MySQL通过UPDATE/DELETE合并重复数据记录

    我有一个看起来像这样的表 mysql gt SELECT FROM Colors ID USERNAME RED GREEN YELLOW BLUE ORANGE PURPLE 1 joe 1 null 1 null null null 2
  • 如何在php/mysql中使用事务

    我正在使用 php mysql 我知道 mysql 中的事务 但不能在我的脚本中使用 下面是我的脚本 如何在我的代码中使用 php 事务 即 BEGIN ROLLBACK COMMIT foreach json a shop as json
  • 当我尝试计算 mysqli 结果时,为什么会收到警告?

    下面的代码会导致此警告 警告 count 参数必须是数组或实现 Countable 的对象 为什么要这样做 如何防止出现警告 if isset GET edit sonum GET edit update true result mysql
  • 在 SQL 中如何获得整数的最大值?

    我试图从 MySQL 数据库中找出整数 有符号或无符号 的最大值 有没有办法从数据库本身提取这些信息 是否有我可以使用的内置常量或函数 标准 SQL 或 MySQL 特定的 At http dev mysql com doc refman
  • 转义用户数据,无需魔法引号

    我正在研究如何在来自外部世界的数据被用于应用程序控制 存储 逻辑等之前正确地对其进行转义 显然 随着 magic quote 指令在 php 5 3 0 中很快被弃用 并在 php6 中被删除 对于任何想要升级并进入新语言功能 同时维护遗留
  • 蟒蛇 | MySQL | AttributeError:模块“mysql.connector”没有属性“connect”

    我正在学习 python 中的一个新库 mysql 我尝试执行以下命令 import mysql connector mydb mysql connector connect host localhost user root passwd
  • 具有“日期之间”的 CakePHP 模型

    我有一个很大的数据集 超过十亿行 数据在数据库中按日期分区 因此 我的查询工具必须在每个查询上指定一个 SQL Between 子句 否则它将必须扫描每个分区 而且 它会在返回之前超时 所以 我的问题是 分区的数据库中的字段是日期 使用 C
  • Monkeyrunner/jython 中未找到 JDBC 驱动程序错误

    我需要在中插入一些东西DB 我在用着JDBC as a connector jython the script mysql数据库和脚本正在运行CentOS 我的代码看起来像这样 from com android monkeyrunner i
  • 控制数据是否存在于数组中

    我在mysql中有两个不同的表 我正在使用curl从json文件中获取数据 我的第一个表名称是 tblclients 该表存储客户端数据 我的第二个表名称是 tblcustomfieldsvalues 该表使用 tblclients 表的
  • MySQL 连接器 C++ 64 位在 Visual Studio 2012 中从源代码构建

    我正在尝试建立mySQL 连接器 C 从源头在视觉工作室2012为了64 bit建筑学 我知道这取决于一些boost头文件和C 连接器 跑步CMake生成一个项目文件 但该项目文件无法编译 因为有一大堆非常令人困惑的错误 这些错误可能与包含

随机推荐