Mengapa mysql menggunakan indeks yang salah untuk pesanan berdasarkan permintaan?

9

Ini meja saya dengan ~ 10.000.000 data baris

CREATE TABLE `votes` (
  `subject_name` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `subject_id` int(11) NOT NULL,
  `voter_id` int(11) NOT NULL,
  `rate` int(11) NOT NULL,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`subject_name`,`subject_id`,`voter_id`),
  KEY `IDX_518B7ACFEBB4B8AD` (`voter_id`),
  KEY `subject_timestamp` (`subject_name`,`subject_id`,`updated_at`),
  KEY `voter_timestamp` (`voter_id`,`updated_at`),
  CONSTRAINT `FK_518B7ACFEBB4B8AD` FOREIGN KEY (`voter_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Berikut adalah kardinalitas indeks

masukkan deskripsi gambar di sini

Jadi ketika saya melakukan kueri ini:

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

Saya mengharapkan itu menggunakan indeks voter_timestamp tetapi mysql memilih untuk menggunakan ini sebagai gantinya:

explain select SQL_NO_CACHE * from votes  where subject_name = 'medium' and voter_id = 1001 and rate = 1 order by updated_at desc limit 20 offset 100;`

type:
    index_merge
possible_keys: 
    PRIMARY,IDX_518B7ACFEBB4B8AD,subject_timestamp,voter_timestamp
key:
    IDX_518B7ACFEBB4B8AD,PRIMARY
key_len:
    102,98
ref:
    NULL
rows:
    9255
filtered:
    10.00
Extra:
    Using intersect(IDX_518B7ACFEBB4B8AD,PRIMARY); Using where; Using filesort

Dan saya mendapat waktu permintaan 200-400ms.

Jika saya memaksanya untuk menggunakan indeks yang tepat seperti:

SELECT SQL_NO_CACHE * FROM votes USE INDEX (voter_timestamp) WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

Mysql dapat mengembalikan hasilnya dalam 1-2ms

dan inilah penjelasannya:

type:
    ref
possible_keys:
    voter_timestamp
key:
    voter_timestamp
key_len:
    4
ref:
    const
rows:
    18714
filtered:
    1.00
Extra:
    Using where

Jadi mengapa mysql tidak memilih voter_timestampindeks untuk permintaan awal saya?

Apa yang saya coba adalah analyze table votes,, optimize table votesletakkan indeks itu dan tambahkan lagi, tetapi mysql masih menggunakan indeks yang salah. tidak begitu mengerti apa masalahnya.

Phoenix
sumber
1
@ ypercubeᵀᴹ Saya pikir tidak perlu untuk mengindeks semua kolom dalam kondisi di mana, seperti yang Anda lihat jika saya memaksa untuk menggunakan indeks (voter_id, updated_at), itu dapat menggunakannya dan menjadi sangat efisien. Jika saya menghapus subject_name = "medium"bagian itu juga dapat memilih indeks yang tepat, tidak perlu indeksrate
Phoenix
Namun, indeks 4 kolom akan lebih efisien daripada 2 (voter_id, updated_at). Indeks lain akan (voter_id, subject_name, updated_at)atau (subject_name, voter_id, updated_at)(tanpa kurs).
ypercubeᵀᴹ
1
Dan ya, Anda - pada titik tertentu - benar. Anda tidak perlu indeks 4 kolom. Itu hanya indeks terbaik untuk permintaan ini. 2-kolom (yang menurut Anda "benar") mungkin ok untuk data dan distribusi yang Anda miliki saat ini. Dengan distribusi yang berbeda, mungkin mengerikan. Contoh: Misalkan 99% dari baris memiliki tingkat> 1 dan hanya 1% memiliki tingkat = 1. Apakah menurut Anda menggunakan indeks 2 kolom akan efisien?
ypercubeᵀᴹ
Itu harus melintasi sebagian besar indeks dan melakukan ribuan pencarian di atas meja, hanya untuk menemukan tingkat itu> 1 dan menolak baris, sampai menemukan 120 yang sesuai dengan kriteria yang tidak dapat dinilai oleh indeks ( subject_name='medium' and rate=1)
ypercubeᵀᴹ
ypercube, Phoenix - MySQL tidak akan sampai ke LIMITatau bahkan ORDER BYkecuali indeks memenuhi semua penyaringan terlebih dahulu. Artinya, tanpa 4 kolom penuh, ia akan mengumpulkan semua baris yang relevan, mengurutkan semuanya, lalu memilih LIMIT. Dengan indeks 4-kolom, query dapat menghindari jenis dan berhenti setelah membaca hanya satu LIMITbaris.
Rick James

Jawaban:

5

MySQL menggunakan model biaya yang relatif sederhana (lebih sederhana dari RDBMS lainnya) untuk perencanaan kueri di mana penyaringan dataset Anda memiliki prioritas yang cukup tinggi. Dalam kueri pertama Anda dengan indeks gabungan, diperkirakan pemindaian ~ 9000 baris akan diperlukan sedangkan yang kedua dengan petunjuk indeks akan membutuhkan 18.000. Taruhan saya adalah bahwa ini akan berbobot dalam perhitungan yang cukup untuk memindahkan skala ke arah gabungan. . Anda dapat mengkonfirmasi ini (atau menemukan alasan lain) dengan menghidupkan optimizer_trace, menjalankan kueri Anda dan mengevaluasi hasilnya.

set global optimizer_trace='enabled=on';

-- run your query 

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

select * from information_schema.`OPTIMIZER_TRACE`;

Satu komentar pada index_merge: dalam kebanyakan kasus Anda akan menemukan bahwa itu cukup mahal. Walaupun sangat berguna untuk skenario tipe OLAP, itu mungkin tidak terlalu cocok untuk OLTP karena operasi dapat mengambil waktu yang signifikan dari permintaan Anda dan seperti yang Anda lihat kadang-kadang rencana eksekusi suboptimal sebenarnya lebih cepat.

Untungnya MySQL menyediakan sakelar untuk pengoptimal sehingga Anda dapat menyesuaikannya sesuai keinginan.

Untuk semua opsi, Anda dapat menjalankan:

show global variables like 'optimizer_switch';

Untuk mengubahnya, Anda tidak perlu menyalin seluruh string. Ia bekerja seperti dict.update()di python.

 set global optimizer_switch='index_merge=off';

Jika memungkinkan saya juga akan melihat struktur meja Anda dan meningkatkan. Memiliki kunci primer ~ 100 byte dengan banyak kunci sekunder tidak disarankan.

Anda memiliki empat kunci sekunder dan beberapa di antaranya berlebihan, misalnya (voter_id)indeks adalah subset dari(voter_id, updated_at)

Károly Nagy
sumber
"Index merge intersect" jarang digunakan oleh MySQL. Dalam semua kasus, mungkin lebih baik memiliki indeks dengan lebih banyak kolom. "Index merge union" terkadang berguna; berubah ORmenjadi UNIONsering kali lebih baik atau lebih baik.
Rick James
5

Untuk permintaan itu, Anda memerlukan indeks ini:

INDEX(voter_id, rate, subject_name, updated_at)

Itu updated_atharus menjadi yang terakhir; tiga lainnya bisa dalam urutan apa pun. (Indeks 3-kolom ypercube tidak terlalu berguna karena tidak menyelesaikan WHEREkolom sebelum mengenai ORDER BYkolom.)

Saat Anda menambahkan indeks ini, Anda mungkin bisa menghilangkan semua kunci sekunder lainnya:

KEY IDX_518B7ACFEBB4B8AD( voter_id), - The FK dapat menggunakan indeks KEY saya subject_timestamp( subject_name, subject_id, updated_at), - sebagian besar berlebihan KEY voter_timestamp( voter_id, updated_at), - mungkin upaya Anda

Dengan indeks 4-kolom, Anda memiliki peluang untuk mengoptimalkan "pagination" dan menghindari OFFSET. Lihat blog ini.

Pada topik lain ... Ketika saya melihat X_namedan X_id, saya menganggap "normalisasi" sedang terjadi. Saya berharap melihat dua kolom dalam sebuah tabel, dengan hampir tidak ada yang lain. Saya tidak akan berharap untuk melihat keduanya di beberapa meja lainnya.

(voter_id, updated_at)tidak akan melewati voter_idkarena belum selesai dengan pemfilteran WHERE. Kemudian, karena indeks yang lain lebih kecil, itu diambil. Milik saya memiliki 3 kolom untuk mengurus pemfilteran, lalu kolom untuk ORDER BY.

Rick James
sumber