是否可以让 MySQL 使用 1 DESC、2 ASC 的 ORDER 索引?
Posted
技术标签:
【中文标题】是否可以让 MySQL 使用 1 DESC、2 ASC 的 ORDER 索引?【英文标题】:Is it possible to make MySQL use an index for the ORDER by 1 DESC, 2 ASC? 【发布时间】:2012-04-30 10:44:14 【问题描述】:我有一个物化路径驱动的公告板。它使用以下查询按顺序获取消息,
SELECT * FROM Board ORDER by root DESC, path ASC LIMIT 0,100
其中root
是线程根消息的id
,path
是具体化路径。
但是,我使这个查询使用索引的努力都没有成功。
mysql> explain extended select path from Board order by root desc, path asc limit 100;
+-------+---------------+----------+---------+------+-------+----------+----------------------------+
| type | possible_keys | key | key_len | ref | rows | filtered | Extra
+-------+---------------+----------+---------+------+-------+----------+-----------------------------
| index | NULL | rootpath | 261 | NULL | 21998 | 100.00 | Using index; Using filesort
目前它在rows
列下显示表中所有行的数量。我想知道,有没有办法减少这个数字或以其他方式优化查询?
CREATE TABLE `Board` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`path` varchar(255) NOT NULL DEFAULT '0',
`root` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `root` (`root`),
KEY `path` (`path`),
KEY `rootpath` (`root`,`path`)
)
查询的主要问题是分页 - 我需要从上一页最后一个消息旁边的消息开始第二页。这就是为什么我希望它以 直接 的方式 - 没有 sublelects 和东西。 虽然当前的设置不太好,因为它从线程中间开始第二页,但至少它是合乎逻辑的。
【问题讨论】:
解释说它正在使用“根路径”索引。 【参考方案1】:this article 很好地解释了您面临的问题。重要的部分是:
最典型的情况是当您想按不同的两列排序时 方向:… 按价格 ASC 订购,日期 DESC LIMIT 10 如果您有 按升序索引(价格,日期),您将无法 很好地优化这个查询——将需要外部排序(“文件排序”)。 如果您能够在价格 ASC 上建立索引,则日期 DESC 相同 查询可以按区域排序的顺序检索数据。
此外,该文章还提到了解决该问题的有效方法:将第二个“顺序”子句颠倒:
但是,您可以通过以下方式解决此问题 “reverse_date”列并将其用于排序。使用 MySQL 5.0,您甚至 可以使用触发器将其更新为实际日期更新,因此它变得更少 丑陋。事实上,这就是为什么你会看到 ***表结构中的“reverse_timestamp”字段。
也来自官方MySQL documentation:
在某些情况下,MySQL 无法使用索引来解析 ORDER BY, 尽管它仍然使用索引来查找与 WHERE 匹配的行 条款。这些案例包括: ..... 您混合 ASC 和 DESC: SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;
作为建议,您最好有一个 reversed_root
列,即 Integer.MAX_VALUE - root
并且在 (reversed_root, path) 上有一个索引。然后你可以有一个查询:
SELECT * FROM Board ORDER by reversed_root ASC,path ASC LIMIT 0,100
【讨论】:
【参考方案2】:您的原始查询
SELECT * FROM Board ORDER by root DESC, path ASC LIMIT 0,100;
创建一个名为 BoardDisplayOrder 的表来保存 root 的负值,并在其中添加名为 rootinv 的新列。
首先是示例数据和您的原始查询:
mysql> drop database if exists YourCommonSense;
Query OK, 2 rows affected (0.06 sec)
mysql> create database YourCommonSense;
Query OK, 1 row affected (0.00 sec)
mysql> use YourCommonSense
Database changed
mysql> CREATE TABLE `Board` (
-> `id` int(11) NOT NULL AUTO_INCREMENT,
-> `path` varchar(255) NOT NULL DEFAULT '0',
-> `root` int(11) NOT NULL DEFAULT '0',
-> PRIMARY KEY (`id`),
-> KEY `root` (`root`),
-> KEY `path` (`path`),
-> KEY `rootpath` (`root`,`path`)
-> );
Query OK, 0 rows affected (0.11 sec)
mysql> INSERT INTO Board (path,root) VALUES
-> ('Rolando Edwards',30),
-> ('Daniel Edwards',30),
-> ('Pamela Edwards',30),
-> ('Dominiuqe Edwards',40),
-> ('Diamond Edwards',40),
-> ('Richard Washington',50),
-> ('George Washington',50),
-> ('Synora Washington',50);
Query OK, 8 rows affected (0.05 sec)
Records: 8 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM Board;
+----+--------------------+------+
| id | path | root |
+----+--------------------+------+
| 2 | Daniel Edwards | 30 |
| 3 | Pamela Edwards | 30 |
| 1 | Rolando Edwards | 30 |
| 5 | Diamond Edwards | 40 |
| 4 | Dominiuqe Edwards | 40 |
| 7 | George Washington | 50 |
| 6 | Richard Washington | 50 |
| 8 | Synora Washington | 50 |
+----+--------------------+------+
8 rows in set (0.00 sec)
mysql> SELECT * FROM Board ORDER by root DESC, path ASC LIMIT 0,100;
+----+--------------------+------+
| id | path | root |
+----+--------------------+------+
| 7 | George Washington | 50 |
| 6 | Richard Washington | 50 |
| 8 | Synora Washington | 50 |
| 5 | Diamond Edwards | 40 |
| 4 | Dominiuqe Edwards | 40 |
| 2 | Daniel Edwards | 30 |
| 3 | Pamela Edwards | 30 |
| 1 | Rolando Edwards | 30 |
+----+--------------------+------+
8 rows in set (0.00 sec)
mysql> EXPLAIN SELECT * FROM Board ORDER by root DESC, path ASC LIMIT 0,100;
+----+-------------+-------+-------+---------------+----------+---------+------+------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+----------+---------+------+------+-----------------------------+
| 1 | SIMPLE | Board | index | NULL | rootpath | 261 | NULL | 8 | Using index; Using filesort |
+----+-------------+-------+-------+---------------+----------+---------+------+------+-----------------------------+
1 row in set (0.00 sec)
mysql>
接下来,使用 rootinv 和涉及 rootinv 的索引创建表 BoardDisplayOrder:
mysql> CREATE TABLE BoardDisplayOrder LIKE Board;
Query OK, 0 rows affected (0.09 sec)
mysql> ALTER TABLE BoardDisplayOrder DROP INDEX root;
Query OK, 0 rows affected (0.11 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE BoardDisplayOrder DROP INDEX path;
Query OK, 0 rows affected (0.09 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE BoardDisplayOrder DROP INDEX rootpath;
Query OK, 0 rows affected (0.08 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE BoardDisplayOrder ADD COLUMN rootinv int(11) NOT NULL;
Query OK, 0 rows affected (0.17 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE BoardDisplayOrder ADD INDEX rootpathid (rootinv,path,id,root);
Query OK, 0 rows affected (0.11 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SHOW CREATE TABLE BoardDisplayOrder \G
*************************** 1. row ***************************
Table: BoardDisplayOrder
Create Table: CREATE TABLE `boarddisplayorder` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`path` varchar(255) NOT NULL DEFAULT '0',
`root` int(11) NOT NULL DEFAULT '0',
`rootinv` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `rootpathid` (`rootinv`,`path`,`id`,`root`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
mysql>
然后,填充 BoardDisplayOrder:
mysql> INSERT INTO BoardDisplayOrder (id,path,root,rootinv)
-> SELECT id,path,root,-root FROM Board;
Query OK, 8 rows affected (0.06 sec)
Records: 8 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM BoardDisplayOrder;
+----+--------------------+------+---------+
| id | path | root | rootinv |
+----+--------------------+------+---------+
| 7 | George Washington | 50 | -50 |
| 6 | Richard Washington | 50 | -50 |
| 8 | Synora Washington | 50 | -50 |
| 5 | Diamond Edwards | 40 | -40 |
| 4 | Dominiuqe Edwards | 40 | -40 |
| 2 | Daniel Edwards | 30 | -30 |
| 3 | Pamela Edwards | 30 | -30 |
| 1 | Rolando Edwards | 30 | -30 |
+----+--------------------+------+---------+
8 rows in set (0.00 sec)
mysql>
现在,针对 BoardDisplayOrder 运行查询,但在 rootinv 上没有 DESC:
mysql> SELECT id,path,root FROM BoardDisplayOrder ORDER by rootinv, path LIMIT 0,100;
+----+--------------------+------+
| id | path | root |
+----+--------------------+------+
| 7 | George Washington | 50 |
| 6 | Richard Washington | 50 |
| 8 | Synora Washington | 50 |
| 5 | Diamond Edwards | 40 |
| 4 | Dominiuqe Edwards | 40 |
| 2 | Daniel Edwards | 30 |
| 3 | Pamela Edwards | 30 |
| 1 | Rolando Edwards | 30 |
+----+--------------------+------+
8 rows in set (0.00 sec)
mysql> EXPLAIN SELECT id,path,root FROM BoardDisplayOrder ORDER by rootinv, path LIMIT 0,100;
+----+-------------+-------------------+-------+---------------+------------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------------+-------+---------------+------------+---------+------+------+-------------+
| 1 | SIMPLE | BoardDisplayOrder | index | NULL | rootpathid | 269 | NULL | 8 | Using index |
+----+-------------+-------------------+-------+---------------+------------+---------+------+------+-------------+
1 row in set (0.00 sec)
mysql>
试试看!!!
警告
这很容易做到,因为 root 是 INT。
如果 root 是 VARCHAR,则 rootinv 必须是字符触发器。换句话说,
A
-> Z
B
-> Y
...
M
-> N
N
-> M
...
Y
-> B
Z
-> A
这原则上适用于您需要执行 DESC 的任何字段。问题源于 MySQL 没有在索引中将键内部排序为 ASC 或 DESC。索引中的所有内容都在升序。这就是为什么当您在 SHOW GLOBAL STATUS LIKE 'handler%';
中看到处理程序统计信息时,您会看到以下内容:
等等。
根据current MySQL Documentation
index_col_name 规范可以以 ASC 或 DESC 结尾。这些 未来扩展允许关键字用于指定升序 或降序索引值存储。目前,它们已被解析,但 忽略;索引值始终按升序存储。
试试看!!!
更新 2012-05-04 06:54 EDT
@frail 对我的回答的评论
ALTER TABLE BoardDisplayOrder ADD INDEX rootpathid (rootinv,path,id,root) 对我来说似乎没有必要,ALTER TABLE BoardDisplayOrder ADD INDEX rootpathid (rootinv,path) 应该足够了
我的解决方案有ALTER TABLE BoardDisplayOrder ADD INDEX rootpathid (rootinv,path,id,root)
的原因是提供一个覆盖索引。在这种情况下,覆盖索引将:
想想原来的查询,
SELECT * FROM Board ORDER by root DESC, path ASC LIMIT 0,100;
这需要检索三列 path、id 和 root。因此,它们需要在索引中。当然,指数规模的增加将是一种权衡。如果 Board 表很大,如果可以更快地检索,有些人不会担心空间。如果根路径索引只是 (rootinv,path),那么每次索引范围扫描都将伴随着对剩余列的表的 ref 查找。这就是我选择ALTER TABLE BoardDisplayOrder ADD INDEX rootpathid (rootinv,path,id,root);
的原因
【讨论】:
伙计,我不敢相信它这么容易,但我自己却这么愚蠢!ALTER TABLE BoardDisplayOrder ADD INDEX rootpathid (rootinv,path,id,root)
对我来说似乎没有必要,ALTER TABLE BoardDisplayOrder ADD INDEX rootpathid (rootinv,path)
应该足够了
@frail 我在回答中提到了你的评论
真的很抱歉,但是我发布的表格有一个简化版本。当然,还有一个标题、作者和日期字段必须一起检索。不过,我认为从表格中读取没有问题
不用担心。我正在根据所提供的内容进行解决。我不会为覆盖索引添加标题。【参考方案3】:
在这种情况下,数据本身无法以您需要的方式检索.
在这种情况下尤其合适,因为数据本身似乎在您保存后并没有更新。一旦您的消息发布后,它们就不会更新(或者从我最初的阅读来看是这样)。
假设您走这条路,我建议的步骤是:
向表中添加一个新列root_path
。
执行此更新语句update Board set root_path = root + path
。 (您可能需要根据现有列的数据类型进行调整。)
每当您向表中添加新行时,也要添加此新列。 (这可以通过触发器来处理,但我会警惕触发器,因为当人们更改代码的其他部分时它们可能会被忽略。)
然后您应该能够在该新列上设置一个索引并针对该列编写您的选择 - 随意点击您的索引。
我相信即使其中一个键必须以相反的顺序排序,这也会起作用。
CREATE TABLE foo
(
id serial NOT NULL,
int_field integer DEFAULT 0,
varchar_field character varying(255),
composite_field character varying(255),
CONSTRAINT foo_pkey PRIMARY KEY (id )
);
CREATE INDEX composite_field_idx ON foo (composite_field);
INSERT INTO foo (int_field, varchar_field, composite_field) VALUES
(1,'t','t1'),
(2,'z','z2'),
(2,'w','w2'),
(4,'u','u4'),
(5,'u','u5'),
(5,'x','x5'),
(7,'v','v7');
explain select * from foo order by composite_field desc;
运行上面的代码,解释语句应该会显示被引用的键composite_field_idx。
查询的结果是:
select * from foo order by composite_field desc;
id | int_field | varchar_field | composite_field
----+-----------+---------------+-----------------
2 | 2 | z | z2
6 | 5 | x | x5
3 | 2 | w | w2
7 | 7 | v | v7
5 | 5 | u | u5
4 | 4 | u | u4
1 | 1 | t | t1
【讨论】:
由于其中一列必须以相反的顺序排序,恐怕这将需要更新每一行,这似乎不是一个好的解决方案。以上是关于是否可以让 MySQL 使用 1 DESC、2 ASC 的 ORDER 索引?的主要内容,如果未能解决你的问题,请参考以下文章