一次磁盘寻址大约需要15ms,也许使用服务器级别的磁盘可以稍微缩短时间。小于500ms的响应时间限制您只能进行约30次随机磁盘访问,这并不多。
在我的小型笔记本电脑上,我有一个开发数据库,其中包含了许多表格。
root@localhost [kris]> select @@innodb_buffer_pool_size/1024/1024 as pool_mb;
+
| pool_mb |
+
| 128.00000000 |
+
1 row in set (0.00 sec)
我的笔记本电脑硬盘速度较慢。我创建了一个得分表格,其中包含
root@localhost [kris]> show create table score\G
*************************** 1. row ***************************
Table: score
Create Table: CREATE TABLE `score` (
`player_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`score` int(11) NOT NULL,
PRIMARY KEY (`player_id`),
KEY `score` (`score`)
) ENGINE=InnoDB AUTO_INCREMENT=2490316 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
我们有一组随机整数分数和连续的player_id值。
root@localhost [kris]> select count(*)/1000/1000 as mrows from score\G
*************************** 1. row ***************************
mrows: 2.09715200
1 row in set (0.39 sec)
数据库在索引score
中按照score
的顺序维护了(score,player_id)
这对数据。由于InnoDB索引中的数据存储在B树中,行指针(数据指针)是主键值,因此定义KEY(score)
实际上在内部成为KEY(score, player_id)
。我们可以通过查看得分检索的查询计划来证明这一点:
root@localhost [kris]> explain select * from score where score = 17\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: score
type: ref
possible_keys: score
key: score
key_len: 4
ref: const
rows: 29
Extra: Using index
1 row in set (0.00 sec)
正如您所看到的,key:score
与 Using index
一起使用,这意味着不需要进行数据访问。
对于给定的常量 player_id
的排名查询在我的笔记本电脑上需要精确的500ms:
root@localhost [kris]> select p.*, count(*) as rank
from score as p join score as s on p.score < s.score
where p.player_id = 479269\G
*************************** 1. row ***************************
player_id: 479269
score: 99901
rank: 2074
1 row in set (0.50 sec)
如果有更多的内存和更快的设备,操作速度会更快,但这仍然是一个相对昂贵的操作,因为计划执行效率很低:
root@localhost [kris]> explain select p.*, count(*) as rank from score as p join score as s on p.score < s.score where p.player_id = 479269;
+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+
| 1 | SIMPLE | p | const | PRIMARY,score | PRIMARY | 4 | const | 1 | |
| 1 | SIMPLE | s | index | score | score | 4 | NULL | 2097979 | Using where; Using index |
+
2 rows in set (0.00 sec)
正如您所看到的,计划中的第二个表是一个索引扫描,因此查询随着玩家数量的增加而线性减慢。
如果要得到完整的排行榜,需要省略where子句,然后您会得到两个扫描和二次执行时间。因此,该计划完全崩溃。
现在需要进行程序化操作:
root@localhost [kris]> set @count = 0;
select *, @count := @count + 1 as rank from score where score >= 99901 order by score desc ;
...
| 2353218 | 99901 | 2075 |
| 2279992 | 99901 | 2076 |
| 2264334 | 99901 | 2077 |
| 2239927 | 99901 | 2078 |
| 2158161 | 99901 | 2079 |
| 2076159 | 99901 | 2080 |
| 2027538 | 99901 | 2081 |
| 1908971 | 99901 | 2082 |
| 1887127 | 99901 | 2083 |
| 1848119 | 99901 | 2084 |
| 1692727 | 99901 | 2085 |
| 1658223 | 99901 | 2086 |
| 1581427 | 99901 | 2087 |
| 1469315 | 99901 | 2088 |
| 1466122 | 99901 | 2089 |
| 1387171 | 99901 | 2090 |
| 1286378 | 99901 | 2091 |
| 666050 | 99901 | 2092 |
| 633419 | 99901 | 2093 |
| 479269 | 99901 | 2094 |
| 329168 | 99901 | 2095 |
| 299189 | 99901 | 2096 |
| 290436 | 99901 | 2097 |
...
由于这是一份过程计划,因此它是不稳定的:
- 您不能使用 LIMIT,因为那会偏移计数器。相反,您必须下载所有这些数据。
- 您不能真正排序。这个
ORDER BY
子句有效是因为它并没有进行排序,而是使用索引。一旦出现 using filesort
, 计数器值将会失控。
虽然如此,它仍然是最接近 NoSQL(即:过程性)数据库执行计划的解决方案。
我们可以在一个子查询中稳定 NoSQL,然后分离出我们感兴趣的部分:
root@localhost [kris]> set @count = 0;
select * from (
select *, @count := @count + 1 as rank
from score
where score >= 99901
order by score desc
) as t
where player_id = 479269;
Query OK, 0 rows affected (0.00 sec)
+
| player_id | score | rank |
+
| 479269 | 99901 | 2094 |
+
1 row in set (0.00 sec)
root@localhost [kris]> set @count = 0;
select * from (
select *, @count := @count + 1 as rank
from score
where score >= 99901
order by score desc
) as t
where rank between 2090 and 2100;
Query OK, 0 rows affected (0.00 sec)
+
| player_id | score | rank |
+
| 1387171 | 99901 | 2090 |
| 1286378 | 99901 | 2091 |
| 666050 | 99901 | 2092 |
| 633419 | 99901 | 2093 |
| 479269 | 99901 | 2094 |
| 329168 | 99901 | 2095 |
| 299189 | 99901 | 2096 |
| 290436 | 99901 | 2097 |
+
8 rows in set (0.01 sec)
子查询将前面的结果集实现为一个临时表,名为t,我们可以在外部查询中访问它。因为这是一个临时表,在MySQL中它将没有索引。这限制了在外部查询中有效地完成的操作。
注意两个查询都满足时间约束条件。这是计划:
root@localhost [kris]> set @count = 0; explain select * from ( select *, @count := @count + 1 as rank from score where score >= 99901 order by score desc ) as t where rank between 2090 and 2100\G
Query OK, 0 rows affected (0.00 sec)
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: <derived2>
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 2097
Extra: Using where
*************************** 2. row ***************************
id: 2
select_type: DERIVED
table: score
type: range
possible_keys: score
key: score
key_len: 4
ref: NULL
rows: 3750
Extra: Using where; Using index
2 rows in set (0.00 sec)
无论是内部的DERIVED
查询还是外部的BETWEEN
约束条件,都会因为排名较低的玩家而变慢,并且会严重违反您的时间限制。
root@localhost [kris]> set @count = 0; select * from ( select *, @count := @count + 1 as rank from score where score >= 0 order by score desc ) as t;
...
2097152 rows in set (3.56 sec)
描述性方法的执行时间稳定(仅取决于表大小):
root@localhost [kris]> select p.*, count(*) as rank
from score as p join score as s on p.score < s.score
where p.player_id = 1134026;
+
| player_id | score | rank |
+
| 1134026 | 0 | 2097135 |
+
1 row in set (0.53 sec)
你来打电话。