Bagaimana saya bisa mengoptimalkan permintaan MySQL ini lebih lanjut?

9

Saya memiliki permintaan yang membutuhkan waktu sangat lama untuk dijalankan (15+ detik) dan semakin lama semakin buruk seiring bertambahnya dataset saya. Saya telah mengoptimalkan ini di masa lalu, dan telah menambahkan indeks, penyortiran tingkat kode dan optimisasi lainnya, tetapi perlu beberapa penyempurnaan lebih lanjut.

SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM `sounds` 
INNER JOIN ratings ON sounds.id = ratings.rateable_id 
WHERE (ratings.rateable_type = 'Sound' 
   AND sounds.blacklisted = false 
   AND sounds.ready_for_deployment = true 
   AND sounds.deployed = true 
   AND sounds.type = "Sound" 
   AND sounds.created_at > "2011-03-26 21:25:49") 
GROUP BY ratings.rateable_id

Tujuan kueri adalah untuk memberi saya sound idperingkat rata-rata dari suara terbaru yang dirilis. Ada sekitar 1500 suara, dan peringkat 2 Juta.

Saya memiliki beberapa indeks sounds

mysql> show index from sounds;
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+————+
| Table  | Non_unique | Key_name                                 | Seq_in_index | Column_name          | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+————+
| sounds |          0 | PRIMARY                                  |            1 | id                   | A         |        1388 |     NULL | NULL   |      | BTREE      |         | 
| sounds |          1 | sounds_ready_for_deployment_and_deployed |            1 | deployed             | A         |           5 |     NULL | NULL   | YES  | BTREE      |         | 
| sounds |          1 | sounds_ready_for_deployment_and_deployed |            2 | ready_for_deployment | A         |          12 |     NULL | NULL   | YES  | BTREE      |         | 
| sounds |          1 | sounds_name                              |            1 | name                 | A         |        1388 |     NULL | NULL   |      | BTREE      |         | 
| sounds |          1 | sounds_description                       |            1 | description          | A         |        1388 |      128 | NULL   | YES  | BTREE      |         | 
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+

dan beberapa ratings

mysql> show index from ratings;
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+————+
| Table   | Non_unique | Key_name                                | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+————+
| ratings |          0 | PRIMARY                                 |            1 | id          | A         |     2008251 |     NULL | NULL   |      | BTREE      |         | 
| ratings |          1 | index_ratings_on_rateable_id_and_rating |            1 | rateable_id | A         |          18 |     NULL | NULL   |      | BTREE      |         | 
| ratings |          1 | index_ratings_on_rateable_id_and_rating |            2 | rating      | A         |        9297 |     NULL | NULL   | YES  | BTREE      |         | 
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

Ini dia EXPLAIN

mysql> EXPLAIN SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM sounds INNER JOIN ratings ON sounds.id = ratings.rateable_id WHERE (ratings.rateable_type = 'Sound' AND sounds.blacklisted = false AND sounds.ready_for_deployment = true AND sounds.deployed = true AND sounds.type = "Sound" AND sounds.created_at > "2011-03-26 21:25:49") GROUP BY ratings.rateable_id;
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+——————+
| id | select_type | table   | type   | possible_keys                                    | key                                     | key_len | ref                                     | rows    | Extra       |
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+——————+
|  1 | SIMPLE      | ratings | index  | index_ratings_on_rateable_id_and_rating          | index_ratings_on_rateable_id_and_rating | 9       | NULL                                    | 2008306 | Using where | 
|  1 | SIMPLE      | sounds  | eq_ref | PRIMARY,sounds_ready_for_deployment_and_deployed | PRIMARY                                 | 4       | redacted_production.ratings.rateable_id |       1 | Using where | 
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+-------------+

Saya melakukan cache hasil yang pernah didapat, sehingga kinerja situs tidak banyak masalah, tetapi penghangat cache saya membutuhkan waktu lebih lama dan lebih lama untuk dijalankan karena panggilan ini memakan waktu begitu lama, dan itu mulai menjadi masalah. Ini sepertinya tidak banyak angka untuk dikacaukan dalam satu permintaan ...

