sql >> Databasteknik >  >> RDS >> Mysql

Hur kan jag ytterligare optimera en härledd tabellfråga som ger bättre resultat än den JOINed-motsvarigheten?

Jag hittade en lösning. Det krävdes mycket experiment, och jag tror att det var en hel del blind tur, men här är det:

CREATE TABLE magic ENGINE=MEMORY
SELECT
  s.shop_id AS shop_id,
  s.id AS shift_id,
  st.dow AS dow,
  st.start AS start,
  st.end AS end,
  su.user_id AS manager_id
FROM shifts s
JOIN shift_times st ON s.id = st.shift_id
JOIN shifts_users su ON s.id = su.shift_id
JOIN shift_positions sp ON su.shift_position_id = sp.id AND sp.level = 1

ALTER TABLE magic ADD INDEX (shop_id, dow);

CREATE TABLE tickets_extra ENGINE=MyISAM
SELECT 
  t.id AS ticket_id,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.created) = m.dow
    AND TIME(t.created) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_created,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.resolved) = m.dow
    AND TIME(t.resolved) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_resolved
FROM tickets t;
DROP TABLE magic;

Lång förklaring

Nu ska jag förklara varför det här fungerar, och min släkting genom processen och stegen för att komma hit.

Först visste jag att frågan jag försökte lida på grund av den enorma härledda tabellen och de efterföljande JOIN-erna till detta. Jag tog min välindexerade biljetttabell och sammanfogade all shift_times-data till den, och lät sedan MySQL tugga på det medan den försöker gå med i shifts och shift_positions-tabellen. Denna härledda behemoth skulle vara en oindexerad röra på upp till 2 miljoner rader.

Nu visste jag att det här hände. Anledningen till att jag gick den här vägen var dock för att det "rätta" sättet att göra detta, att strikt använda JOINs, tog ännu längre tid. Detta beror på det otäcka kaoset som krävs för att avgöra vem som är chef för ett visst skift. Jag måste gå ner till shift_times för att ta reda på vad det korrekta skiftet ens är, samtidigt som jag går ner till shift_positions för att ta reda på användarens nivå. Jag tror inte att MySQL-optimeraren hanterar detta särskilt bra, och det slutar med att den skapar en ENORM monstrositet av en tillfällig tabell över anslutningarna och filtrerar sedan bort det som inte gäller.

Så eftersom den härledda tabellen verkade vara "vägen att gå" fortsatte jag envist i detta ett tag. Jag försökte lägga ner det i en JOIN-klausul, ingen förbättring. Jag försökte skapa en temporär tabell med den härledda tabellen i den, men återigen var den för långsam eftersom den tillfälliga tabellen var oindexerad.

Jag insåg att jag var tvungen att hantera denna beräkning av skift, tider, positioner på ett förnuftigt sätt. Jag tänkte, kanske en VIEW skulle vara vägen att gå. Vad händer om jag skapade en VIEW som innehöll denna information:(shop_id, shift_id, dow, start, end, manager_id). Då skulle jag helt enkelt behöva gå med i biljetttabellen via shop_id och hela DAYOFWEEK/TIME-beräkningen, och jag skulle vara igång. Naturligtvis missade jag att komma ihåg att MySQL hanterar VIEWs ganska smidigt. Det materialiserar dem inte alls, det kör helt enkelt frågan du skulle ha använt för att få vyn åt dig. Så genom att gå med biljetter till detta körde jag i princip min ursprungliga fråga - ingen förbättring.

Så istället för en VY bestämde jag mig för att använda ett TILLFÄLLIGT BORD. Detta fungerade bra om jag bara hämtade en av cheferna (skapad eller löst) åt gången, men det gick fortfarande ganska långsamt. Jag fick också reda på att med MySQL kan du inte referera till samma tabell två gånger i samma fråga (jag skulle behöva gå med i min temporära tabell två gånger för att kunna skilja mellan manager_created och manager_resolved). Det här är en stor WTF, eftersom jag kan göra det så länge jag inte specificerar "TILLÄMPLIG" - det var här CREATE TABLE magic ENGINE=MEMORY kom in i bilden.

Med denna pseudo temporära tabell i handen försökte jag min JOIN för just manager_created igen. Det fungerade bra, men ändå ganska långsamt. Ändå, när jag gick med igen för att få manager_resolved i samma fråga, tickade frågetiden tillbaka upp i stratosfären. När du tittade på EXPLAIN visades den fullständiga tabellskanningen av biljetter (rader ~2 miljoner), som förväntat, och JOINs till det magiska bordet på ~2 087 vardera. Återigen verkade jag stöta på ett misslyckande.

Jag började nu fundera på hur jag skulle undvika JOINs helt och hållet och det var då jag hittade något obskyrt uråldrigt anslagstavlainlägg där någon föreslog att man skulle använda underval (kan inte hitta länken i min historia). Detta är vad som ledde till den andra SELECT-frågan som visas ovan (ticket_extra-skapandet). När det gäller att välja bara ett enskilt managerfält, fungerade det bra, men återigen med båda var det skit. Jag tittade på EXPLAIN och såg detta:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 2. row ***************************
           id: 3
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
*************************** 3. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
3 rows in set (0.00 sec)

Ack, den fruktade BEROENDE SUBQUERY. Det rekommenderas ofta att undvika dessa, eftersom MySQL vanligtvis kör dem på ett externt sätt, och exekverar den inre frågan för varje rad i den yttre. Jag ignorerade detta och undrade:"Tja... tänk om jag bara indexerade den här dumma magiska tabellen?". Således föddes ADD-indexet (shop_id, dow).

Kolla in det här:

mysql> CREATE TABLE magic ENGINE=MEMORY
<snip>
Query OK, 3220 rows affected (0.40 sec)

mysql> ALTER TABLE magic ADD INDEX (shop_id, dow);
Query OK, 3220 rows affected (0.02 sec)

mysql> CREATE TABLE tickets_extra ENGINE=MyISAM
<snip>
Query OK, 1933769 rows affected (24.18 sec)

mysql> drop table magic;
Query OK, 0 rows affected (0.00 sec)

Nu DET ÄR vad jag pratar om!

Slutsats

Detta är definitivt första gången jag har skapat en icke-TILLÄMPLIG tabell i farten, och INDEXERAD den i farten, helt enkelt för att göra en enda fråga effektivt. Jag antar att jag alltid har antagit att lägga till ett index i farten är en oöverkomligt dyr operation. (Att lägga till ett index på min biljetttabell med 2 miljoner rader kan ta över en timme). Ändå, för bara 3 000 rader är detta en cakewalk.

Var inte rädd för BEROENDE SUBQUERIES, skapa TILLÄMPLIGA tabeller som verkligen inte är det, indexering i farten eller utomjordingar. De kan alla vara bra saker i rätt situation.

Tack för all hjälp StackOverflow. :-D



  1. Laravel vältalig med()-> returnerar null

  2. Hur väljer man en Oracle-leverantör för .Net-applikationen?

  3. Ta bort enorma mängder data från en enorm tabell

  4. Hur man skapar ett nytt databasdiagram med MySQL Workbench