Kinerja operator MySQL “IN” pada sejumlah nilai (besar?)

94

Saya telah bereksperimen dengan Redis dan MongoDB akhir-akhir ini dan tampaknya sering kali ada kasus di mana Anda akan menyimpan berbagai id di MongoDB atau Redis. Saya akan tetap menggunakan Redis untuk pertanyaan ini karena saya bertanya tentang operator MySQL IN .

Saya bertanya-tanya bagaimana kinerjanya untuk mencantumkan sejumlah besar (300-3000) id di dalam operator IN, yang akan terlihat seperti ini:

SELECT id, name, price
FROM products
WHERE id IN (1, 2, 3, 4, ...... 3000)

Bayangkan sesuatu yang sederhana seperti tabel produk dan kategori yang biasanya Anda GABUNG bersama untuk mendapatkan produk dari kategori tertentu . Dalam contoh di atas Anda dapat melihat bahwa di bawah kategori tertentu di Redis ( category:4:product_ids) saya mengembalikan semua id produk dari kategori dengan id 4, dan menempatkannya di SELECTkueri di atas di dalam INoperator.

Bagaimana performanya?

Apakah ini situasi yang "tergantung"? Atau apakah ada yang konkret "ini (tidak) dapat diterima" atau "cepat" atau "lambat" atau haruskah saya menambahkan LIMIT 25, atau tidakkah itu membantu?

SELECT id, name, price
FROM products
WHERE id IN (1, 2, 3, 4, ...... 3000)
LIMIT 25

Atau haruskah saya memangkas larik id produk yang dikembalikan oleh Redis untuk membatasinya menjadi 25 dan hanya menambahkan 25 id ke kueri daripada 3000 dan LIMIT-ing ke 25 dari dalam kueri?

SELECT id, name, price
FROM products
WHERE id IN (1, 2, 3, 4, ...... 25)

Setiap saran / umpan balik sangat dihargai!

Michael van Rooijen
sumber
Saya tidak yakin persis apa yang Anda tanyakan? Satu kueri dengan "id IN (1,2,3, ... 3000))" lebih cepat dari 3000 kueri dengan "id = value". Tetapi penggabungan dengan "kategori = 4" akan lebih cepat daripada kedua cara di atas.
Ronnis
Benar, meskipun karena sebuah produk dapat termasuk dalam beberapa kategori I, Anda tidak dapat melakukan "kategori = 4". Menggunakan Redis saya akan menyimpan semua id produk yang termasuk dalam kategori tertentu dan kemudian menanyakannya. Saya kira pertanyaan sebenarnya adalah, bagaimana id IN (1,2,3 ... 3000)performanya dibandingkan dengan tabel JOIN products_categories. Atau itu yang kamu katakan?
Michael van Rooijen
Berhati-hatilah dari bug itu di MySql stackoverflow.com/questions/3417074/…
Itay Moav -Malimovka
Tentu saja tidak ada alasan mengapa ini tidak seefisien metode lain untuk mengambil baris terindeks; itu hanya tergantung pada apakah pembuat database telah menguji dan mengoptimalkannya. Dalam hal kompleksitas komputasi, paling buruk kita akan melakukan pengurutan O (n log N) pada INklausa (ini bahkan mungkin linier pada daftar yang diurutkan seperti yang Anda tunjukkan, bergantung pada algoritme), dan kemudian perpotongan / pencarian linier .
jberryman

Jawaban:

40

Secara umum, jika INdaftar menjadi terlalu besar (untuk beberapa nilai 'terlalu besar' yang didefinisikan dengan buruk yang biasanya berada di wilayah 100 atau lebih kecil), itu menjadi lebih efisien untuk menggunakan gabungan, membuat tabel sementara jika perlu. untuk menahan angka.

Jika angkanya adalah kumpulan yang padat (tidak ada celah - seperti yang disarankan data sampel), Anda dapat melakukannya lebih baik lagi WHERE id BETWEEN 300 AND 3000.

