修改一行SQL代碼 性能提高了100倍

在PostgreSQL中修改了一行不明顯的代碼,把(ANY(ARRAY[...]) 改爲 ANY(VALUES(...))),結果查詢時間從20s變爲0.2s。最初咱們學習使用EXPLAN ANALYZE來優化代碼,到後來,Postgres社區也成爲咱們學習提高的一個好幫手,付出總會有回報,咱們產品的性能也所以獲得了極大的提高。html

事出有因java

咱們所開發的產品是Datadog,它是專門爲那些編寫和運營大規模應用的團隊、IT運營商提供監控服務的一個平臺,幫助他們把海量的數據轉化爲切實可行的計劃、操做方案。而在這周早些時候,咱們的許多數據庫所面臨的一個性能問題是在一個較小的表上進行大量的key查詢。這些查詢中的99.9%都是高效靈活的。在極少數實例中,有些數量的性能指標tag查詢是費時的,這些查詢須要花費20s時間。這也就意味着用戶須要在瀏覽器面前花費這麼長的時間來等待圖形編輯器作出響應。即便是0.1%,這樣的用戶體驗也顯然糟透了,對此,咱們進行了監測,探究爲什麼速度會這麼慢。sql

查詢與計劃數據庫

結果使人震驚,罪魁禍首居然是下面這個簡單的查詢:數組


1
2
3
4
5
6
7
8
9
10
SELECT c.key,
c.x_key,
c.tags,
x.name
FROM context c
JOIN x
ON c.x_key = x.key
WHERE c.key = ANY (ARRAY[ 15368196 , -- 11 , 000 other keys --)])
AND c.x_key = 1
AND c.tags @> ARRAY[E 'blah' ];



X表擁有上千行數據,C表擁有1500萬行數據,這兩個表的「key」列都帶有適當的索引主鍵。簡單地說,它就是一個簡單的主鍵查詢。但有趣地是,隨着key列中記錄的增長,例如在11000行時,咱們經過添加EXPLAIN (ANALYZE, BUFFERS)前綴來查看key列的值是否與數組中的值匹配:瀏覽器


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Nested Loop  (cost= 6923.33 .. 11770.59 rows= 1 width= 362 ) (actual time= 17128.188 .. 22109.283 rows= 10858 loops= 1 )
Buffers: shared hit= 83494
->  Bitmap Heap Scan on context c  (cost= 6923.33 .. 11762.31 rows= 1 width= 329 ) (actual time= 17128.121 .. 22031.783 rows= 10858 loops= 1 )
Recheck Cond: ((tags @> '{blah}' ::text[]) AND (x_key = 1 ))
Filter: (key = ANY ( '{15368196,(a lot more keys here)}' ::integer[]))
Buffers: shared hit= 50919
->  BitmapAnd  (cost= 6923.33 .. 6923.33 rows= 269 width= 0 ) (actual time= 132.910 .. 132.910 rows= 0 loops= 1 )
Buffers: shared hit= 1342
->  Bitmap Index Scan on context_tags_idx  (cost= 0.00 .. 1149.61 rows= 15891 width= 0 ) (actual time= 64.614 .. 64.614 rows= 264777 loops= 1 )
Index Cond: (tags @> '{blah}' ::text[])
Buffers: shared hit= 401
->  Bitmap Index Scan on context_x_id_source_type_id_idx  (cost= 0.00 .. 5773.47 rows= 268667 width= 0 ) (actual time= 54.648 .. 54.648 rows= 267659 loops= 1 )
Index Cond: (x_id = 1 )
Buffers: shared hit= 941
->  Index Scan using x_pkey on x  (cost= 0.00 .. 8.27 rows= 1 width= 37 ) (actual time= 0.003 .. 0.004 rows= 1 loops= 10858 )
Index Cond: (x.key = 1 )
Buffers: shared hit= 32575
Total runtime: 22117.417 ms


此次查詢共花費22s,咱們能夠經過下圖對這22s進行很直觀的瞭解,其中大部分時間花費在Postgres和OS之間,而磁盤I/O則花費很是少的時間。

在最低水平,這些查詢看起來就像是這些CPU利用率的峯值。在這裏主要是想證明一個關鍵點:數據庫不會等待磁盤去讀取數據,而是作排序、散列和行比較這些事。編輯器

經過Postgres獲取與峯值最接近的行數。ide


顯然,咱們的查詢在大多數狀況下都有條不紊的執行着。oop

