Mysql
 sql >> Baza danych >  >> RDS >> Mysql

Ranking z milionami wpisów

Wyszukiwanie pojedynczego dysku trwa około 15 ms, może trochę mniej w przypadku dysków klasy serwerowej. Czas odpowiedzi krótszy niż 500 ms ogranicza Cię do około 30 losowych dostępów do dysku. To niewiele.

Na moim małym laptopie mam programistyczną bazę danych z

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

i powolny dysk laptopa. Utworzyłem tabelę wyników za pomocą

[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)

z losowymi wynikami liczb całkowitych i kolejnymi wartościami player_id. Mamy

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

Baza danych utrzymuje parę (score, player_id) w score kolejność w indeksie score , ponieważ dane w indeksie InnoDB są przechowywane w BTREE, a wskaźnik wiersza (wskaźnik danych) jest wartością klucza podstawowego, tak że definicja KEY (score) kończy się jako KEY(score, player_id) wewnętrznie. Możemy to udowodnić, patrząc na plan zapytań w celu uzyskania wyniku:

[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)

Jak widać, key: score jest używany z Using index , co oznacza, że ​​dostęp do danych nie jest konieczny.

Zapytanie rankingowe dla danej stałej player_id trwa dokładnie 500 ms na moim laptopie:

[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)

Przy większej ilości pamięci i szybszym pudełku może być szybciej, ale nadal jest to stosunkowo kosztowna operacja, ponieważ plan jest do bani:

[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)

Jak widać, druga tabela w planie to skanowanie indeksu, więc zapytanie spowalnia liniowo wraz z liczbą graczy.

Jeśli chcesz mieć pełną tabelę wyników, musisz pominąć klauzulę where, a następnie otrzymasz dwa skany i kwadratowe czasy wykonania. Więc ten plan całkowicie się imploduje.

Czas przejść do procedury:

[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 |
...

Ponieważ jest to plan proceduralny, jest niestabilny:

  • Nie możesz użyć LIMIT, ponieważ spowoduje to przesunięcie licznika. Zamiast tego musisz pobrać wszystkie te dane.
  • Tak naprawdę nie możesz sortować. To ORDER BY klauzula działa, ponieważ nie sortuje, ale używa indeksu. Jak tylko zobaczysz using filesort , wartości liczników będą szalenie wyłączone.

Jest to jednak rozwiązanie, które jest najbliższe temu, co baza danych NoSQL (czytaj:proceduralna) zrobi jako plan wykonania.

Możemy jednak ustabilizować NoSQL w podzapytaniu, a następnie wyciąć część, która nas interesuje:

[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)

Podzapytanie zmaterializuje poprzedni zestaw wyników jako tabelę ad hoc o nazwie t, do której możemy następnie uzyskać dostęp w zapytaniu zewnętrznym. Ponieważ jest to tabela ad-hoc, w MySQL nie będzie miała indeksu. To ogranicza to, co jest możliwe efektywnie w zewnętrznym zapytaniu.

Zauważ jednak, że oba zapytania spełniają twoje ograniczenia czasowe. Oto plan:

[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)

Oba składniki zapytania (wewnętrzny, DERIVED zapytanie i zewnętrzny BETWEEN ograniczenie) będzie jednak wolniejsze dla graczy o słabym rankingu, a następnie rażąco naruszy ograniczenia czasowe.

[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)

Czas wykonania dla podejścia opisowego jest stabilny (zależny tylko od rozmiaru tabeli):

[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)

Twój telefon.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. PDO::query a PDOStatement::execute (PHP i MySQL)

  2. Błąd migracji Laravel:błąd składni lub naruszenie dostępu:1071 Podany klucz był za długi; maksymalna długość klucza to 767 bajtów

  3. Jak przekazać parametry do wywołania zwrotnego zapytania mysql w nodejs?

  4. 2 klucze obce odwołujące się do tego samego klucza podstawowego w MySQL

  5. Pobranie wszystkich wierszy nadrzędnych w jednym zapytaniu SQL