JavaRush /Java Blog /Random-TW /「不必要但必須的工作」所造成的 SQL 效能問題

「不必要但必須的工作」所造成的 SQL 效能問題

在 Random-TW 群組發布
理解本文所需的知識水平:對資料庫和 SQL 有一般了解,並且有一些 DBMS 的實務經驗。
SQL導致的效能問題
要編寫有效的 SQL 查詢,您可以學習的最重要的事情可能是索引。然而,緊隨其後的第二位是許多 SQL 用戶端要求資料庫執行大量「不必要但必要的工作」的知識。跟著我重複一遍:
不必要但必須的工作
什麼是「不必要但必須做的工作」?正如 Captain Obvious 告訴我們的那樣,她:

不必要

讓我們的客戶端應用程式需要以下數據:
SQL導致的效能問題
沒什麼不尋常的。我們正在使用電影資料庫(例如Sakila 資料庫),並希望向使用者顯示所有電影的標題和評分。下面的查詢可以給出我們需要的結果:
SELECT title, rating
FROM film
然而,我們的應用程式(或我們的 ORM)改為執行此查詢:
SELECT *
FROM film
我們得到什麼結果?猜測。我們收到很多無用的訊息:
SQL導致的效能問題
在右側,您甚至可以看到正在載入一些複雜的 JSON:
  • 從磁碟
  • 快取
  • 電匯
  • 為了紀念客戶
  • 最後丟掉[不必要]
是的,我們丟棄了大部分資訊。事實證明,為提取此資訊而採取的所有行動都是完全無用的。這是真的嗎?是真的嗎。

強制的

現在 - 最糟糕的部分。儘管優化器現在可以做很多事情,但這些操作對於資料庫來說是強制性的。資料庫無法知道客戶端應用程式不需要 95% 的資料。這只是最簡單的例子。想像一下連接多個表...那麼,你會說,資料庫速度很快嗎?讓我來啟發您一些您可能沒有想到的事情。當然,單一請求的執行時間並不會真正影響任何事情。好吧,它跑得慢了一倍半,但我們會撐過去的,對吧?為了方便?有時這是真的。但如果你總是為了方便而犧牲性能,這些小事就會開始累積。我們將不再談論效能(單一請求的執行速度),而是談論吞吐量(系統回應時間),然後就會出現嚴重的問題,而這些問題並不那麼容易解決。那就是你失去可擴展性的時候。讓我們來看看執行計劃,在本例中是 Oracle DBMS:
--------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes |
--------------------------------------------------
|   0 | SELECT STATEMENT  |      |  1000 |   166K|
|   1 |  TABLE ACCESS FULL| FILM |  1000 |   166K|
--------------------------------------------------
和....相比:
--------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes |
--------------------------------------------------
|   0 | SELECT STATEMENT  |      |  1000 | 20000 |
|   1 |  TABLE ACCESS FULL| FILM |  1000 | 20000 |
--------------------------------------------------
執行 SELECT * 查詢而不是 SELECT 標題,評級在資料庫中使用的記憶體多出 8 倍。沒什麼意外的吧?我們知道這會發生。但對於我們的許多請求,我們仍然同意這一點,但我們根本不需要所有這些數據。我們為資料庫創建了不必要但強制性的工作,這些工作不斷堆積。我們使用的記憶體是所需記憶體的 8 倍(當然,倍數會改變)。同時,在所有其他階段(磁碟 I/O、網路上的資料傳輸、客戶端的記憶體消耗)問題都是完全相同的,但我將跳過它們並查看...

使用索引

