Le query individuali vengono eseguite su 10 ms, con UNION ALL stanno prendendo 290ms + (7,7 milioni di record MySQL DB). Come ottimizzare?


9

Ho una tabella che memorizza gli appuntamenti disponibili per gli insegnanti, consentendo due tipi di inserimenti:

  1. Base oraria : con totale libertà di aggiungere slot illimitati al giorno per insegnante (purché gli slot non si sovrappongano): il 15 aprile, un insegnante può avere slot alle 10:00, 11:00, 12:00 e 16:00 . Una persona viene servita dopo aver scelto un orario / orario specifico dell'insegnante.

  2. Periodo / intervallo : il 15 aprile apre un altro insegnante che può lavorare dalle 10:00 alle 12:00 e poi dalle 14:00 alle 18:00. Una persona è servita per ordine di arrivo, quindi se un insegnante lavora dalle 10:00 alle 12:00, tutte le persone che arrivano in questo periodo saranno seguite dall'ordine di arrivo (coda locale).

Dato che devo restituire tutti gli insegnanti disponibili in una ricerca, ho bisogno di salvare tutti gli slot nella stessa tabella dell'ordine degli intervalli di arrivo. In questo modo posso ordinare per data_da ASC, mostrando prima i primi slot disponibili sui risultati della ricerca.

Struttura della tabella corrente

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;

Query di ricerca

Devo filtrare per: data effettiva, city_id, subject_id e se è disponibile uno slot (status = 0).

Per ogni ora devo mostrare tutte le fasce orarie disponibili per il primo giorno disponibile più vicino per ogni insegnante (mostra tutte le fasce orarie di un determinato giorno e non posso mostrare più di un giorno per lo stesso insegnante). (Ho ricevuto la domanda con l'aiuto di mattedgod ).

Per il range based (order_of_arrival = 1), devo mostrare il range più vicino disponibile, solo una volta per insegnante.

La prima query viene eseguita individualmente in circa 0,10 ms, la seconda query 0,08 ms e UNION ALL in media 300ms.

(
    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;

Domanda

Esiste un modo per ottimizzare UNION, in modo che io possa ottenere una risposta ragionevole di un massimo di ~ 20 ms o persino un intervallo di ritorno basato su + ogni ora in una sola query (con un IF, ecc.)?

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

MODIFICARE:

Ho provato un po 'di denormalizzazione creando un campo "only_date_from" in cui ho memorizzato solo la data, quindi ho potuto cambiare questo ...

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

... a questo

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

Mi ha già salvato 100ms! Ancora 200ms in media.

Risposte:


1

In primo luogo, penso che la tua query originale potrebbe non essere "corretta"; Con riferimento alla vostra SQLFiddle, mi sembra come se si dovrebbe essere tornando righe con ID= 2, 3e 4(in aggiunta alla riga con ID= 1si stanno ottenendo da questa metà), perché la logica esistente appare come se si intendeva per queste altre righe da includere, poiché soddisfano esplicitamente la OR (date_from >= '2014-04-10 08:00:00')parte della seconda WHEREclausola.

La GROUP BY teacher_idclausola nella seconda parte del tuo ti UNIONsta facendo perdere quelle righe. Questo perché in realtà non stai aggregando alcuna colonna nel tuo elenco di selezione, e in questo caso GROUP BYcauserà un comportamento 'difficile da definire'.

Inoltre, anche se non riesco a spiegare le scarse prestazioni della tua UNION, posso aggirarla per te rimuovendola completamente dalla tua query:

Invece di usare due set di logica separati (e in parti, ripetendo) per ottenere righe dalla stessa tabella, ho consolidato la tua logica in una query con le differenze nella tua logica ORed insieme - cioè se una riga incontra l'una o l'altra delle tue WHEREclausole originali , è inclusa. Questo è possibile perché ho sostituito quello (INNER) JOINche stavi usando per trovare il closestDatecon a LEFT JOIN.

Ciò LEFT JOINsignifica che ora siamo anche in grado di distinguere quale set di logica dovrebbe essere applicato a una riga; Se il join funziona (la data più vicina NON È NULL) applichiamo la logica della prima metà, ma se il join non riesce (la data più vicina È NULL), applichiamo la logica della seconda metà.

Quindi questo restituirà tutte le righe restituite dalla tua query (nel violino) e raccoglierà anche quelle aggiuntive.

  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;

Inoltre, si può "riordinare" la query ulteriormente, in modo che non c'è bisogno di "plug-in" le vostre status, city_ide subject_idparametri più di una volta.

Per fare ciò, modifica la sottoquery aper selezionare anche quelle colonne e per raggrupparle anche su quelle colonne. Quindi, la clausola della JOINs ONdovrebbe essere necessario mappare quelle colonne ai loro ts.xxxequivalenti.

Non penso che ciò influirà negativamente sulle prestazioni, ma non posso esserne sicuro senza test su un set di dati di grandi dimensioni.

Quindi il tuo join sarà più simile a:

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)

2

Prova questa query:

(
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;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.