sql查询时间大于某一时间_查询时间从24分钟到2秒钟:记一次神奇的SQL优化
首先,我覺得有必要介紹一下 VWO(https://vwo.com/)平臺。人們可以在這個平臺上運行各種與他們的網站有關的工作負載,比如 A/B 測試、跟蹤訪問用戶、轉換、漏斗分析、渲染熱點圖、重放訪問用戶步驟,等等。
這個平臺真正強大的地方在于它所提供的報告。如果沒有這個平臺,即使企業用戶收集了大量數據也是毫無用處的,因為他們無法從數據中獲取洞見。
有了這個平臺,用戶可以針對海量數據執行各種復雜的查詢,比如下面這個: ?
Show?all clicks?by?visitors?on?webpage?"abc.com"FROM?<date?d1>?TO?<date?d2>
for?people who were either
using?Chrome?as?a browser?OR?
(were browsing?from?Europe?AND?were?using?iPhone devices)
請注意查詢中的布爾運算符,查詢接口為用戶提供了這些東西,他們可以隨意運行復雜的查詢來獲得想要的數據。
慢查詢這個用戶執行的查詢從表面上看應該是很快的: ?
Show?me all?session?recordings?for?users?who visited?any?webpage?
containing the?url?that matches the pattern?"/jobs"
這個網站的流量是非常巨大的,我們保存了數百萬個唯一的 URL。這個用戶想要查詢符合他們業務需求的 URL。
初步診斷現在讓我們來看看在數據庫方面都發生了什么。下面是相應的 SQL 語句: ?
SELECT?count(*)?
FROM?
acc_{account_id}.urls?as?recordings_urls,?
acc_{account_id}.recording_data?as?recording_data,?
acc_{account_id}.sessions?as?sessions?
WHERE?
recording_data.usp_id = sessions.usp_id?
AND?sessions.referrer_id = recordings_urls.id?
AND?( urls &&?array(select?id?from?acc_{account_id}.urls?where?url?ILIKE?'%enterprise_customer.com/jobs%')::text[] )?
AND?r_time > to_timestamp(1542585600)?
AND?r_time < to_timestamp(1545177599)?
AND?recording_data.duration >=5?
AND?recording_data.num_of_pages >?0?;
這是它的執行時間: ?
Planning?time: 1.480?msExecution?time: 1431924.650?ms
這個語句查詢的行數在 15 萬行左右。查詢計劃顯示了一些信息,但還不能看出瓶頸出在哪里。
我們再來進一步分析一下查詢語句。這個語句連接了三張表: ?
sessions:用于展示會話信息的表,例如 browser、user-agent、country,等等。
recording_data:記錄 url、頁面、時間段,等等。
urls:為了避免出現重復的 url,我們使用單獨的表對 url 進行了規范化。
另外請注意,我們使用 account_id 對這三表進行了分區,所以不可能出現因為某些賬號記錄過多導致性能變慢的情況。
尋找線索經過進一步排查,我們發現這個查詢有一些不一樣的地方。比如下面這行: ?
urls && array(select?id?from?acc_{account_id}.urls?
where?url?ILIKE?'%enterprise_customer.com/jobs%'
)::text[]
最開始我認為針對所有長 URL 執行“ILIKE”操作是導致速度變慢的元兇,但其實并不是! ?
SELECT?id?FROM?urls?WHERE?url?ILIKE?'%enterprise_customer.com/jobs%';id
--------
...
(198661 rows)
Time: 5231.765 ms
模式匹配查詢本身只花了 5 秒鐘,所以要匹配數百萬個 URL 顯然并不是個問題。
第二個可疑的地方是 JOIN 語句,或許是大量的連接操作導致速度變慢?一般來說,如果查詢速度變慢,我們首先會認為連接操作是罪魁禍首,但對于目前這個情況,我不認為是這樣的。 ?
analytics_db=# SELECTcount(*)
FROM
acc_{account_id}.urls?as?recordings_urls,
acc_{account_id}.recording_data_0?as?recording_data,
acc_{account_id}.sessions_0?as?sessions
WHERE
recording_data.usp_id = sessions.usp_id
AND?sessions.referrer_id = recordings_urls.id
AND?r_time > to_timestamp(1542585600)
AND?r_time < to_timestamp(1545177599)
AND?recording_data.duration >=5
AND?recording_data.num_of_pages >?0?;
count
-------
8086
(1?row)
Time:?147.851?ms
看,JOIN 操作實際上是很快的。
縮小可疑范圍我開始調整查詢語句,盡一切可能提升查詢性能。我和我的團隊想出了兩個方案。
針對子查詢使用 EXISTS:我們想要進一步確認問題是不是出在 URL 子查詢上。一種方法是使用 EXISTS,它會在找到第一條匹配記錄時就返回,對性能提升很有幫助。 ?
SELECT? ?count(*)?
FROM?
? ?acc_{account_id}.urls?as?recordings_urls,
? ?acc_{account_id}.recording_data?as?recording_data,
? ?acc_{account_id}.sessions?as?sessions
WHERE
? ?recording_data.usp_id = sessions.usp_id
AND?(?1?=?1?)
AND?sessions.referrer_id = recordings_urls.id
AND?(exists(select?id?from?acc_{account_id}.urls?where?url?ILIKE?'%enterprise_customer.com/jobs%'))
AND?r_time > to_timestamp(1547585600)
AND?r_time < to_timestamp(1549177599)
AND?recording_data.duration >=5
AND?recording_data.num_of_pages >?0?;
? ?count
? ?32519
? ?(1 row)
Time: 1636.637 ms
使用了 EXISTS 后,速度變快了很多。那么問題來了,為什么 JOIN 查詢和子查詢都很快,但放在一起就變得這么慢呢?
將子查詢移到 CTE 中:如果子查詢本身很快,我們可以預先計算結果,然后再傳給主查詢。 ?
WITH matching_urls AS (????select?id::text?from?acc_{account_id}.urls?where?url?ILIKE?'%enterprise_customer.com/jobs%'
)
SELECT?
? ?count(*)?FROM?acc_{account_id}.urls?as?recordings_urls,?
? ?acc_{account_id}.recording_data?as?recording_data,?
? ?acc_{account_id}.sessions?as?sessions,
? ?matching_urls
WHERE?
? ?recording_data.usp_id = sessions.usp_id?
? ?AND?(?1?=?1?)?
? ?AND?sessions.referrer_id = recordings_urls.id
? ?AND?(urls &&?array(SELECT?id?from?matching_urls)::text[])
? ?AND?r_time > to_timestamp(1542585600)?
? ?AND?r_time < to_timestamp(1545107599)
? ?AND?recording_data.duration >=5?
? ?AND?recording_data.num_of_pages >?0;
但這樣仍然很慢。
尋找元兇還有個地方之前一直被我忽略了,但因為沒有其他辦法了,所以我決定看看這個地方,那就是 && 運算符。既然 EXISTS 對性能提升起到了很大作用,那么剩下的就只有 && 可能會導致查詢變慢了。
&& 被用來找出兩個數組的公共元素。
初始查詢中的 && 是這樣的: ?
AND ( urls && array(select?id?from?acc_{account_id}.urls?where?url?ILIKE?'%enterprise_customer.com/jobs%')::text[] )我們對 URL 進行了模式匹配,然后與所有 URL 進行交集操作。這里的“urls“并不是指包含了所有 URL 的表,而是 recording_data 的”urls“列。
因為現在對 && 有所懷疑,我使用 EXPLAIN ANALYZE 對查詢語句進行了分析。 ?
Filter: ((urls && ($0)::text[])?AND?(r_time >?'2018-12-17 12:17:23+00'::timestamp with time zone)?AND?(r_time '2018-12-18 23:59:59+00'::timestamp with time zone)?AND?(duration >=?'5'::double precision)?AND?(num_of_pages >?0))Rows Removed by Filter:?52710
因為有好多行 &&,說明它被執行了好幾次。
我通過單獨執行這些過濾條件確認了是這個問題。
SELECT?1FROM?
? acc_{account_id}.urls?as?recordings_urls,?
? acc_{account_id}.recording_data_30?as?recording_data_30,?
? acc_{account_id}.sessions_30?as?sessions_30?
WHERE?
? urls &&?array(select?id?from?acc_{account_id}.urls?where?url?ILIKE?'%enterprise_customer.com/jobs%')::text[]
這個查詢的 JOIN 很快,子查詢也很快,所以問題出在 && 上面。
解決方案&& 之所以很慢,是因為兩個集合都很大。如果我把 urls 替換成{"http://google.com/","http://wingify.com/"},這個操作就很快。
我開始在谷歌上搜索如何在 Postgre 中不使用 && 進行交集操作,但并沒有找到答案。
最后,我們決定這樣做:獲取所有匹配的 urls 行,像下面這樣: ?
SELECT?urls.urlFROM?
? acc_{account_id}.urls?as?urls,
? (SELECT?unnest(recording_data.urls)?AS?id)?AS?unrolled_urls
WHERE
? urls.id = unrolled_urls.id?AND
? urls.url?ILIKE?'%jobs%'
這里沒有使用 JOIN 語句,而是使用了一個子查詢,并展開 recording_data.urls 數組,這樣就可以直接在 where 語句中應用查詢條件。
這里的 && 用來判斷一個給定的 recording 是否包含匹配的 URL。它會遍歷數組(或者說表中的行),在條件滿足時立即停止,這個看起來是不是跟 EXISTS 很像?
因為我們可以在子查詢之外引用 recording_data.urls,在必要時可以使用 EXISTS 來包裝子查詢。
把所有的東西放在一起,我們就得到了最終這個優化的查詢: ?
SELECT?? count(*)?
FROM?
? acc_{account_id}.urls?as?recordings_urls,?
? acc_{account_id}.recording_data?as?recording_data,?
? acc_{account_id}.sessions?as?sessions?
WHERE?
? recording_data.usp_id = sessions.usp_id?
? AND?(?1?=?1?)?
? AND?sessions.referrer_id = recordings_urls.id?
? AND?r_time > to_timestamp(1542585600)?
? AND?r_time < to_timestamp(1545177599)?
? AND?recording_data.duration >=5?
? AND?recording_data.num_of_pages >?0
? AND?EXISTS(
? ?SELECT?urls.url
? ?FROM?
? ? ?acc_{account_id}.urls?as?urls,
? ? ?(SELECT?unnest(urls)?AS?rec_url_id?FROM?acc_{account_id}.recording_data)?
? ?AS?unrolled_urls
? ?WHERE
? ? ?urls.id = unrolled_urls.rec_url_id?AND
? ? ?urls.url?ILIKE?'%enterprise_customer.com/jobs%'
? ?);
這個查詢的執行時間為 1898.717 毫秒,是不是值得慶祝一下?
等等,我們還要驗證一下結果是不是對的。我對 EXISTS 有所懷疑,因為它有可能會改變查詢邏輯,導致過早地退出。我們要確保不會在查詢中引入新的 bug。
我們對慢查詢和快查詢結果進行了 count(*) 比較,不同數據集的查詢結果都是一致的。對于一些較小的數據集,我們還手動比對了具體數據,也沒有問題。
學到的教訓在這次性能排查過程中,我們學到了這些東西: ?
查詢計劃并不會告訴我們所有東西,但還是很有用的;
越是明顯的疑點越不太可能是元兇;
一個慢查詢可能包含多個單獨的瓶頸點;
并非所有優化都是可簡化的;
在可能的地方使用 EXISTS 來獲得大幅性能提升。
我們將一個查詢的運行時間從 24 分鐘降到了 2 秒鐘,一個不可思議的性能提升!我們花了 1 個半到 2 個小時的時間來優化和測試這個查詢。SQL 其實是一門非常神奇的語言,只要你放開心態去擁抱它。
英文原文:
https://parallelthoughts.xyz/2019/05/a-tale-of-query-optimization/
你也「在看」嗎??
總結
以上是生活随笔為你收集整理的sql查询时间大于某一时间_查询时间从24分钟到2秒钟:记一次神奇的SQL优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: setfacl命令_一名合格的Linux
- 下一篇: mysql数据库二级233_MySQL数