sql >> Database teknologi >  >> RDS >> Mysql

Rangering med millioner af tilmeldinger

En enkelt disksøgning er omkring 15 ms, måske lidt mindre med diske af serverkvalitet. En responstid på mindre end 500ms begrænser dig til omkring 30 tilfældige diskadgange. Det er ikke meget.

På min lille bærbare computer har jeg en udviklingsdatabase med

[email protected] [kris]> select @@innodb_buffer_pool_size/1024/1024 as pool_mb;
+--------------+
| pool_mb      |
+--------------+
| 128.00000000 |
+--------------+
1 row in set (0.00 sec)

og en langsom bærbar disk. Jeg oprettede en scoretabel med

[email protected] [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)

med tilfældige heltalsscore og sekventielle player_id-værdier. Vi har

[email protected] [kris]> select count(*)/1000/1000 as mrows from score\G
*************************** 1. row ***************************
mrows: 2.09715200
1 row in set (0.39 sec)

Databasen vedligeholder parret (score, player_id) i score rækkefølge i indekset score , da data i et InnoDB-indeks lagres i et BTREE, og rækkemarkøren (datamarkøren) er den primære nøgleværdi, således at definitionen KEY (score) ender med at blive KEY(score, player_id) internt. Det kan vi bevise ved at se på forespørgselsplanen for en scorehentning:

[email protected] [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)

Som du kan se, er key: score bliver brugt med Using index , hvilket betyder, at ingen dataadgang er nødvendig.

Rangeringsforespørgslen for en given konstant player_id tager præcis 500ms på min bærbare computer:

[email protected] [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)

Med mere hukommelse og på en hurtigere boks kan det være hurtigere, men det er stadig en forholdsvis dyr operation, fordi planen stinker:

[email protected] [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)

Som du kan se, er den anden tabel i planen en indeksscanning, så forespørgslen bremses lineært med antallet af spillere.

Hvis du vil have en fuld leaderboard, skal du udelade where-klausulen, og så får du to scanninger og kvadratiske eksekveringstider. Så denne plan imploderer fuldstændigt.

Tid til at gå procedure her:

[email protected] [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 |
...

Fordi dette er en procedureplan, er den ustabil:

  • Du kan ikke bruge LIMIT, fordi det vil udligne tælleren. I stedet skal du downloade alle disse data.
  • Du kan ikke rigtig sortere. Denne ORDER BY klausul virker, fordi den ikke sorterer, men bruger et indeks. Så snart du ser using filesort , vil tællerværdierne være vildt slukket.

Det er dog den løsning, der kommer tættest på, hvad en NoSQL (læs:proceduremæssig) database vil gøre som en eksekveringsplan.

Vi kan dog stabilisere NoSQL inde i en underforespørgsel og derefter udskære den del, der er af interesse for os:

[email protected] [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)

[email protected] [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)

Underforespørgslen vil materialisere det tidligere resultatsæt som en ad-hoc-tabel ved navn t, som vi så kan få adgang til i den ydre forespørgsel. Fordi det er en ad-hoc tabel, vil den i MySQL ikke have noget indeks. Dette begrænser, hvad der er muligt effektivt i den ydre forespørgsel.

Bemærk dog, hvordan begge forespørgsler opfylder din tidsbegrænsning. Her er planen:

[email protected] [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)

Begge forespørgselskomponenter (den indre, DERIVED forespørgsel og den ydre BETWEEN constraint) vil dog blive langsommere for dårligt rangerede spillere og derefter groft overtræde dine timing-begrænsninger.

[email protected] [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)

Udførelsestiden for den beskrivende tilgang er stabil (kun afhængig af tabelstørrelsen):

[email protected] [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)

Dit opkald.



  1. CAST til DECIMAL i MySQL

  2. Distinkt vs gruppe efter

  3. MySQL hvordan får man værdi til at udløbe?

  4. JPA Hibernate Persistence undtagelse [PersistenceUnit:default] Kan ikke bygge Hibernate SessionFactory