Mengapa MySQL mengabaikan indeks bahkan untuk pesanan ini?


14

Saya menjalankan EXPLAIN:

mysql> explain select last_name from employees order by last_name;
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  

Indeks di meja saya:

mysql> show index from employees;  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| Table     | Non_unique | Key_name      | Seq_in_index | Column_name   | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| employees |          0 | PRIMARY       |            1 | subsidiary_id | A         |           6 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          0 | PRIMARY       |            2 | employee_id   | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          1 | idx_last_name |            1 | last_name     | A         |       10031 |      700 | NULL   |      | BTREE      |         |               |  
| employees |          1 | date_of_birth |            1 | date_of_birth | A         |       10031 |     NULL | NULL   | YES  | BTREE      |         |               |  
| employees |          1 | date_of_birth |            2 | subsidiary_id | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
5 rows in set (0.02 sec)  

Ada indeks pada last_name tetapi pengoptimal tidak menggunakannya.
Jadi saya lakukan:

mysql> explain select last_name from employees force index(idx_last_name) order by last_name;  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  

Tapi tetap saja indeksnya tidak digunakan! Apa yang saya lakukan salah di sini?
Apakah itu ada hubungannya dengan fakta bahwa indeks itu NON_UNIQUE? BTW, last_name adalahVARCHAR(1000)

Pembaruan diminta oleh @Rando_MySQLDBA

mysql> SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;  
+---------------+  
| DistinctCount |  
+---------------+  
|         10000 |  
+---------------+  
1 row in set (0.05 sec)  


mysql> SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;  
+----------+  
| COUNT(1) |  
+----------+  
|        0 |  
+----------+  
1 row in set (0.15 sec)  

Silakan jalankan dua pertanyaan ini: 1) SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;2) SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;. Apa hasil dari setiap penghitungan?
RolandoMySQLDBA

@RolandoMySQLDBA: Saya memperbarui OP dengan info yang Anda minta.
Cratylus

Dua pertanyaan lagi, tolong: 1) SELECT COUNT(1) FullTableCount FROM employees;dan 2) SELECT * FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A LIMIT 10;.
RolandoMySQLDBA

Sudahlah, saya melihat penjelasan dengan apa yang saya butuhkan.
RolandoMySQLDBA

2
@Cratylus Anda menerima jawaban yang salah, Anda harus menerima jawaban yang
miracle173

Jawaban:


6

MASALAH # 1

Lihatlah kueri

select last_name from employees order by last_name;

Saya tidak melihat klausa WHERE yang berarti, dan begitu pula Pengoptimal Permintaan MySQL. Tidak ada insentif untuk menggunakan indeks.

MASALAH # 2

Lihatlah kueri

select last_name from employees force index(idx_last_name) order by last_name; 

Anda memberikannya indeks, tetapi Query Opitmizer mengambil alih. Saya telah melihat perilaku ini sebelumnya ( Bagaimana cara memaksa JOIN untuk menggunakan indeks spesifik di MySQL? )

Mengapa ini harus terjadi?

Tanpa WHEREklausa, Pengoptimal Kueri mengatakan yang berikut untuk dirinya sendiri:

  • Ini adalah Tabel InnoDB
  • Ini kolom yang diindeks
  • Indeks ini memiliki row_id dari gen_clust_index (alias Clustered Index)
  • Kenapa saya harus melihat indeks kapan
    • tidak ada WHEREklausa?
    • Saya harus selalu bangkit kembali ke meja?
  • Karena semua baris dalam tabel InnoDB berada di blok 16K yang sama dengan gen_clust_index, saya akan melakukan pemindaian tabel penuh sebagai gantinya.

Pengoptimal Kueri memilih jalur dengan resistensi paling rendah.

Anda akan mengalami sedikit kejutan, tetapi ini dia: Tahukah Anda bahwa Query Optimizer akan menangani MyISAM dengan sangat berbeda?

Anda mungkin mengatakan HUH ???? BAGAIMANA ????

MyISAM menyimpan data dalam .MYDfile dan semua indeks dalam .MYIfile.

Permintaan yang sama akan menghasilkan rencana EXPLAIN yang berbeda karena indeks tinggal di file yang berbeda dari data. Mengapa Inilah alasannya:

  • Data yang dibutuhkan ( last_namekolom) sudah dipesan di.MYI
  • Dalam kasus terburuk, Anda akan memiliki pemindaian indeks lengkap
  • Anda hanya akan mengakses kolom last_namedari indeks
  • Anda tidak perlu menyaring yang tidak diinginkan
  • Anda tidak akan memicu pembuatan file temp untuk disortir

Bagaimana bisa begitu yakin tentang ini? Saya telah menguji teori kerja ini tentang bagaimana menggunakan penyimpanan yang berbeda akan menghasilkan rencana EXPLAIN yang berbeda (kadang-kadang lebih baik): Haruskah indeks mencakup semua kolom yang dipilih agar dapat digunakan untuk ORDER OLEH?


