[英]Optimize query with 1 join, on tables with 10+ millions rows
我正在考慮使用 2 個表更快地提出請求。
我有以下 2 個表:
表“日志”
id varchar(36) PK
date timestamp(2)
該表具有 PHP Laravel 框架所稱的與其他幾個對象的“多態多對多”關系,因此還有第二個表“logs_pivot”:
id unsigned int PK
log_id varchar(36) FOREIGN KEY (logs.id)
model_id varchar(40)
model_type varchar(50)
有一個或幾個條目logs_pivot
每個條目logs
。 它們分別有 20+ 和 10+ 百萬行。
我們做這樣的查詢:
select * from logs
join logs_pivot on logs.id = logs_pivot.log_id
where model_id = 'some_id' and model_type = 'My\Class'
order by date desc
limit 50;
顯然,我們在 model_id 和 model_type 字段上都有一個復合索引,但請求仍然很慢:每次都有幾(幾十)秒。
我們在date
字段上也有一個索引,但EXPLAIN
顯示這是使用的model_id_model_type
索引。
解釋聲明:
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
| 1 | SIMPLE | logs_pivot | NULL | ref | logs_pivot_model_id_model_type_index,logs_pivot_log_id_index | logs_pivot_model_id_model_type_index | 364 | const,const | 1 | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | logs | NULL | eq_ref | PRIMARY | PRIMARY | 146 | the_db_name.logs_pivot.log_id | 1 | 100.00 | NULL |
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
在其他表中,通過在索引中包含日期字段,我能夠更快地發出類似的請求。 但在這種情況下,它們位於單獨的表中。
當我們想要訪問這些數據時,它們通常是幾個小時/幾天前的。
我們的 InnoDB 池太小,無法在內存中保存所有數據(+所有其他表),因此數據很可能總是在磁盤上查詢。
我們可以通過哪些方式更快地提出該請求?
理想情況下,只使用另一個索引,或者通過改變它的完成方式。
非常感謝 !
編輯 17h05 :
到目前為止,謝謝大家的回答,我會嘗試像 O Jones 建議的那樣,並以某種方式將日期字段包含在數據透視表中,以便我可以包含在索引索引中。
編輯 14/10 10 小時。
解決方案 :
所以我最終改變了請求的真正完成方式,通過對數據透視表的 id 字段進行排序,這確實允許將其放入索引中。
此外,當未按日期過濾時,計算總行數的請求更改為僅在數據透視表上完成。
謝謝你們 !
我看到兩個問題:
當表相對於 RAM 大小而言很大時,UUID 成本很高。
由於WHERE
子句來自一個表,而ORDER BY
列來自另一個表,因此無法以最佳方式處理LIMIT
。 也就是說,它將執行所有JOIN
,然后排序並最后剝離幾行。
只是一個建議。 使用復合索引顯然是一件好事。 另一種可能是按日期對 ID 進行資格預審,並根據 (model_id, model_type, log_id ) 上的 logs_pivot 表索引擴展您的索引。
如果您的查詢數據和整個歷史記錄是 20 多萬條記錄,那么數據可以追溯到多遠,您只需要處理每個給定的模型 ID/類型類別的 50 條記錄的限制。 說3個月? vs 說你 5 年的日志? (沒有在帖子中列出,只是一個例子)。 因此,如果您可以查詢日期大於 3 個月前的最小日志 ID,則該 ID 可以限制您的 logs_pivot 表中發生的其他事情。
就像是
select
lp.*,
l.date
from
logs_pivot lp
JOIN Logs l
on lp.log_id = l.id
where
model_id = 'some_id'
and model_type = 'My\Class'
and log_id >= ( select min( id )
from logs
where date >= datesub( curdate(), interval 3 month ))
order by
l.date desc
limit
50;
因此,log_id 的 where 子句只執行一次,並且只返回 3 個月前的 ID,而不是 logs_pivot 的整個歷史記錄。 然后使用優化后的model id/type 兩部分鍵進行查詢,同時也使用索引鍵中包含的ID 跳轉到其索引的末尾以跳過所有歷史記錄。
您可能想要包含的另一件事是一些預聚合表,其中包含每個給定模型類型/ID 的記錄數量,例如每月/每年。 將其用作向用戶呈現的預查詢,然后您可以將其用作深入了解以進一步獲取更多詳細信息。 預聚合表可以對所有歷史內容進行一次,因為它是靜態的並且不會改變。 您唯一需要不斷更新的是當前單月期間的內容,例如每晚更新一次。 或者甚至可能更好,通過觸發器在每次添加完成時插入記錄,或者根據年/月聚合更新給定模型/類型的計數。 同樣,只是一個建議,因為沒有關於如何/為什么將數據呈現給最終用戶的其他上下文。
SELECT columns FROM big table ORDER BY something LIMIT small number
是一個臭名昭著的查詢性能反模式。 為什么? 服務器對一大堆長行進行排序,然后丟棄幾乎所有的行。 您的其中一columns
是 LOB -- 一個 TEXT 列並沒有幫助。
這是一種可以減少這種開銷的方法:通過查找所需的主鍵集來確定所需的行,然后僅獲取這些行的內容。
你想要什么行? 這個子查詢會找到它們。
SELECT id
FROM logs
JOIN logs_pivot
ON logs.id = logs_pivot.log_id
WHERE logs_pivot.model_id = 'some_id'
AND logs_pivot.model_type = 'My\Class'
ORDER BY logs.date DESC
LIMIT 50
這完成了計算您想要的行的所有繁重工作。 因此,這是您需要優化的查詢。
可以通過logs
上的這個索引來加速
CREATE INDEX logs_date_desc ON logs (date DESC);
以及logs_pivot
上的這個三列復合索引
CREATE INDEX logs_pivot_lookup ON logs_pivot (model_id, model_type, log_id);
這個索引可能會更好,因為優化器會看到對logs_pivot
的過濾,而不是對logs
的過濾。 因此,它將首先查看logs_pivot
。
或者可能
CREATE INDEX logs_pivot_lookup ON logs_pivot (log_id, model_id, model_type);
先嘗試一個,然后再嘗試另一個,看看哪個會產生更快的結果。 (我不確定 JOIN 將如何使用復合索引。)(或者簡單地添加兩者,然后使用EXPLAIN
來查看它使用的是哪一個。)
然后,當您對子查詢的性能感到滿意或滿意時,使用它來獲取所需的行,如下所示
SELECT *
FROM logs
WHERE id IN (
SELECT id
FROM logs
JOIN logs_pivot
ON logs.id = logs_pivot.log_id
WHERE logs_pivot.model_id = 'some_id'
AND model_type = 'My\Class'
ORDER BY logs.date DESC
LIMIT 50
)
ORDER BY date DESC
這是有效的,因為它對較少的數據進行排序。 logs_pivot
上的覆蓋三列索引也將有所幫助。
請注意,子查詢和主查詢都有 ORDER BY 子句,以確保返回的詳細結果集符合您需要的順序。
編輯Darnit,使用 MariaDB 10+ 和 MySQL 8+ 太久了,我忘記了舊的限制。 試試這個。
SELECT *
FROM logs
JOIN (
SELECT id
FROM logs
JOIN logs_pivot
ON logs.id = logs_pivot.log_id
WHERE logs_pivot.model_id = 'some_id'
AND model_type = 'My\Class'
ORDER BY logs.date DESC
LIMIT 50
) id_set ON logs.id = id_set.id
ORDER BY date DESC
最后,如果您知道您只關心比某個時間更新的行,您可以將這樣的內容添加到您的子查詢中。
AND logs.date >= NOW() - INTERVAL 5 DAY
如果您的表中有大量歷史數據,這將很有幫助。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.