Apa lagi yang bisa saya lakukan untuk membuat ini tampil lebih baik ?

Coneybeare
sumber
Bisakah Anda menunjukkan EXPLAINhasilnya? EXPLAIN SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM sounds INNER JOIN ratings ON sounds.id = ratings.rateable_id WHERE (ratings.rateable_type = 'Sound' AND sounds.blacklisted = false AND sounds.ready_for_deployment = true AND sounds.deployed = true AND sounds.type = "Sound" AND sounds.created_at > "2011-03-26 21:25:49") GROUP BY ratings.rateable_id
Derek Downey
@coneybeare Ini adalah tantangan yang sangat menarik bagi saya hari ini !!! +1 untuk pertanyaan Anda. Saya ingin lebih banyak pertanyaan seperti ini datang dalam waktu dekat.
RolandoMySQLDBA
@coneybeare Sepertinya EXPLAIN baru hanya bertuliskan 21540 baris (359 X 60), bukan 2.008.306. Jalankan EXPLAIN pada kueri yang awalnya saya sarankan dalam jawaban saya. Saya ingin melihat jumlah baris yang berasal dari sana.
RolandoMySQLDBA
@RolandoMySQLDBA. Penjelasan baru memang menunjukkan bahwa jumlah baris yang lebih kecil dengan indeks, namun, waktu untuk mengeksekusi query masih sekitar 15 detik, tidak menunjukkan peningkatan
coneybeare
@coneybeare Saya menyetel kueri dengan baik. Silakan jalankan EXPLAIN pada kueri baru saya. Saya menambahkannya ke jawaban saya.
RolandoMySQLDBA

Jawaban:

7

Setelah memeriksa kueri, tabel, dan klausa WHERE AND GROUP BY, saya merekomendasikan yang berikut ini:

Rekomendasi # 1) Refactor the Query

Saya mengatur ulang kueri untuk melakukan tiga (3) hal:

  1. buat tabel temp yang lebih kecil
  2. Memproses klausa WHERE pada tabel temp tersebut
  3. Keterlambatan bergabung hingga yang terakhir

Inilah permintaan yang saya ajukan:

SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

Rekomendasi # 2) Buat indeks tabel suara dengan indeks yang akan mengakomodasi klausa WHERE

Kolom indeks ini mencakup semua kolom dari klausa WHERE dengan nilai statis pertama dan target bergerak terakhir

ALTER TABLE sounds ADD INDEX support_index
(blacklisted,ready_for_deployment,deployed,type,created_at);

Saya sungguh percaya Anda akan terkejut. Cobalah !!!

UPDATE 2011-05-21 19:04

Saya baru saja melihat kardinalitasnya. Aduh !!! Kardinalitas 1 untuk rateable_id. Wah, aku merasa bodoh !!!

UPDATE 2011-05-21 19:20

Mungkin membuat indeks akan cukup untuk memperbaiki keadaan.

UPDATE 2011-05-21 22:56

Silakan jalankan ini:

EXPLAIN SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes FROM
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

UPDATE 2011-05-21 23:34

Saya refactored lagi. Tolong Coba Yang Ini:

EXPLAIN
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes FROM
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
;

UPDATE 2011-05-21 23:55

Saya refactored lagi. Silakan Coba Yang Terakhir Ini (Terakhir Kali):

EXPLAIN
  SELECT A.id,avg(B.rating) AS avg_rating, count(B.rating) AS votes FROM
  (
    SELECT BB.* FROM
    (
      SELECT id FROM sounds
      WHERE blacklisted = false 
      AND   ready_for_deployment = true 
      AND   deployed = true 
      AND   type = "Sound" 
      AND   created_at > '2011-03-26 21:25:49'
    ) AA INNER JOIN sounds BB USING (id)
  ) A INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) B
  ON A.id = B.rateable_id
  GROUP BY B.rateable_id;

UPDATE 2011-05-22 00:12

