Permintaan individu berjalan pada 10ms, dengan UNION ALL mereka mengambil 290ms + (7,7M mencatat MySQL DB). Bagaimana cara mengoptimalkan?

9

Saya memiliki tabel yang menyimpan janji temu untuk guru, memungkinkan dua jenis sisipan:

  1. Berbasis per jam : dengan kebebasan penuh untuk menambahkan slot tanpa batas per hari per guru (selama slot tidak tumpang tindih): pada 15 / April seorang guru mungkin memiliki slot pada 10:00, 11:00, 12:00 dan 16:00 . Seseorang dilayani setelah memilih waktu / slot guru tertentu.

  2. Periode / rentang waktu : pada 15 / Apr guru lain mungkin bekerja dari 10:00 hingga 12:00 dan kemudian dari 14:00 hingga 18:00. Seseorang dilayani berdasarkan urutan kedatangan, jadi jika seorang guru bekerja dari pukul 10:00 hingga 12:00, semua orang yang tiba pada periode ini akan dihadiri oleh urutan kedatangan (antrian lokal).

Karena saya harus mengembalikan semua guru yang tersedia dalam pencarian, saya perlu semua slot disimpan dalam tabel yang sama dengan urutan kedatangan. Dengan cara ini saya dapat memesan berdasarkan date_from ASC, menunjukkan slot yang tersedia pertama terlebih dahulu pada hasil pencarian.

Struktur tabel saat ini