Postgres的性能問題:位圖堆掃描
post

rows_fetched度量與下面的部分計劃是一致的:


1
2
3
4
5
Buffers: shared hit= 83494
->  Bitmap Heap Scan on context c  (cost= 6923.33 .. 11762.31 rows= 1 width= 329 ) (actual time= 17128.121 .. 22031.783 rows= 10858 loops= 1 )
Recheck Cond: ((tags @> '{blah}' ::text[]) AND (x_key = 1 ))
Filter: (key = ANY ( '{15368196,(a lot more keys here)}' ::integer[]))
Buffers: shared hit= 50919


Postgres使用位圖堆掃描( Bitmap Heap Scan )來讀取C表數據。當關鍵字的數量較少時,它能夠在內存中很是高效地使用索引構建位圖。若是位圖太大,查詢優化器會改變其查找數據的方式。在咱們這個案例中,須要檢查大量的關鍵字,因此它使用了很是類似的方法來檢查候選行而且單獨檢查與x_key和tag相匹配的每一行。而全部的這些「在內存中加載」和「檢查每一行」都須要花費大量的時間。


幸運的是,咱們的表有30%都是裝載在RAM中,因此在從磁盤上檢查行的時候,它不會表現的太糟糕。但在性能上,它仍然存在很是明顯的影響。查詢過於簡單,這是一個很是簡單的key查找,因此沒有顯而易見的數據庫或應用重構,它很難找到一些簡單的方式來解決這個問題。最後,咱們使用PGSQL-Performance郵件向社區求助。

解決方案

開源幫了咱們,經驗豐富的且代碼貢獻量很是多的Tom Lane讓咱們試試這個:


1
2
3
4
5
6
7
8
9
10
SELECT c.key,
c.x_key,
c.tags,
x.name
FROM context c
JOIN x
ON c.x_key = x.key
WHERE c.key = ANY (VALUES ( 15368196 ), -- 11 , 000 other keys --)
AND c.x_key = 1
AND c.tags @> ARRAY[E 'blah' ];


你能發現有啥不一樣之處嗎?把ARRAY換成了VALUES。

咱們使用ARRAY[...]列舉出全部的關鍵字來進行查詢,但卻欺騙了查詢優化器。Values(...)讓優化器充分使用關鍵字索引。僅僅是一行代碼的改變,而且沒有產生任何語義的改變。

下面是新查詢語句的寫法,差異就在於第三和第十四行。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
Nested Loop  (cost= 168.22 .. 2116.29 rows= 148 width= 362 ) (actual time= 22.134 .. 256.531 rows= 10858 loops= 1 )
Buffers: shared hit= 44967
->  Index Scan using x_pkey on x  (cost= 0.00 .. 8.27 rows= 1 width= 37 ) (actual time= 0.071 .. 0.073 rows= 1 loops= 1 )
Index Cond: (id = 1 )
Buffers: shared hit= 4
->  Nested Loop  (cost= 168.22 .. 2106.54 rows= 148 width= 329 ) (actual time= 22.060 .. 242.406 rows= 10858 loops= 1 )
Buffers: shared hit= 44963
->  HashAggregate  (cost= 168.22 .. 170.22 rows= 200 width= 4 ) (actual time= 21.529 .. 32.820 rows= 11215 loops= 1 )
->  Values Scan on "*VALUES*" (cost= 0.00 .. 140.19 rows= 11215 width= 4 ) (actual time= 0.005 .. 9.527 rows= 11215 loops= 1 )
->  Index Scan using context_pkey on context c  (cost= 0.00 .. 9.67 rows= 1 width= 329 ) (actual time= 0.015 .. 0.016 rows= 1 loops= 11215 )
Index Cond: (c.key = "*VALUES*" .column1)
Filter: ((c.tags @> '{blah}' ::text[]) AND (c.x_id = 1 ))
Buffers: shared hit= 44963
Total runtime: 263.639 ms


從22000ms到200ms,僅僅修改了一行代碼,速度提高了100倍還多。


產品裏新的查詢

部署後的代碼:

數據庫看起來更美觀


Postgres慢查詢將一去不復返了。但有誰願意由於這個0.1%的倒黴蛋再去折磨呢?咱們使用Datadog來驗證修改是否正確,它可以作出即時驗證。若是你想查看Postgres查詢速度的各類影響, 不妨試試Datadog吧。

相關文章
相關標籤/搜索