Namun, mungkin ada celah dalam himpunan, pada titik mana mungkin lebih baik untuk menggunakan daftar nilai yang valid setelah semua (kecuali celahnya relatif sedikit jumlahnya, dalam hal ini Anda dapat menggunakan:

WHERE id BETWEEN 300 AND 3000 AND id NOT BETWEEN 742 AND 836

Atau apapun celahnya.

Jonathan Leffler
sumber
46
Bisakah Anda memberi contoh "gunakan gabungan, membuat tabel sementara"?
Jake
jika kumpulan data berasal dari antarmuka (multi-pilih elemen) dan ada celah pada data yang dipilih dan celah ini bukan celah berurutan (hilang: 457, 490, 658, ..) maka AND id NOT BETWEEN XXX AND XXXtidak akan berfungsi dan lebih baik untuk tetap dengan yang setara (x = 1 OR x = 2 OR x = 3 ... OR x = 99)seperti yang ditulis @David Fells.
deepcell
menurut pengalaman saya - bekerja di situs web e-niaga, kami harus menampilkan hasil penelusuran dari ~ 50 ID produk yang tidak terkait, kami mendapatkan hasil yang lebih baik dengan "1. 50 kueri terpisah", vs "2. satu kueri dengan banyak nilai di" IN ayat"". Saya tidak memiliki cara untuk membuktikannya untuk saat ini, kecuali bahwa kueri # 2 akan selalu muncul sebagai kueri yang lambat dalam sistem pemantauan kami, sedangkan # 1 tidak akan pernah muncul, terlepas dari jumlah eksekusi yang dilakukan jutaan ... apakah ada yang memiliki pengalaman yang sama? (kami mungkin dapat menghubungkannya dengan cache yang lebih baik, atau mengizinkan kueri lain untuk saling terkait di antara kueri ...)
Chaim Klar
24

Saya telah melakukan beberapa tes, dan seperti yang dikatakan David Fells dalam jawabannya , ini dioptimalkan dengan cukup baik. Sebagai referensi, saya telah membuat tabel InnoDB dengan 1.000.000 register dan melakukan seleksi dengan operator "IN" dengan 500.000 nomor acak, hanya dibutuhkan 2,5 detik pada MAC saya; memilih hanya register genap membutuhkan waktu 0,5 detik.

Satu-satunya masalah yang saya miliki adalah saya harus meningkatkan max_allowed_packetparameter dari my.cnffile tersebut. Jika tidak, kesalahan misterius "MYSQL telah hilang" dihasilkan.

Berikut kode PHP yang saya gunakan untuk melakukan tes:

$NROWS =1000000;
$SELECTED = 50;
$NROWSINSERT =15000;

$dsn="mysql:host=localhost;port=8889;dbname=testschema";
$pdo = new PDO($dsn, "root", "root");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$pdo->exec("drop table if exists `uniclau`.`testtable`");
$pdo->exec("CREATE  TABLE `testtable` (
        `id` INT NOT NULL ,
        `text` VARCHAR(45) NULL ,
        PRIMARY KEY (`id`) )");

$before = microtime(true);

$Values='';
$SelValues='(';
$c=0;
for ($i=0; $i<$NROWS; $i++) {
    $r = rand(0,99);
    if ($c>0) $Values .= ",";
    $Values .= "( $i , 'This is value $i and r= $r')";
    if ($r<$SELECTED) {
        if ($SelValues!="(") $SelValues .= ",";
        $SelValues .= $i;
    }
    $c++;

    if (($c==100)||(($i==$NROWS-1)&&($c>0))) {
        $pdo->exec("INSERT INTO `testtable` VALUES $Values");
        $Values = "";
        $c=0;
    }
}
$SelValues .=')';
echo "<br>";


$after = microtime(true);
echo "Insert execution time =" . ($after-$before) . "s<br>";

$before = microtime(true);  
$sql = "SELECT count(*) FROM `testtable` WHERE id IN $SelValues";
$result = $pdo->prepare($sql);  
$after = microtime(true);
echo "Prepare execution time =" . ($after-$before) . "s<br>";

$before = microtime(true);

$result->execute();
$c = $result->fetchColumn();

$after = microtime(true);
echo "Random selection = $c Time execution time =" . ($after-$before) . "s<br>";



$before = microtime(true);

$sql = "SELECT count(*) FROM `testtable` WHERE id %2 = 1";
$result = $pdo->prepare($sql);
$result->execute();
$c = $result->fetchColumn();

$after = microtime(true);
echo "Pairs = $c Exdcution time=" . ($after-$before) . "s<br>";

Dan hasilnya:

Insert execution time =35.2927210331s
Prepare execution time =0.0161771774292s
Random selection = 499102 Time execution time =2.40285992622s
Pairs = 500000 Exdcution time=0.465420007706s
jbaylina
sumber
Demi orang lain, saya akan menambahkan bahwa berjalan di VirtualBox (CentOS) pada MBP 2013 Akhir saya dengan i7, baris ketiga (yang relevan dengan pertanyaan) dari output adalah: Pilihan acak = 500744 Waktu eksekusi waktu = 53.458173036575s .. 53 detik mungkin dapat ditoleransi tergantung pada aplikasi Anda. Untuk kegunaan saya, tidak juga. Juga, perhatikan bahwa pengujian untuk bilangan genap tidak relevan untuk pertanyaan yang ada karena menggunakan operator modulo ( %) dengan operator sama dengan ( =), bukan IN().
rinogo
Ini relevan karena ini adalah cara untuk membandingkan kueri dengan operator IN dengan kueri serupa tanpa fungsionalitas ini. Mungkin waktu yang lebih tinggi yang Anda dapatkan adalah karena ini adalah waktu unduh, karena mesin Anda sedang bertukar atau bekerja di mesin virtual lain.
jbaylina
14

Anda dapat membuat tabel sementara tempat Anda dapat memasukkan sejumlah ID dan menjalankan kueri bertingkat. Contoh:

CREATE [TEMPORARY] TABLE tmp_IDs (`ID` INT NOT NULL,PRIMARY KEY (`ID`));

dan pilih:

SELECT id, name, price
FROM products
WHERE id IN (SELECT ID FROM tmp_IDs);
Vladimir Jotov
sumber
6
lebih baik bergabung dengan tabel temp Anda daripada menggunakan subkueri
scharette
3
@loopkin dapatkah Anda menjelaskan bagaimana Anda akan melakukan ini dengan bergabung vs. subkueri?
Jeff Solomon
3
@jeffSolomon PILIH products.id, nama, harga DARI produk GABUNG tmp_IDs di products.id = tmp_IDs.ID;
scharette
JAWABAN INI! adalah apa yang saya cari, sangat cepat untuk pendaftar lama
Damián Rafael Lattenero
Terima kasih banyak. Ini bekerja sangat cepat.
mrHalfer
4

Penggunaan INdengan set parameter besar pada daftar besar record sebenarnya akan lambat.

Dalam kasus yang saya selesaikan baru-baru ini, saya memiliki dua klausa di mana, satu dengan 2.50 parameter dan yang lainnya dengan 3.500 parameter, menanyakan tabel 40 Juta catatan.

Permintaan saya memakan waktu 5 menit menggunakan standar WHERE IN. Dengan menggunakan subkueri untuk pernyataan IN (meletakkan parameter di tabel terindeksnya sendiri), saya menurunkan kueri menjadi DUA detik.

Bekerja untuk MySQL dan Oracle menurut pengalaman saya.

yoyodunno
sumber
1
Saya tidak mengerti maksud Anda "Dengan menggunakan subkueri untuk pernyataan IN (meletakkan parameter di tabel yang diindeks sendiri)". Apakah maksud Anda alih-alih menggunakan "WHERE ID IN (1,2,3)", kita harus menggunakan "WHERE ID IN (SELECT id FROM xxx)"?
Penjahit Istiyak
4

INbaik-baik saja, dan dioptimalkan dengan baik. Pastikan Anda menggunakannya di bidang yang diindeks dan Anda baik-baik saja.

Secara fungsional setara dengan:

(x = 1 OR x = 2 OR x = 3 ... OR x = 99)

Sejauh menyangkut mesin DB.

David Fells
sumber
1
Tidak benar-benar. Saya menggunakan IN clouse untuk mengambil 5k record dari DB. Clouse IN berisi daftar PK sehingga kolom terkait diindeks dan dijamin unik. JELASKAN mengatakan, bahwa pemindaian tabel lengkap dilakukan secara otomatis menggunakan pencarian PK dalam gaya "fifo-queue-alike".
Antoniossss
Di MySQL saya tidak percaya mereka "setara secara fungsional" . INmenggunakan pengoptimalan untuk kinerja yang lebih baik.
Joshua Pinter
1
Josh, jawabannya dari tahun 2011 - Saya yakin banyak hal telah berubah sejak saat itu, tetapi dulu IN diubah menjadi serangkaian pernyataan ATAU.
David Fells
1
Jawaban ini tidak benar. Dari MySQL Kinerja Tinggi : Tidak demikian halnya di MySQL, yang mengurutkan nilai dalam daftar IN () dan menggunakan pencarian biner cepat untuk melihat apakah suatu nilai ada dalam daftar. Ini adalah O (log n) dalam ukuran list, sedangkan rangkaian ekuivalen klausa OR adalah O (n) dalam ukuran list (yaitu, jauh lebih lambat untuk list besar).
Bert
Bert - ya. Jawaban ini sudah usang. Jangan ragu untuk menyarankan pengeditan.
David Fells
-2

Saat Anda memberikan banyak nilai untuk INoperator, pertama kali harus mengurutkannya untuk menghapus duplikat. Setidaknya saya curiga. Jadi tidak baik memberikan terlalu banyak nilai, karena pengurutan membutuhkan waktu N log N.

Pengalaman saya membuktikan bahwa mengiris kumpulan nilai menjadi subset yang lebih kecil dan menggabungkan hasil dari semua kueri dalam aplikasi akan memberikan kinerja terbaik. Saya akui bahwa saya mengumpulkan pengalaman pada database yang berbeda (Pervasive), tetapi hal yang sama mungkin berlaku untuk semua mesin. Hitungan nilai saya per set adalah 500-1000. Lebih atau kurang secara signifikan lebih lambat.

Jarekczek
sumber
Saya tahu ini sudah 7 tahun berlalu, tetapi masalah dengan jawaban ini hanyalah bahwa itu adalah komentar berdasarkan tebakan.
Giacomo1968