CREATE TABLE `teacher_slots` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `teacher_id` mediumint(8) unsigned NOT NULL,
  `city_id` smallint(5) unsigned NOT NULL,
  `subject_id` smallint(5) unsigned NOT NULL,
  `date_from` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `date_to` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `status` tinyint(4) NOT NULL DEFAULT '0',
  `order_of_arrival` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `by_hour_idx` (`teacher_id`,`order_of_arrival`,`status`,`city_id`,`subject_id`,`date_from`),
  KEY `order_arrival_idx` (`order_of_arrival`,`status`,`city_id`,`subject_id`,`date_from`,`date_to`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Permintaan pencarian

Saya perlu memfilter menurut: datetime aktual, city_id, subject_id dan jika slot tersedia (status = 0).

Untuk setiap jam saya harus menunjukkan semua slot yang tersedia untuk hari terdekat pertama yang tersedia untuk setiap guru (tampilkan semua slot waktu pada hari tertentu dan tidak dapat menampilkan lebih dari satu hari untuk guru yang sama). (Saya mendapat permintaan dengan bantuan dari mattedgod ).

Untuk rentang berdasarkan (order_of_arrival = 1), saya harus menunjukkan rentang terdekat yang tersedia, hanya satu kali per guru.

Permintaan pertama berjalan secara individual di sekitar 0,10 ms, permintaan kedua 0,08 ms dan UNION ALL rata-rata 300 ms.

(
    SELECT id, teacher_slots.teacher_id, date_from, date_to, order_of_arrival
    FROM teacher_slots
    JOIN (
        SELECT DATE(MIN(date_from)) as closestDay, teacher_id
        FROM teacher_slots
        WHERE   date_from >= '2014-04-10 08:00:00' AND order_of_arrival = 0
                AND status = 0 AND city_id = 6015 AND subject_id = 1
        GROUP BY teacher_id
    ) a ON a.teacher_id = teacher_slots.teacher_id
    AND DATE(teacher_slots.date_from) = closestDay
    WHERE teacher_slots.date_from >= '2014-04-10 08:00:00'
        AND teacher_slots.order_of_arrival = 0
        AND teacher_slots.status = 0
        AND teacher_slots.city_id = 6015
        AND teacher_slots.subject_id = 1
)

UNION ALL

(
    SELECT id, teacher_id, date_from, date_to, order_of_arrival
    FROM teacher_slots
    WHERE order_of_arrival = 1 AND status = 0 AND city_id = 6015 AND subject_id = 1
        AND (
            (date_from <= '2014-04-10 08:00:00' AND  date_to >= '2014-04-10 08:00:00')
            OR (date_from >= '2014-04-10 08:00:00')
        )
    GROUP BY teacher_id
)

ORDER BY date_from ASC;

Pertanyaan

Apakah ada cara untuk mengoptimalkan UNION, jadi saya bisa mendapatkan respons yang masuk akal dari maksimum ~ 20 ms atau bahkan rentang pengembalian + jam berdasarkan hanya dalam satu permintaan (dengan IF, dll)?

SQL Fiddle: http://www.sqlfiddle.com/#!2/59420/1/0

EDIT:

Saya mencoba beberapa denormalisasi dengan membuat bidang "only_date_from" di mana saya hanya menyimpan tanggalnya, jadi saya bisa mengubah ini ...

DATE(MIN(date_from)) as closestDay / DATE(teacher_slots.date_from) = closestDay

... untuk ini

MIN(only_date_from) as closestDay / teacher_slots.only_date_from = closestDay

Itu sudah menyelamatkan saya 100 ms! Masih rata-rata 200 ms.

AlfredBaudisch
sumber

Jawaban:

1

Pertama, saya pikir permintaan awal Anda mungkin tidak "benar"; Dengan mengacu SQLFiddle Anda, tampak bagi saya seolah-olah Anda harus kembali baris dengan ID= 2, 3dan 4(selain baris dengan ID= 1Anda sedang mendapatkan dari setengah ini), karena logika yang ada muncul seolah-olah Anda dimaksudkan untuk baris lainnya untuk dimasukkan, karena mereka secara eksplisit memenuhi OR (date_from >= '2014-04-10 08:00:00')bagian dari WHEREklausa kedua Anda .

The GROUP BY teacher_idklausul dalam bagian kedua Anda dari Anda UNIONyang menyebabkan Anda kehilangan orang-baris. Ini karena Anda sebenarnya tidak mengagregasi kolom apa pun dalam daftar pilih Anda, dan dalam hal ini GROUP BYakan menyebabkan perilaku 'sulit untuk didefinisikan'.

Selain itu, sementara saya tidak dapat menjelaskan kinerja Anda yang buruk UNION, saya dapat mengatasinya untuk Anda dengan langsung menghapusnya dari permintaan Anda:

Alih-alih menggunakan dua set logika terpisah (dan sebagian, berulang) untuk mendapatkan baris dari tabel yang sama, saya telah mengonsolidasikan logika Anda menjadi satu kueri dengan perbedaan dalam logika Anda ORbersama-sama - yaitu jika satu baris bertemu satu atau yang lain WHEREklausa asli Anda , sudah termasuk. Ini dimungkinkan karena saya telah mengganti yang (INNER) JOINAnda gunakan untuk menemukan closestDatedengan a LEFT JOIN.

Ini LEFT JOINberarti kita sekarang juga mampu membedakan mana set logika harus diterapkan berturut-turut; Jika gabungan berfungsi (paling dekat adalah TIDAK NULL) kami menerapkan logika Anda dari babak pertama, tetapi jika bergabung gagal (paling dekat adalah IS NULL) maka kami menerapkan logika dari bagian kedua Anda.

Jadi ini akan mengembalikan semua baris yang dikembalikan oleh kueri Anda (di biola), dan itu juga mengambil yang tambahan.

  SELECT
    *

  FROM 
    teacher_slots ts

    LEFT JOIN 
    (
      SELECT 
        teacher_id,
        DATE(MIN(date_from)) as closestDay

      FROM 
        teacher_slots

      WHERE   
        date_from >= '2014-04-10 08:00:00' 
        AND order_of_arrival = 0
        AND status = 0 
        AND city_id = 6015 
        AND subject_id = 1

      GROUP BY 
        teacher_id

    ) a
    ON a.teacher_id = ts.teacher_id
    AND a.closestDay = DATE(ts.date_from)

  WHERE 
    /* conditions that were common to both halves of the union */
    ts.status = 0
    AND ts.city_id = 6015
    AND ts.subject_id = 1

    AND
    (
      (
        /* conditions that were from above the union 
           (ie when we joined to get closest future date) */
        a.teacher_id IS NOT NULL
        AND ts.date_from >= '2014-04-10 08:00:00'
        AND ts.order_of_arrival = 0
      ) 
      OR
      (
        /* conditions that were below the union 
          (ie when we didn't join) */
        a.teacher_id IS NULL       
        AND ts.order_of_arrival = 1 
        AND 
        (
          (
            date_from <= '2014-04-10 08:00:00' 
            AND  
            date_to >= '2014-04-10 08:00:00'
          )

          /* rows that met this condition were being discarded 
             as a result of 'difficult to define' GROUP BY behaviour. */
          OR date_from >= '2014-04-10 08:00:00' 
        )
      )
    )

  ORDER BY 
   ts.date_from ASC;

Selanjutnya, Anda dapat "merapikan" kueri Anda lebih lanjut sehingga Anda tidak perlu "menyambungkan" parameter Anda status, city_iddan subject_idlebih dari sekali.

Untuk melakukan ini, ubah subquery auntuk juga memilih kolom tersebut, dan juga mengelompokkan pada kolom tersebut. Kemudian, JOIN's ONklausa akan perlu untuk memetakan kolom tersebut untuk mereka ts.xxxsetara.

Saya tidak berpikir ini akan berdampak negatif pada kinerja, tetapi tidak bisa memastikan tanpa pengujian pada dataset besar.

Jadi gabung Anda akan terlihat seperti:

LEFT JOIN 
(
  SELECT 
    teacher_id,
    status,
    city_id,
    subject_id,
    DATE(MIN(date_from)) as closestDay

  FROM 
    teacher_slots

  WHERE   
    date_from >= '2014-04-10 08:00:00' 
    AND order_of_arrival = 0
  /* These no longer required here...
    AND status = 0 
    AND city_id = 6015 
    AND subject_id = 1
  */

  GROUP BY 
    teacher_id,
    status,
    city_id,
    subject_id

) a
ON a.teacher_id = ts.teacher_id
AND a.status = ts.status 
AND a.city_id = ts.city_id 
AND a.subject_id = ts.city_id
AND a.closestDay = DATE(ts.date_from)
Sepster
sumber
2

Coba kueri ini:

(
select * from (SELECT id, teacher_slots.teacher_id, date_from, date_to,  order_of_arrival
FROM teacher_slots  WHERE teacher_slots.date_from >= '2014-04-10 08:00:00'
    AND teacher_slots.order_of_arrival = 0
    AND teacher_slots.status = 0
    AND teacher_slots.city_id = 6015
    AND teacher_slots.subject_id = 1) 
 teacher_slots
JOIN (
    SELECT DATE(MIN(date_from)) as closestDay, teacher_id
    FROM teacher_slots
    WHERE   date_from >= '2014-04-10 08:00:00' AND order_of_arrival = 0
            AND status = 0 AND city_id = 6015 AND subject_id = 1
    GROUP BY teacher_id
) a ON a.teacher_id = teacher_slots.teacher_id
AND DATE(teacher_slots.date_from) = closestDay

)

UNION ALL

(
SELECT id, teacher_id, date_from, date_to, order_of_arrival
FROM teacher_slots
WHERE order_of_arrival = 1 AND status = 0 AND city_id = 6015 AND subject_id = 1
    AND (
        (date_from <= '2014-04-10 08:00:00' AND  date_to >= '2014-04-10 08:00:00')
        OR (date_from >= '2014-04-10 08:00:00')
    )
GROUP BY teacher_id
)

ORDER BY date_from ASC;
Hackerman
sumber