Aku benci menyerah !!!!

EXPLAIN
  SELECT A.*,avg(B.rating) AS avg_rating, count(B.rating) AS votes FROM
  (
    SELECT BB.* FROM
    (
      SELECT id FROM sounds
      WHERE blacklisted = false 
      AND   ready_for_deployment = true 
      AND   deployed = true 
      AND   type = "Sound" 
      AND   created_at > '2011-03-26 21:25:49'
    ) AA INNER JOIN sounds BB USING (id)
  ) A,
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
    AND AAA.rateable_id = A.id
  ) B
  GROUP BY B.rateable_id;

UPDATE 2011-05-22 07:51

Sudah mengganggu saya bahwa peringkat akan kembali dengan 2 juta baris di EXPLAIN. Lalu, aku tersadar. Anda mungkin perlu indeks lain di tabel peringkat yang dimulai dengan rateable_type:

ALTER TABLE ratings ADD INDEX
rateable_type_rateable_id_ndx (rateable_type,rateable_id);

Tujuan dari indeks ini adalah untuk mengurangi tabel temp yang memanipulasi peringkat sehingga kurang dari 2 juta. Jika kami bisa mendapatkan tabel temp secara signifikan lebih kecil (setidaknya setengah), maka kami dapat memiliki harapan yang lebih baik dalam kueri Anda dan tambang saya bekerja lebih cepat juga.

Setelah membuat indeks itu, silakan Coba lagi permintaan yang diajukan asli saya dan juga coba milik Anda:

SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

UPDATE 2011-05-22 18:39: FINAL WORDS

Saya telah refactored permintaan dalam prosedur tersimpan dan menambahkan indeks untuk membantu menjawab pertanyaan tentang mempercepat hal-hal. Saya mendapat 6 suara positif, mendapat jawaban yang diterima, dan menerima 200 hadiah.

Saya juga refactored permintaan lain (hasil marginal) dan menambahkan indeks (hasil dramatis). Saya mendapat 2 suara positif dan jawaban diterima.

Saya menambahkan indeks untuk tantangan permintaan lain dan telah di-upgrade sekali

dan sekarang pertanyaanmu .

Ingin menjawab semua pertanyaan seperti ini (termasuk pertanyaan Anda) terinspirasi oleh video YouTube yang saya tonton di pertanyaan refactoring.

Sekali lagi terima kasih, @coneybeare !!! Saya ingin menjawab pertanyaan ini semaksimal mungkin, tidak hanya menerima poin atau pujian. Sekarang, saya bisa merasakan bahwa saya mendapatkan poin !!!

RolandoMySQLDBA
sumber
Saya menambahkan indeks, tidak ada peningkatan tepat waktu. Ini EXPLAIN baru: cloud.coneybeare.net/6y7c
coneybeare
EXPLAIN pada kueri dari rekomendasi 1: cloud.coneybeare.net/6xZ2 Butuh waktu sekitar 30 detik untuk menjalankan kueri ini
coneybeare
Saya memang harus mengedit sedikit sintaks Anda untuk beberapa alasan (saya menambahkan FROM sebelum permintaan pertama, dan saya harus menyingkirkan alias AAA). Inilah EXPLAIN: cloud.coneybeare.net/6xlq Permintaan aktual membutuhkan waktu sekitar 30 detik untuk menjalankan
coneybeare
@RolandoMySQLDBA: JELASKAN pembaruan 23:55 Anda: cloud.coneybeare.net/6wrN Permintaan aktual berjalan lebih dari satu menit sehingga saya mematikan prosesnya
coneybeare
Seleksi dalam kedua tidak dapat mengakses tabel pilih A, sehingga A.id melempar kesalahan.
coneybeare
3

Terima kasih atas output EXPLAIN. Seperti yang Anda tahu dari pernyataan itu, alasan waktu yang lama adalah tablescan penuh pada tabel peringkat. Tidak ada dalam pernyataan WHERE yang menyaring 2 juta baris.