如今大多數資料庫已經認識到覆蓋索引的概念。覆蓋索引本身並不是一種特殊類型的索引。但它可能會成為特定查詢的“特殊索引”,無論是“偶然”還是因為它本來就是這樣的。考慮以下查詢:
SELECT *
FROM actor
WHERE last_name LIKE 'A%'
就其實施而言,沒有什麼意外的。這是一個簡單的請求。按索引查看範圍,訪問表 - 就完成了:
-------------------------------------------------------------------
| Id  | Operation                   | Name                | Rows  |
-------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                     |     8 |
|   1 |  TABLE ACCESS BY INDEX ROWID| ACTOR               |     8 |
|*  2 |   INDEX RANGE SCAN          | IDX_ACTOR_LAST_NAME |     8 |
-------------------------------------------------------------------
好計劃,不是嗎?好吧,如果我們真的需要這個,那就不需要:
SQL導致的效能問題
顯然,我們正在浪費內存等。讓我們將此查詢視為替代方案:
SELECT first_name, last_name
FROM actor
WHERE last_name LIKE 'A%'
他的計劃是這樣的:
----------------------------------------------------
| Id  | Operation        | Name            | Rows  |
----------------------------------------------------
|   0 | SELECT STATEMENT |                 |     8 |
|*  1 |  INDEX RANGE SCAN| IDX_ACTOR_NAMES |     8 |
----------------------------------------------------
我們能夠完全消除對錶的訪問,這要歸功於滿足我們查詢的所有需求的索引的存在……覆蓋索引。是不是重要?如何!這種方法可讓您將某些查詢速度加快一個數量級(或索引在某些變更後不再覆蓋時將其速度減慢一個數量級)。覆蓋索引並不總是可用。您必須為索引付費,並且不應該添加太多索引。但在這種情況下,一切都是顯而易見的。我們來評估一下性能:
SET SERVEROUTPUT ON
DECLARE
  v_ts TIMESTAMP;
  v_repeat CONSTANT NUMBER := 100000;
BEGIN
  v_ts := SYSTIMESTAMP;

  FOR i IN 1..v_repeat LOOP
    FOR rec IN (
      -- Наихудший вариант requestа: перерасход памяти ПЛЮС доступ к таблице
      SELECT *
      FROM actor
      WHERE last_name LIKE 'A%'
    ) LOOP
      NULL;
    END LOOP;
  END LOOP;

  dbms_output.put_line('Оператор 1 : ' || (SYSTIMESTAMP - v_ts));
  v_ts := SYSTIMESTAMP;

  FOR i IN 1..v_repeat LOOP
    FOR rec IN (
      -- Улучшенный request, но все равно с доступом к таблице
      SELECT /*+INDEX(actor(last_name))*/
        first_name, last_name
      FROM actor
      WHERE last_name LIKE 'A%'
    ) LOOP
      NULL;
    END LOOP;
  END LOOP;

  dbms_output.put_line('Оператор 2 : ' || (SYSTIMESTAMP - v_ts));
  v_ts := SYSTIMESTAMP;

  FOR i IN 1..v_repeat LOOP
    FOR rec IN (
      -- Оптимальный request: покрывающий индекс
      SELECT /*+INDEX(actor(last_name, first_name))*/
        first_name, last_name
      FROM actor
      WHERE last_name LIKE 'A%'
    ) LOOP
      NULL;
    END LOOP;
  END LOOP;

  dbms_output.put_line('Оператор 3 : ' || (SYSTIMESTAMP - v_ts));
END;
/

結果我們得到:


接線生 1:+000000000 00:00:02.479000000

接線生 2:+000000000 00:00:02.261000000

接線生 3:+000000000 00:00:01.857000000

請注意,actor 表只有 4 列,因此語句 1 和 2 之間的效能差異並不大,但仍然很顯著。我還要指出,我使用 Oracle 優化器提示讓優化器為查詢選擇一個或另一個特定索引。操作員 3 是我們比賽中無可爭議的獲勝者。它的性能要好得多,而且我們正在討論一個極其簡單的查詢。同樣,當我們編寫 SELECT * 時,我們為資料庫創建了不必要但強制性的工作,而資料庫無法最佳化。她不會選擇覆蓋索引,因為它的開銷比她選擇的 LAST_NAME 索引稍高,除其他外,她仍然必須訪問表以檢索無用的 LAST_UPDATE 列。但我們分析 SELECT * 越深入,事情就會變得越糟。讓我們來談談...