1
-1 @Rolando jawaban ini tidak kurang tepat daripada jawaban yang benar dari Michael-sqlbot tetapi itu salah, misalnya manual mengatakan: "MySQL menggunakan indeks untuk operasi ini: (...) Untuk mengurutkan atau mengelompokkan tabel jika menyortir atau pengelompokan dilakukan pada awalan paling kiri dari indeks yang dapat digunakan (...) ". Juga beberapa pernyataan lain dari postingan Anda dapat diperdebatkan. Saya akan merekomendasikan Anda menghapus jawaban ini atau untuk memperbaikinya.
miracle173

Jawaban ini tidak benar. Indeks masih dapat digunakan bahkan jika tidak ada klausa WHERE jika itu menghindari penyortiran.
oysteing

19

Sebenarnya, masalahnya di sini adalah ini terlihat seperti indeks awalan. Saya tidak melihat definisi tabel dalam pertanyaan, tetapi sub_part= 700? Anda belum mengindeks seluruh kolom, sehingga indeks tidak dapat digunakan untuk menyortir dan juga tidak berguna sebagai indeks penutup. Itu hanya dapat digunakan untuk menemukan baris yang "mungkin" cocok dengan WHEREdan lapisan server (di atas mesin penyimpanan) harus lebih lanjut menyaring baris yang cocok. Apakah Anda benar-benar membutuhkan 1000 karakter untuk nama belakang?


pembaruan untuk menggambarkan: Saya memiliki tabel uji tabel dengan litle lebih dari 500 baris di dalamnya, masing-masing dengan nama domain dari situs web dalam kolom domain_name VARCHAR(254) NOT NULLdan tidak ada indeks.

mysql> alter table keydemo add key(domain_name);
Query OK, 0 rows affected (0.17 sec)
Records: 0  Duplicates: 0  Warnings: 0

Dengan kolom lengkap diindeks, kueri menggunakan indeks:

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
| id | select_type | table   | type  | possible_keys | key         | key_len | ref  | rows | Extra       |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
|  1 | SIMPLE      | keydemo | index | NULL          | domain_name | 764     | NULL |  541 | Using index |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
1 row in set (0.01 sec)

Jadi, sekarang, saya akan menjatuhkan indeks itu, dan hanya mengindeks 200 karakter pertama dari nama_domain.

mysql> alter table keydemo drop key domain_name;
Query OK, 0 rows affected (0.11 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> alter table keydemo add key(domain_name(200));
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table   | type | possible_keys | key  | key_len | ref  | rows | Extra          |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
|  1 | SIMPLE      | keydemo | ALL  | NULL          | NULL | NULL    | NULL |  541 | Using filesort |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
1 row in set (0.00 sec)

mysql>

Voila.

Perhatikan juga, bahwa indeks, pada 200 karakter, lebih panjang dari nilai terpanjang di kolom ...

mysql> select max(length(domain_name)) from keydemo;
+--------------------------+
| max(length(domain_name)) |
+--------------------------+
|                       43 |
+--------------------------+
1 row in set (0.04 sec)

... tapi itu tidak ada bedanya. Indeks yang dideklarasikan dengan panjang awalan hanya dapat digunakan untuk pencarian, bukan untuk penyortiran, dan bukan sebagai indeks penutup, karena tidak berisi nilai kolom penuh, menurut definisi.

Selain itu, kueri di atas dijalankan pada tabel InnoDB, tetapi menjalankannya pada tabel MyISAM menghasilkan hasil yang hampir identik. Satu- satunya perbedaan dalam hal ini adalah bahwa InnoDB dihitungrows sedikit mati (541) sementara MyISAM menunjukkan jumlah baris yang tepat (563) yang merupakan perilaku normal karena dua mesin penyimpanan menangani penyelaman indeks sangat berbeda.

Saya masih akan menyatakan bahwa kolom last_name kemungkinan lebih besar dari yang dibutuhkan, tetapi masih mungkin untuk mengindeks seluruh kolom, jika Anda menggunakan InnoDB dan menjalankan MySQL 5.5 atau 5.6:

Secara default, kunci indeks untuk indeks satu kolom bisa mencapai 767 byte. Batas panjang yang sama berlaku untuk awalan kunci indeks apa pun. Lihat Bagian 13.1.13, “ CREATE INDEXSintaks”. Misalnya, Anda dapat mencapai batas ini dengan indeks awalan kolom lebih dari 255 karakter pada TEXTatau VARCHARkolom, dengan asumsi satu UTF-8set karakter dan maksimum 3 byte untuk setiap karakter. Ketika innodb_large_prefixopsi konfigurasi diaktifkan, batas panjang ini dinaikkan menjadi 3072 byte, untuk InnoDBtabel yang menggunakan format DYNAMICdan COMPRESSEDbaris.

- http://dev.mysql.com/doc/refman/5.5/id/innodb-restrictions.html


Sudut pandang yang menarik. Kolomnya varchar(1000)tapi ini di luar batas maksimum yang diizinkan untuk indeks yaitu ~ 750
Cratylus

8
Jawaban ini harus diterima.
ypercubeᵀᴹ

1
@ ypercube Jawaban ini lebih tepat daripada milik saya. +1 untuk komentar Anda dan +1 untuk jawaban ini. Semoga ini diterima sebagai gantinya milik saya.
RolandoMySQLDBA

1
@Timo, itu pertanyaan menarik ... yang saya sarankan posting sebagai pertanyaan baru, di sini, mungkin dengan tautan ke jawaban ini, untuk konteks. Memposting keluaran lengkap dari EXPLAIN SELECT ..., serta SHOW CREATE TABLE ...dan SELECT @@VERSION;karena perubahan ke pengoptimal lintas versi mungkin relevan.
Michael - sqlbot

1
Sekarang saya dapat melaporkan bahwa (setidaknya untuk 5,7) indeks awalan tidak membantu dengan pengindeksan nol, seperti yang saya minta dalam komentar saya di atas.
Timo

2

Saya membuat jawaban tentang karena komentar tidak akan mendukung pemformatan dan RolandoMySQL DBA berbicara tentang gen_clust_index dan innodb. Dan ini sangat penting pada tabel berbasis innodb. Ini lebih jauh dari pengetahuan DBA normal karena Anda harus dapat menganalisis kode C ..

Anda harus SELALU SELALU membuat KUNCI UTAMA atau KUNCI UNIK jika Anda menggunakan Innodb. Jika Anda tidak innodb akan menggunakan itu sendiri ROW_ID yang dihasilkan yang bisa lebih merugikan Anda daripada kebaikan.

Saya akan mencoba menjelaskannya dengan mudah karena buktinya didasarkan pada kode C.

/**********************************************************************//**
Returns a new row id.
@return the new id */
UNIV_INLINE
row_id_t
dict_sys_get_new_row_id(void)
/*=========================*/
{
    row_id_t    id;

    mutex_enter(&(dict_sys->mutex));

    id = dict_sys->row_id;

    if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
          dict_hdr_flush_row_id();
    }

    dict_sys->row_id++;
    mutex_exit(&(dict_sys->mutex));
    return(id);
}

Masalah pertama

mutex_enter (& (dict_sys-> mutex));

Baris ini memastikan hanya satu utas yang dapat mengakses dict_sys-> mutex secara bersamaan. Bagaimana jika nilai sudah ditransmisikan ... ya utas harus menunggu sehingga Anda mendapatkan sesuatu seperti fitur acak yang bagus seperti penguncian utas atau jika Anda memiliki lebih banyak tabel tanpa KUNCI UTAMA atau KUNCI UNIK Anda akan memiliki fitur yang bagus dengan innodb ' table locking ' bukankah ini bukan alasan mengapa MyISAM digantikan oleh InnoDB karena dari fitur yang bagus itu disebut penguncian berbasis catatan / baris ..

Masalah kedua

(0 == (id% DICT_HDR_ROW_ID_WRITE_MARGIN))

modulo (%) perhitungan lambat tidak baik jika Anda memasukkan batch karena harus dihitung ulang setiap kali ..., dan karena DICT_HDR_ROW_ID_WRITE_MARGIN (nilai 256) adalah kekuatan dua ini dapat dibuat lebih cepat ..

(0 == (id & (DICT_HDR_ROW_ID_WRITE_MARGIN - 1))))

Catatan tambahan jika kompiler C dikonfigurasi untuk mengoptimalkan dan itu adalah pengoptimal yang baik, optimizer C akan memperbaiki kode "berat" ke versi yang lebih ringan

moto cerita selalu buat KUNCI UTAMA Anda sendiri atau pastikan Anda memiliki indeks UNIK ketika Anda membuat tabel dari awal


Tambahkan replikasi berbasis baris dan fakta bahwa ID baris tidak konsisten di seluruh server, dan poin Raymond tentang selalu membuat kunci utama bahkan lebih penting.

Tolong jangan menyarankan itu UNIQUEsudah cukup - itu juga perlu menyertakan hanya kolom non-NULL untuk indeks unik untuk dipromosikan menjadi PK.
Rick James

"perhitungan modulo (%) lambat" - Yang lebih penting adalah berapa persen waktu yang INSERTdihabiskan dalam fungsi ini. Saya kira tidak penting. Kontras upaya untuk menyekop kolom di sekitar, lakukan operasi BTree, termasuk blok-split sesekali, berbagai mutex pada buffer_pool, ganti-penyangga, dll.
Rick James

Benar @ RickJames overhead mungkin jumlah yang sangat kecil tetapi banyak juga jumlah kecil bertambah (masih akan menjadi optimasi mikro) .. Selain masalah pertama adalah yang paling merepotkan beberapa
Raymond Nijland
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.