Anda dapat menambahkan indeks pada ratings.type, tetapi tebakan saya adalah CARDINALITY akan menjadi sangat rendah dan Anda masih akan memindai beberapa baris ratings.

Atau Anda dapat mencoba menggunakan petunjuk indeks untuk memaksa mysql menggunakan indeks suara.

Diperbarui:

Jika itu saya, saya akan menambahkan indeks sounds.createdsebagai yang memiliki kesempatan terbaik untuk menyaring baris dan mungkin akan memaksa pengoptimal permintaan mysql untuk menggunakan indeks tabel suara. Berhati-hatilah dengan pertanyaan yang menggunakan jangka waktu yang lama (1 tahun, 3 bulan, hanya tergantung pada ukuran tabel suara).

Derek Downey
sumber
Sepertinya saran Anda terkenal untuk @coneybeare. +1 dari saya juga.
RolandoMySQLDBA
Indeks yang dibuat tidak mencukur kapan saja. Berikut adalah EXPLAIN yang diperbarui. cloud.coneybeare.net/6xvc
coneybeare
2

Jika ini harus menjadi permintaan yang tersedia "on-the-fly" , maka itu membatasi opsi Anda sedikit.

Saya akan menyarankan membagi dan menaklukkan untuk masalah ini.

--
-- Create an in-memory table
CREATE TEMPORARY TABLE rating_aggregates (
rateable_id INT,
avg_rating NUMERIC,
votes NUMERIC
);
--
-- For now, just aggregate. 
INSERT INTO rating_aggregates
SELECT ratings.rateable_id, 
avg(ratings.rating) AS avg_rating, 
count(ratings.rating) AS votes FROM `sounds`  
WHERE ratings.rateable_type = 'Sound' 
GROUP BY ratings.rateable_id;
--
-- Now get your final product --
SELECT 
sounds.*, 
rating_aggregates.avg_rating, 
rating_aggregates.votes AS votes,
rating_aggregates.rateable_id 
FROM rating_aggregates 
INNER JOIN sounds ON (sounds.id = rating_aggregates.rateable_id) 
WHERE 
ratings.rateable_type = 'Sound' 
   AND sounds.blacklisted = false 
   AND sounds.ready_for_deployment = true 
   AND sounds.deployed = true 
   AND sounds.type = "Sound" 
   AND sounds.created_at > "2011-03-26 21:25:49";
randomx
sumber
tampaknya @coneybeare melihat sesuatu di saran Anda. +1 dari saya !!!
RolandoMySQLDBA
Sebenarnya saya tidak bisa membuatnya bekerja. Saya mendapatkan kesalahan sql bahwa saya tidak yakin bagaimana cara mendekati. Saya tidak pernah benar-benar bekerja dengan tabel sementara
coneybeare
Saya akhirnya mendapatkannya (saya harus menambahkan DARI sounds, ratingske permintaan tengah), tapi itu mengunci kotak sql saya dan saya harus mematikan prosesnya.
coneybeare
0

Gunakan GABUNG, bukan subkueri. Apakah ada upaya subquery Anda yang membantu?

TAMPILKAN MENCIPTAKAN MEJA terdengar \ G

Tunjukkan peringkat CREATE TABLE \ G

Seringkali menguntungkan memiliki indeks "gabungan", bukan indeks satu kolom. Mungkin INDEX (ketik, Created_at)

Anda memfilter pada kedua tabel dalam GABUNG; yang mungkin menjadi masalah kinerja.

Ada sekitar 1500 suara, dan peringkat 2 Juta.

Rekomendasikan Anda memiliki id auto_increment ratings, buat tabel ringkasan, dan gunakan id AI untuk melacak di mana Anda "tinggalkan". Namun, jangan menyimpan rata-rata dalam tabel ringkasan:

rata-rata (ratings.rating) SEBAGAI rata-rata,

Alih-alih, pertahankan SUM (peringkat. Pemberian). Rata-rata rata-rata secara matematis tidak benar untuk menghitung rata-rata; (jumlah penjumlahan) / (jumlah penghitungan) benar.

Rick James
sumber