SQL 轉換

優化器表現如此之好,是因為它們轉換了 SQL 查詢(我最近在蘇黎世 Voxxed Days 上的演講中談到了它的工作原理)。例如,有一個極其強大的“異常JOIN”轉換。考慮以下我們必須建立的輔助視圖,以避免每次都手動連接所有這些表:
CREATE VIEW v_customer AS
SELECT
  c.first_name, c.last_name,
  a.address, ci.city, co.country
FROM customer c
JOIN address a USING (address_id)
JOIN city ci USING (city_id)
JOIN country co USING (country_id)
此視圖只是在 CUSTOMER 客戶表和各個表之間執行所有「...對一」連接以取得其位址的一部分。謝謝,正常化。想像一下,在稍微使用這個視圖之後,我們就習慣了它並忘記了基礎表。現在我們執行以下查詢:
SELECT *
FROM v_customer
結果,我們得到了一個非常令人印象深刻的計劃:
----------------------------------------------------------------
| Id  | Operation            | Name     | Rows  | Bytes | Cost |
----------------------------------------------------------------
|   0 | SELECT STATEMENT     |          |   599 | 47920 |   14 |
|*  1 |  HASH JOIN           |          |   599 | 47920 |   14 |
|   2 |   TABLE ACCESS FULL  | COUNTRY  |   109 |  1526 |    2 |
|*  3 |   HASH JOIN          |          |   599 | 39534 |   11 |
|   4 |    TABLE ACCESS FULL | CITY     |   600 | 10800 |    3 |
|*  5 |    HASH JOIN         |          |   599 | 28752 |    8 |
|   6 |     TABLE ACCESS FULL| CUSTOMER |   599 | 11381 |    4 |
|   7 |     TABLE ACCESS FULL| ADDRESS  |   603 | 17487 |    3 |
----------------------------------------------------------------
嗯,當然。資料庫正在執行所有這些連接和全表掃描,因為這就是我們告訴它要做的事情 - 獲取所有這些資料。現在,再次想像我們真正需要的是:
SQL導致的效能問題
什麼,說真的,對吧?現在你開始明白我在說什麼了。但想像我們從過去的錯誤中學到了一些東西,並執行這個更優化的查詢:
SELECT first_name, last_name
FROM v_customer
現在讓我們檢查一下發生了什麼!
------------------------------------------------------------------
| Id  | Operation          | Name        | Rows  | Bytes | Cost  |
------------------------------------------------------------------
|   0 | SELECT STATEMENT   |             |   599 | 16173 |     4 |
|   1 |  NESTED LOOPS      |             |   599 | 16173 |     4 |
|   2 |   TABLE ACCESS FULL| CUSTOMER    |   599 | 11381 |     4 |
|*  3 |   INDEX UNIQUE SCAN| SYS_C007120 |     1 |     8 |     0 |
------------------------------------------------------------------
在執行方面發生了巨大的變化,變得更好。連接已被消除,因為優化器現在可以看到它們是無用的,並且如果它可以看到這一點(並且您沒有通過選擇 * 來強制執行該工作),那麼它根本無法完成所有這些工作。為何在本案中會出現這樣的情況呢?主鍵 ADDRESS.ADDRESS_ID 的外鍵 CUSTOMER.ADDRESS_ID 保證後者只有一個值,這表示 JOIN 操作將是「...一對一」聯接,不會增加或減少行數。由於我們根本不選擇或請求任何行,因此加載它們根本沒有意義。刪除 JOIN 可能根本不會影響查詢的結果。資料庫一直在這樣做。您可以在幾乎任何資料庫上執行以下查詢:
-- Oracle
SELECT CASE WHEN EXISTS (
  SELECT 1 / 0 FROM dual
) THEN 1 ELSE 0 END
FROM dual

-- Более адекватные диалекты SQL, например, PostgreSQL
SELECT EXISTS (SELECT 1 / 0)
在這種情況下,您可能會期望拋出算術異常,就像執行以下查詢一樣:
SELECT 1 / 0 FROM dual

