2012-07-30 33 views
0

我有兩個MySQL數據庫表,如下所述。一個表包含設備信息,另一個表是關於每個設備的一對多日誌。這個查詢是不是複雜的?

CREATE TABLE `device` (
    `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    `name` VARCHAR(255) NOT NULL, 
    `active` INT NOT NULL DEFAULT 1, 
    INDEX (`active`) 
); 

CREATE TABLE `log` (
    `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    `device_id` INT NOT NULL, 
    `message` VARCHAR(255) NOT NULL, 
    `when` DATETIME NOT NULL, 
    INDEX (`device_id`) 
); 

我想要做的是在單個查詢(如果可能)中抓取設備信息以及每個設備的最新日誌條目。到目前爲止,我有如下:

SELECT d.id, d.name, l.message 
FROM device AS d 
LEFT JOIN (
    SELECT l1.device_id, l1.message 
    FROM log AS l1 
    LEFT JOIN log AS l2 ON (l1.device_id = l2.device_id AND l1.when < l2.when) 
    WHERE l2.device_id IS NULL 
) AS l ON (d.id = l.device_id) 
WHERE d.active = 1 
GROUP BY d.id 
ORDER BY d.id ASC; 

這些查詢都是我的實際設置,在我的日誌表是超過10萬行(實際上有幾個日誌表我看看)的簡單複製品。查詢確實運行,但非常非常緩慢(比如超過兩分鐘)。我確信有一個更簡潔/優雅/「SQL」的方式來形成這個查詢來獲得我需要的數據,但我還沒有找到它。

即使沒有醜陋的sub-SELECT和self-JOIN,我想要做什麼?我可以用不同的策略完成工作嗎?或者,查詢的本質是不可複製的?

同樣,應用程序的邏輯是這樣的,我可以「手動加入」表,如果這不起作用,但我覺得MySQL應該能夠處理這樣的事情而不窒息 - 但我承認綠色當涉及到這種複雜的集合代數。

編輯:由於這是一個人爲的例子,我忘了給索引添加到device.active

+0

如果您在添加索引(DEVICE_ID時)?這可能會使JOIN更有效率。 – 2012-07-30 21:25:18

+0

正如你經歷過的第一手MySQL越慢查詢越大,你存儲的數據越多等等。如果你的情況超過了100k行,我會推薦使用不同的解決方案:NoSQL – libjup 2012-07-30 21:28:51

+7

@libjup,without意思是把OP放到10萬行並不是很大,實際上它相當小。建議不僅要改變RDBMS而且要改變數據庫管理系統,因爲有一個10萬個表是一個巨大的過度反應。 – Ben 2012-07-30 21:30:44

回答

3

這裏有一個稍微不同的方式來查詢,避免自聯接:

SELECT d.id, d.name, l.message 
FROM device AS d 
LEFT JOIN (
    SELECT l1.device_id, l1.message 
    FROM log AS l1 
    WHERE l1.when = (
     SELECT MAX(l2.when) 
     FROM log AS l2 
     WHERE l2.device_id = l1.device_id 
) l ON l.device_id = d.id 
WHERE d.active = 1 
ORDER BY d.id ASC; 

由於100k不是一個非常大的表,即使沒有適當的索引,我也不會期望這個查詢花費幾秒鐘。但是,如評論所示,您可以考慮根據您的explain plan的結果添加其他索引。

+0

不錯,但可能是MAX()而不是MIN()? – KolA 2012-07-31 02:00:37

+0

@KolA哎呀!你是對的......感謝你指出了這一點! – 2012-07-31 03:32:53

0

你的查詢,以及下面的策略將從指數ON log(device_id,when)受益。該索引可以取代索引ON log(device_id),因爲該索引是多餘的。


如果有日誌條目的整體一大堆每個設備時,在查詢JOIN將會產生良好的中小型中間結果集,這將得到滲透到每個設備一行。我不相信MySQL優化器對於反連接操作有任何「快捷方式」(至少不是5.1)......但是您的查詢可能是最有效的。

問:我可以用不同的策略完成工作嗎?

是的,還有其他的策略,但我不知道任何這些都比你的查詢「更好」。


UPDATE:你可能會考慮是增加另一個表,您的架構,一個適用於每個設備的最新的日誌條目

一種策略。這可以通過log表中定義的TRIGGER來維護。如果您只執行插入操作(不更新最新日誌條目的UPDATE和DELETE,則非常簡單,只要針對log表執行插入操作,就會觸發AFTER INSERT FOR EACH ROW觸發器,該觸發器將插入到日誌中的when值device_id的表格與log_latest表格中當前的when值相比較,並插入/更新log_latest表格中的行,以便最新的行始終存在。或者,您可以將latest_whenlatest_message列添加到設備表中,並將其保留在那裏。)

但是,這種策略超出了您的原始問題......但是如果您需要頻繁運行「針對所有設備的最新日誌消息」查詢,這是一個可行的策略。缺點是你有一張額外的表格,並且在執行插入log表格時性能受到影響。這個表格可以使用類似你的原始查詢或下面的替代方法完全刷新。


一種方法是查詢,做了簡單的devicelog表的加盟,獲得由設備和下降when命令行。然後使用一個內存變量來處理行,過濾除「最新」日誌條目以外的所有行。請注意,此查詢返回一個額外的列。 (這額外的一列可以通過包裹整個查詢作爲內嵌視圖中刪除,但你可能會得到更好的性能,如果你可以返回一個額外的列活:

SELECT IF(s.id = @prev_device_id,0,1) AS latest_flag 
    , @prev_device_id := s.id AS id 
    , s.name 
    , s.message 
    FROM (SELECT d.id 
      , d.name 
      , l.message 
      FROM device d 
      LEFT 
      JOIN log l ON l.device_id = d.id 
     WHERE d.active = 1 
     ORDER BY d.id, l.when DESC 
     ) s 
    JOIN (SELECT @prev_device_id := NULL) i 
HAVING latest_flag = 1 

什麼在選擇第一表達列表正在做的是「標記」一行,只要該行上的設備標識值與前一行中的設備標識差異HAVING子句過濾掉所有未標記爲1的行(可以省略HAVING子句來看看這個表達式是如何工作的。)

(我沒有測試過這個語法錯誤,如果你有錯誤,讓我知道,我會仔細看看,我的桌面檢查說沒關係...但我可能錯過了一個paren或comm一,)

(您可以通過包裝,在另一個查詢「擺脫」額外列

SELECT r.id,r.name,r.message FROM (
/* query from above */ 
) r 

(但同樣,這可能會影響性能,你可能會得到,如果你能更好的性能與額外的列一起生活)

當然,在最外層的查詢中添加一個ORDER BY,以確保您的結果集按您需要的方式排序。

這種方法對於一大堆設備來說工作得很好,而且在日誌中只有幾個相關的行。否則,這將產生大量的中間結果集(按照日誌表中的行數),該結果集將被轉移到臨時的MyISAM表中。

UPDATE:

如果從device基本上讓所有的行(其中謂詞是不是非常有選擇性的),你也許可以得到通過獲得在每一個DEVICE_ID最新的日誌條目更好的性能log表,並推遲加入device表。 (但注意,指數將不提供設置爲做好加入該中間結果,所以它真的需要測試來衡量性能。)

SELECT d.id 
    , d.name 
    , t.message 
    FROM device d 
    LEFT 
    JOIN (SELECT IF(s.device_id = @prev_device_id,0,1) AS latest_flag 
      , @prev_device_id := s.device_id AS device_id 
      , s.messsage 
      FROM (SELECT l.device_id 
        , l.message 
        FROM log l 
       ORDER BY l.device_id DESC, l.when DESC 
       ) s 
      JOIN (SELECT @prev_device_id := NULL) i 
     HAVING latest_flag = 1 
     ) t 
    ON t.device_id = d.id 

注:我們指定兩個降序內聯視圖的ORDER BY子句中的device_idwhen列別名爲s,這不是因爲我們需要降序device_id順序的行,而是允許MySQL通過允許MySQL執行「反向掃描」操作來避免文件操作操作帶有前導列的索引(device_id,when)。

NOTE:該查詢仍然會將中間結果集作爲臨時MyISAM表進行假脫機,並且不會有任何索引。所以它的可能性不如原來的查詢。


另一種策略是在SELECT列表中使用相關子查詢。你只返回從日誌表中的單個列,所以這是很容易查詢到理解:

SELECT d.id 
    , d.name 
    , (SELECT l.message 
      FROM log l 
      WHERE l.device_id = d.id 
      ORDER BY l.when DESC 
      LIMIT 1 
     ) AS message 
    FROM device d 
WHERE d.active = 1 
ORDER BY d.id ASC; 

注:由於id是在device表的主鍵(或唯一鍵),和由於您沒有執行任何會生成額外行的JOIN,因此可以省略GROUP BY子句。

注:此查詢將使用「嵌套循環」操作。也就是說,對於從device表返回的每一行,(實質上)需要運行單獨的查詢以從日誌中獲取相關行。對於只有少數device行(如將與在device表更具選擇性的謂詞被退回),併爲每個設備日誌條目的一大堆,性能不會太差。但對於很多設備,每個設備只有幾條日誌消息,其他方法很可能會更加高效。)

另請注意,使用此方法時請注意,您可以輕鬆地將其擴展爲也返回第二個最新的日誌消息作爲一個單獨的列,通過向SELECT列表添加另一個子查詢(就像第一個子查詢),只需更改LIMIT子句跳過第一行,然後獲取第二行。

 , (SELECT l.message 
      FROM log l 
      WHERE l.device_id = d.id 
      ORDER BY l.when DESC 
      LIMIT 1,1 
     ) AS message_2 

對於從設備獲得基本上都行,你可能會得到使用JOIN操作的最佳性能。這種方法的一個缺點是,當有兩個(或更多)行與設備的最新when值匹配時,它有可能爲設備返回多行。 (基本上,這種做法是保證返回一個「正確」的結果集的時候,我們有一個保證log(device_id,when)是唯一

有了這個查詢作爲內嵌視圖,以獲得「最新的」當值:

SELECT l.device_id 
    , MAX(l.when) 
    FROM log l 
GROUP BY l.device_id 

我們可以加入此將日誌和設備表。

SELECT d.id 
    , d.name 
    , m.messsage 
    FROM device d 
    LEFT 
    JOIN (
     SELECT l.device_id 
       , MAX(l.when) AS `when` 
      FROM log l 
      GROUP BY l.device_id 
     ) k 
    ON k.device_id = d.id 
    LEFT 
    JOIN log m 
    ON m.device_id = d.id 
     AND m.device_id = k.device_id 
     AND m.when = k.when 
ORDER BY d.id 

所有這些都是備選策略(我相信是你問的問題),但我也不清楚箇中ose將會更好地滿足您的特殊需求。 (但它總是好的有幾個不同的工具,在工具帶酌情使用。)

+0

相關的子查詢幾乎總是表現最差的代碼。你永遠不應該建議他們替換派生表。它是逐行運行的東西和作爲數據集運行的東西之間的差異。最好養成使用正確技術的習慣,而不是像這樣使用劣質技術。 – HLGEM 2012-07-30 21:59:30

+2

@HLGEM:有時,相關的子查詢是最有效的方法。實際上,在某些情況下,它是返回指定結果集的最有效方法。 (我相信我在回答中包含了關於這種方法對性能問題的注意事項。)派生表不是一個神奇的子彈,它們也有一些性能方面的考慮。當然,你可以自由地相信相關的子查詢是一種詛咒,你可以自由地認爲這是一種「不良技術」,並且「從不建議」它們。 OP要求採取替代策略。相關的子查詢就是這樣。 – spencer7593 2012-07-30 22:17:58

+0

在我看來,這些解決方案都不如原來那麼複雜? – 2012-07-31 17:40:27

1

這裏的,只需要一個日誌表的實例替代:

SELECT d.id, d.name, 
      SUBSTRING_INDEX(
       GROUP_CONCAT(
        l.message 
        SEPARATOR '~' 
        ORDER BY l.when DESC 
      ) 
      , '~' 
      , 1 
     ) 
FROM  device d 
LEFT JOIN log l 
ON  d.id = l.device_id 
WHERE  d.active = 1 
GROUP BY d.id 

此查詢通過創建消息的波浪線分隔的列表,通過按照從大到小的順序日期排序查找最近的日誌信息。這由GROUP_CONCAT完成。該列表的第一個條目的SUBSTRING_INDEX芯片。

有2個缺點,這種方法:

  • 它使用GROUP_CONCAT。如果該函數的結果變得太長,結果將被截斷。您可以彌補,如果你在運行查詢之前做

    SET @@group_concat_max_len = @@max_allowed_packet;

。您甚至可以做得更好:因爲您只想獲取一條消息,所以您可以將group_concat_max_len設置爲與message列的最大字符長度一樣大。與使用@@max_alowed_packet相比,這將節省大量內存。

  • 它依賴於一個不能出現在消息文本中的特殊分隔符(在本例中是tilde('~'))。只要您確定它不出現在消息文本中,就可以將其更改爲您喜歡的任何分隔符字符串。

如果你能忍受這些限制,那麼這個查詢可能是最快的。

以下是更多與您的選擇一樣複雜的替代方案,但性能可能會更好。

SELECT d.id 
,   d.name 
,   l.message 
FROM  (
      SELECT d.id, d.name, MAX(l.when) lmax 
      FROM  device d 
      LEFT JOIN log l 
      ON  d.id = l.device_id 
      WHERE  d.active = 1 
      GROUP BY d.id 
     ) d 
LEFT JOIN log  l 
ON  d.id = l.device_id 
AND  d.lmax = l.when 
ORDER BY d.id ASC; 

另一種選擇:

SELECT d.id 
,   d.name 
,   l2.message 
FROM  device d 
LEFT JOIN (
      SELECT l.device_id 
      ,  MAX(l.when) lmax 
      FROM  log l 
      GROUP BY l.device_id 
     ) l1 
ON  d.id = l1.device_id 
LEFT JOIN log  l2 
ON  l1.device_id = l2.device_id 
AND  l1.lmax  = l2.when 
WHERE  d.active  = 1 
ORDER BY d.id ASC; 
+0

GROUP_CONCAT查詢很聰明。至少,我認爲你打算在GROUP_CONCAT函數中包含'SEPARATOR'〜''這就是我閱讀它的方式。 – spencer7593 2012-07-31 18:34:02

+0

@ spencer7593謝謝!確實很好的電話,我忘了分隔條款!編輯以反映這一點。 – 2012-07-31 19:20:30