Apakah ada cara untuk mengoptimalkan penyortiran berdasarkan kolom dari tabel gabungan?

10

Ini permintaan lambat saya:

SELECT `products_counts`.`cid`
FROM
  `products_counts` `products_counts`

  LEFT OUTER JOIN `products` `products` ON (
  `products_counts`.`product_id` = `products`.`id`
  )
  LEFT OUTER JOIN `trademarks` `trademark` ON (
  `products`.`trademark_id` = `trademark`.`id`
  )
  LEFT OUTER JOIN `suppliers` `supplier` ON (
  `products_counts`.`supplier_id` = `supplier`.`id`
  )
WHERE
  `products_counts`.product_id IN
  (159, 572, 1075, 1102, 1145, 1162, 1660, 2355, 2356, 2357, 3236, 6471, 6472, 6473, 8779, 9043, 9095, 9336, 9337, 9338, 9445, 10198, 10966, 10967, 10974, 11124, 11168, 16387, 16689, 16827, 17689, 17920, 17938, 17946, 17957, 21341, 21352, 21420, 21421, 21429, 21544, 27944, 27988, 30194, 30196, 30230, 30278, 30699, 31306, 31340, 32625, 34021, 34047, 38043, 43743, 48639, 48720, 52453, 55667, 56847, 57478, 58034, 61477, 62301, 65983, 66013, 66181, 66197, 66204, 66407, 66844, 66879, 67308, 68637, 73944, 74037, 74060, 77502, 90963, 101630, 101900, 101977, 101985, 101987, 105906, 108112, 123839, 126316, 135156, 135184, 138903, 142755, 143046, 143193, 143247, 144054, 150164, 150406, 154001, 154546, 157998, 159896, 161695, 163367, 170173, 172257, 172732, 173581, 174001, 175126, 181900, 182168, 182342, 182858, 182976, 183706, 183902, 183936, 184939, 185744, 287831, 362832, 363923, 7083107, 7173092, 7342593, 7342594, 7342595, 7728766)
ORDER BY
  products_counts.inflow ASC,
  supplier.delivery_period ASC,
  trademark.sort DESC,
  trademark.name ASC
LIMIT
  0, 3;

Waktu permintaan rata-rata adalah 4,5 detik pada dataset saya dan ini tidak dapat diterima.

Solusi yang saya lihat:

Tambahkan semua kolom dari klausa pesanan ke products_countstabel. Tapi saya punya ~ 10 jenis pesanan dalam aplikasi, jadi saya harus membuat banyak kolom dan indeks. Plus products_countsmemiliki pembaruan / sisipan / penghapusan yang sangat intensif, jadi saya harus segera memperbarui semua kolom terkait pesanan (menggunakan pemicu?).

Apakah ada solusi lain?

Menjelaskan:

+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
| id | select_type | table           | type   | possible_keys                               | key                    | key_len | ref                              | rows | Extra                                        |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
|  1 | SIMPLE      | products_counts | range  | product_id_supplier_id,product_id,pid_count | product_id_supplier_id | 4       | NULL                             |  227 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | products        | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products_counts.product_id  |    1 |                                              |
|  1 | SIMPLE      | trademark       | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products.trademark_id       |    1 |                                              |
|  1 | SIMPLE      | supplier        | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products_counts.supplier_id |    1 |                                              |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+

Struktur tabel:

CREATE TABLE `products_counts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_id` int(11) unsigned NOT NULL,
  `supplier_id` int(11) unsigned NOT NULL,
  `count` int(11) unsigned NOT NULL,
  `cid` varchar(64) NOT NULL,
  `inflow` varchar(10) NOT NULL,
  `for_delete` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `cid` (`cid`),
  UNIQUE KEY `product_id_supplier_id` (`product_id`,`supplier_id`),
  KEY `product_id` (`product_id`),
  KEY `count` (`count`),
  KEY `pid_count` (`product_id`,`count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `name` varchar(255) NOT NULL,
  `category_id` int(11) unsigned NOT NULL,
  `trademark_id` int(11) unsigned NOT NULL,
  `photo` varchar(255) NOT NULL,
  `sort` int(11) unsigned NOT NULL,
  `otech` tinyint(1) unsigned NOT NULL,
  `not_liquid` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `applicable` varchar(255) NOT NULL,
  `code_main` varchar(64) NOT NULL,
  `code_searchable` varchar(128) NOT NULL,
  `total` int(11) unsigned NOT NULL,
  `slider` int(11) unsigned NOT NULL,
  `slider_title` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `external_id` (`external_id`),
  KEY `category_id` (`category_id`),
  KEY `trademark_id` (`trademark_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `trademarks` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `name` varchar(255) NOT NULL,
  `country_id` int(11) NOT NULL,
  `sort` int(11) unsigned NOT NULL DEFAULT '0',
  `sort_list` int(10) unsigned NOT NULL DEFAULT '0',
  `is_featured` tinyint(1) unsigned NOT NULL,
  `is_direct` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `external_id` (`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `suppliers` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `code` varchar(64) NOT NULL,
  `name` varchar(255) NOT NULL,
  `delivery_period` tinyint(1) unsigned NOT NULL,
  `is_default` tinyint(1) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `external_id` (`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Informasi server MySQL:

mysqld  Ver 5.5.45-1+deb.sury.org~trusty+1 for debian-linux-gnu on i686 ((Ubuntu))
Stanislav Gamayunov
sumber
3
Bisakah Anda memberikan SQL Fiddle dengan indeks, skema tabel, dan data uji? Juga apa target waktu Anda? Apakah Anda ingin menyelesaikannya dalam 3 detik, 1 detik, 50 milidetik? Berapa banyak catatan yang Anda miliki di berbagai tabel 1k, 100k, 100M?
Erik
Jika bidang yang Anda sortir tidak diindeks dan data yang disetel sangat besar, mungkinkah Anda melihat masalah sort_buffer_size? Anda bisa mencoba mengubah nilai Anda di sesi Anda dan menjalankan kueri untuk melihat apakah nilai membaik.
Brian Efting
Sudahkah Anda mencoba menambahkan indeks (inflow, product_id)?
ypercubeᵀᴹ
Pastikan Anda memiliki yang layak innodb_buffer_pool_size. Biasanya sekitar 70% dari RAM yang tersedia bagus.
Rick James

Jawaban:

6

Meninjau definisi tabel Anda menunjukkan bahwa Anda memiliki indeks yang cocok di seluruh tabel yang terlibat. Ini harus menyebabkan sambungan terjadi secepat mungkin dalam batas MySQL'slogika bergabung.

Namun, pengurutan dari beberapa tabel lebih kompleks.

Pada 2007, Sergey Petrunia mendeskripsikan 3 MySQLalgoritma penyortiran dengan urutan kecepatan MySQLdi: http://s.petrunia.net/blog/?m=201407

  1. Gunakan metode akses berbasis indeks yang menghasilkan output yang dipesan
  2. Gunakan filesort()pada tabel 1 tidak konstan
  3. Masukkan hasil gabungan ke dalam tabel sementara dan gunakan filesort()di atasnya

Dari definisi tabel dan gabungan yang ditunjukkan di atas, Anda dapat melihat bahwa Anda tidak akan pernah mendapatkan yang tercepat . Itu berarti Anda akan bergantung pada filesort()kriteria penyortiran yang Anda gunakan.

Namun, jika Anda mendesain dan menggunakan Tampilan Terwujud Anda akan dapat menggunakan algoritma pengurutan tercepat.

Untuk melihat rincian yang ditentukan untuk MySQL 5.5metode penyortiran, lihat: http://dev.mysql.com/doc/refman/5.5/en/order-by-optimization.html

Untuk MySQL 5.5(dalam contoh ini) untuk meningkatkan ORDER BYkecepatan jika Anda tidak bisa MySQLmenggunakan indeks daripada fase penyortiran tambahan, cobalah strategi berikut:

• Meningkatkan nilai sort_buffer_sizevariabel.

• Meningkatkan nilai read_rnd_buffer_sizevariabel.

• Gunakan lebih sedikit RAM per baris dengan mendeklarasikan kolom hanya sebesar yang dibutuhkan untuk menyimpan nilai aktual. [Mis. Mengurangi varchar (256) menjadi varchar (ActualLongestString)]

• Ubah tmpdirvariabel sistem untuk menunjuk ke sistem file khusus dengan sejumlah besar ruang kosong. (Detail lainnya ditawarkan dalam tautan di atas.)

Ada lebih banyak detail yang disediakan dalam MySQL 5.7dokumentasi untuk meningkatkan ORDERkecepatan, beberapa di antaranya mungkin sedikit meningkatkan perilaku:

http://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html

Tampilan Terwujud - Pendekatan Berbeda untuk Menyortir Tabel yang Bergabung

Anda menyinggung Tampilan Terwujud dengan pertanyaan Anda merujuk menggunakan pemicu. MySQL tidak memiliki fungsionalitas bawaan untuk membuat Tampilan Terwujud tetapi Anda memiliki alat yang diperlukan. Dengan menggunakan pemicu untuk menyebarkan beban, Anda dapat mempertahankan Tampilan Terwujud hingga saat ini.

Tampilan Terwujud sebenarnya adalah tabel yang diisi melalui kode prosedural untuk membangun atau membangun kembali Tampilan Terwujud dan dikelola oleh pemicu untuk menjaga data tetap up-to-date.

Karena Anda sedang membangun sebuah tabel yang akan memiliki indeks , maka Materialized View Ketika ditanya dapat menggunakan urutan tercepat : Gunakan metode akses berbasis indeks yang memproduksi output memerintahkan

Karena MySQL 5.5penggunaan pemicu untuk mempertahankan Tampilan Terwujud , Anda juga akan memerlukan proses, skrip, atau prosedur tersimpan untuk membangun Tampilan Terwujud awal .

Tapi itu jelas proses yang terlalu berat untuk dijalankan setelah setiap pembaruan ke tabel dasar tempat Anda mengelola data. Di situlah pemicu berperan untuk menjaga data tetap mutakhir saat perubahan dilakukan. Dengan cara ini masing-masing insert,, updatedan deleteakan menyebarkan perubahannya, menggunakan pemicu Anda, ke Tampilan Terwujud .

Organisasi FROMDUAL di http://www.fromdual.com/ memiliki kode sampel untuk mempertahankan Tampilan Terwujud . Jadi, daripada menulis sampel saya sendiri, saya akan mengarahkan Anda ke sampel mereka:

http://www.fromdual.com/mysql-materialized-views

Contoh 1: Membangun Tampilan Terwujud

DROP TABLE sales_mv;
CREATE TABLE sales_mv (
    product_name VARCHAR(128)  NOT NULL
  , price_sum    DECIMAL(10,2) NOT NULL
  , amount_sum   INT           NOT NULL
  , price_avg    FLOAT         NOT NULL
  , amount_avg   FLOAT         NOT NULL
  , sales_cnt    INT           NOT NULL
  , UNIQUE INDEX product (product_name)
);

INSERT INTO sales_mv
SELECT product_name
    , SUM(product_price), SUM(product_amount)
    , AVG(product_price), AVG(product_amount)
    , COUNT(*)
  FROM sales
GROUP BY product_name;

Ini memberi Anda Tampilan Terwujud pada saat refresh. Namun, karena Anda memiliki basis data yang bergerak cepat, Anda juga ingin menjaga pandangan ini tetap terbaru.

Oleh karena itu tabel data dasar yang terpengaruh harus memiliki pemicu untuk menyebarkan perubahan dari tabel dasar ke tabel Tampilan Terwujud. Sebagai satu contoh:

Contoh 2: Memasukkan Data Baru Ke Tampilan Terwujud

DELIMITER $$

CREATE TRIGGER sales_ins
AFTER INSERT ON sales
FOR EACH ROW
BEGIN

  SET @old_price_sum = 0;
  SET @old_amount_sum = 0;
  SET @old_price_avg = 0;
  SET @old_amount_avg = 0;
  SET @old_sales_cnt = 0;

  SELECT IFNULL(price_sum, 0), IFNULL(amount_sum, 0), IFNULL(price_avg, 0)
       , IFNULL(amount_avg, 0), IFNULL(sales_cnt, 0)
    FROM sales_mv
   WHERE product_name = NEW.product_name
    INTO @old_price_sum, @old_amount_sum, @old_price_avg
       , @old_amount_avg, @old_sales_cnt
  ;

  SET @new_price_sum = @old_price_sum + NEW.product_price;
  SET @new_amount_sum = @old_amount_sum + NEW.product_amount;
  SET @new_sales_cnt = @old_sales_cnt + 1;
  SET @new_price_avg = @new_price_sum / @new_sales_cnt;
  SET @new_amount_avg = @new_amount_sum / @new_sales_cnt;

  REPLACE INTO sales_mv
  VALUES(NEW.product_name, @new_price_sum, @new_amount_sum, @new_price_avg
       , @new_amount_avg, @new_sales_cnt)
  ;

END;
$$
DELIMITER ;

Tentu saja, Anda juga akan memerlukan pemicu untuk mempertahankan Menghapus Data Dari Tampilan Terwujud dan Perbarui Data dalam Tampilan Terwujud . Sampel juga tersedia untuk pemicu ini.

TERAKHIR: Bagaimana Itu Membuat Penyortiran Meja Bergabung Lebih Cepat?

Tampilan Terwujud sedang dibangun terus-menerus karena pembaruan dibuat untuk itu. Oleh karena itu Anda dapat menentukan Indeks (atau Indeks ) yang ingin Anda gunakan untuk menyortir data dalam Tampilan atau Tabel Terwujud .

Jika overhead dari pemeliharaan data tidak terlalu berat, maka Anda menghabiskan beberapa sumber daya (CPU / IO / dll) untuk setiap perubahan data yang relevan untuk menjaga Tampilan Terwujud dan dengan demikian data indeks up-to-date dan tersedia. Karena itu, pilih akan lebih cepat, karena Anda:

  1. Sudah menghabiskan CPU dan IO tambahan untuk mendapatkan data yang siap untuk SELECT Anda.
  2. Indeks pada Tampilan Materialized dapat menggunakan metode pengurutan tercepat yang tersedia untuk MySQL, yaitu Menggunakan metode akses berbasis indeks yang menghasilkan output yang dipesan .

Tergantung pada keadaan Anda dan bagaimana perasaan Anda tentang proses keseluruhan, Anda mungkin ingin membangun kembali Tampilan Terwujud setiap malam selama periode yang lambat.

Catatan: Dalam Microsoft SQL Server Tampilan Terwujud, disebut Tampilan Terindeks dan secara otomatis diperbarui berdasarkan metadata Tampilan Terindeks .

RLF
sumber
6

Tidak banyak yang bisa dibahas di sini, tapi saya kira masalah utamanya adalah Anda membuat tabel sementara yang cukup besar dan mengurutkan file pada disk setiap kali. Alasannya adalah:

  1. Anda menggunakan UTF8
  2. Anda menggunakan beberapa bidang varchar (255) besar untuk menyortir

Ini berarti bahwa tabel temporer dan file sortir Anda bisa cukup besar, seperti saat membuat tabel sementara bidang dibuat pada panjang MAX, dan ketika menyortir catatan semua pada panjang MAX (dan UTF8 adalah 3 byte per karakter). Ini juga kemungkinan menghalangi penggunaan tabel sementara dalam memori. Untuk info lebih lanjut, lihat detail tabel temp internal .

LIMIT juga tidak ada gunanya bagi kita di sini, karena kita perlu mematerialisasikan dan memesan seluruh hasil yang ditetapkan sebelum kita tahu apa 3 baris pertama.

Sudahkah Anda mencoba memindahkan tmpdir Anda ke sistem file tmpfs ? Jika / tmp belum menggunakan tmpfs (MySQL menggunakan tmpdir=/tmpsecara default pada * nix), maka Anda bisa menggunakan / dev / shm secara langsung. Dalam file my.cnf Anda:

[mysqld]
...
tmpdir=/dev/shm  

Maka Anda perlu me-restart mysqld.

Itu bisa membuat perbedaan besar . Jika Anda cenderung berada di bawah tekanan memori pada sistem, Anda mungkin ingin membatasi ukurannya (biasanya distro linux membatasi tmpfs pada 50% dari total RAM secara default) untuk menghindari pertukaran segmen memori ke disk, atau bahkan lebih buruk dari situasi OOM . Anda dapat melakukannya dengan mengedit baris di /etc/fstab:

tmpfs                   /dev/shm                tmpfs   rw,size=2G,noexec,nodev,noatime,nodiratime        0 0

Anda juga dapat mengubah ukurannya "online". Sebagai contoh:

mount -o remount,size=2G,noexec,nodev,noatime,nodiratime /dev/shm

Anda juga dapat memutakhirkan ke MySQL 5.6 - yang memiliki subkueri berkinerja tinggi dan tabel turunan - dan bermain-main dengan kueri sedikit lebih banyak. Saya tidak berpikir kita akan melihat kemenangan besar melalui rute itu, dari apa yang saya lihat.

Semoga berhasil!

Matt Lord
sumber
Terima kasih atas jawaban anda. Memindahkan tmpdir ke tmpfs memberikan kinerja yang bagus.
Stanislav Gamayunov