發生:


ORA-01476: 除數等於零

但這並沒有發生。優化器(甚至解析器)可以確保 EXISTS 謂詞(SELECT ..)中的選擇清單元素不會更改查詢結果,因此無需執行它。像這樣!

同時...

ORM 最煩人的問題之一是它們很容易編寫 SELECT * 查詢。事實上,例如在HQL/JPQL中它們一般都是預設使用的。我們可以完全省略 SELECT 子句,因為我們將檢索整個實體,對吧?例如:
FROM v_customer
例如,Vlad Mihalcea 是一位使用 Hibernate 進行開發的專家和倡導者,他建議當您確定不想在結帳後保存任何更改時,幾乎總是使用 [qualified] 查詢。ORM 大大方便了物件圖持久化問題的解決。註:堅持。實際修改物件圖和儲存變更的任務是密不可分的。但如果你不打算這麼做,那為什麼還要費力提取精華呢?為什麼不寫一個[精煉]請求?讓我們明確一點:從效能的角度來看,專門針對您的特定用例編寫查詢顯然比任何其他選項都要好。您可能不在乎,因為您的資料集很小而且並不重要。偉大的。但是,當您最終需要可擴展性時,重新設計應用程式以使用查詢而不是實體圖的命令式遍歷將非常具有挑戰性。沒有它你也會有事可做。

計算行數以查明是否存在某些內容

最嚴重的資源浪費之一是執行 COUNT(*) 查詢只是為了查看資料庫中是否有某些內容。例如,我們需要找出給定用戶是否有訂單。我們執行請求:
SELECT count(*)
FROM orders
WHERE user_id = :user_id
初級。如果 COUNT = 0,則沒有訂單。否則,是的。效能不會那麼差,因為我們可能在 ORDERS.USER_ID 欄位上有一個索引。但是您認為上述查詢的效能與以下選項相比如何:
-- Oracle
SELECT CASE WHEN EXISTS (
  SELECT *
  FROM orders
  WHERE user_id = :user_id
) THEN 1 ELSE 0 END
FROM dual

-- Более адекватные диалекты SQL, например, PostgreSQL
SELECT EXISTS (
  SELECT *
  FROM orders
  WHERE user_id = :user_id
)
不需要火箭科學家就能發現,真正的存在謂詞一旦找到第一個字串就會停止搜尋其他字串。因此,如果結果是“無訂單”,那麼速度將是可比的。但如果結果是“是的,有訂單”,那麼在不需要清點具體數量的情況下,會更快收到答复。畢竟,我們對確切的數字不感興趣。然而,我們告訴資料庫計算它(不必要的工作),而資料庫不知道我們忽略了所有大於 1 的結果(必需的工作)。當然,如果我們在 JPA 支援的集合上呼叫 list.size() 來獲得相同的結果,情況會更糟。 我之前已經在我的部落格上寫過這一點,並對這兩個選項進行了比較測試...

結論

這篇文章陳述了顯而易見的事。不要強迫資料庫做不必要但必要的工作。這是不必要的,因為根據要求,您知道不需要完成某些特定的工作。但是,您告訴資料庫執行此操作。這是必需的,因為資料庫無法確保這項工作是不必要的。此資訊僅對客戶端可用,對伺服器不可用。所以資料庫必須執行它。這篇文章重點討論了 SELECT *,主要是因為它是一個非常方便查看的物件。但這不僅適用於資料庫。這適用於所有分散式演算法,其中客戶端告訴伺服器做不必要但必需的工作。在您的平均 AngularJS 應用程式中有多少個N+1任務,其中 UI 循環存取服務 A 的結果,多次呼叫服務 B,而不是將對 B 的所有呼叫打包到單一呼叫中?這是一個非常普遍的現象。解決方案總是相同的。您向執行命令的實體提供的資訊越多,它(理論上)執行這些命令的速度就越快。編寫最佳查詢。總是。您的整個系統都會為此感謝您。 來源文章
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION