你又不小心 IN 了嗎?

最怕有一天突然變慢

古哥
pgsql-tw
4 min readNov 9, 2022

--

SQL 語法中 IN 的用法,算是一種邁向進階的象徵吧,也可以說是一種考古題。第一次遇到的時候,有被埋了地雷的感覺。寫的時候很順利,初次上線時也很順利,直到有一天突然它引爆了 😢

先平反一下,IN 這個語法,其實是相當具有可讀性的:

不論你在查詢語法的何處看到它,應該都能很快聯想到它的資料意義,對於可讀性是相當有幫助的。

缺點很直接地說,就是效能不佳。

但它成為陷井的點卻來自於,小資料量的時候效能是不錯的,所以沒注意的話,在開發或上線初期並不會立即被發現。直到某一天資料量成長到某個門檻時,效能才會突然之間有跳躍式地「延遲」。

本篇提供給初學者預習一下這個地雷,使用 NOT IN 為例。

CREATE UNLOGGED TABLE s (key INT, value TEXT);
CREATE UNLOGGED TABLE t (key INT, value TEXT);
CREATE INDEX ON s(key);
CREATE INDEX ON t(key);
INSERT INTO s SELECT (random()*100000)::INT, md5(random()::TEXT) FROM generate_series(1, 100000);
INSERT INTO t SELECT (random()*100000)::INT, md5(random()::TEXT) FROM generate_series(1, 100000);

建立好 2 個簡單的 TABLE,INSERT 一些少量的資料。

我想要執行的查詢是:回傳有多少筆資料,其 TABLE s 的 key 在 TABLE t 中是找不到的。
應用上可能的情境是,不同通路回傳客戶資料,系統進行資料比對,找出有差異的客戶清單。

一個直觀的語法:

SELECT count(1) FROM s WHERE s.key NOT IN (SELECT key FROM t);

你可能在網路上會找到建議改成 NOT EXISTS:

SELECT count(1) FROM s WHERE NOT EXISTS (SELECT key FROM t WHERE t.key=s.key);

簡單測試之後會發現,根本沒什麼差呀!反正前面都寫好了,就趕快交差吧!實際上線還是順順的,不用太計較啦。

只是很不小心,公司生意興隆,資料當然也變多了。來模擬多餵一些資料。

INSERT INTO s SELECT (random()*100000)::INT, md5(random()::TEXT) FROM generate_series(1, 100000);
INSERT INTO t SELECT (random()*100000)::INT, md5(random()::TEXT) FROM generate_series(1, 100000);

有一天起床才突然發現,速度慢到不行。「明明昨天還好好的呀!最近我又沒有改什麼!」

資料多了一倍,時間竟然一夕之間變成快 2 萬倍!

那用 NOT EXISTS 呢?

兩相比較,其實在 IN 的查詢中,括號內的 SUBQUERY 是先被查出來,再和每一個 s.key 比較,更直接地說,就是兩層迴圈暴力跑完。

而稍有效率的 EXISTS,則是使用了 JOIN 的方式已經先對應好 s.key 和 t.key 的關係,再篩選結果出來。

那應該也可以寫成 JOIN 吧!這個查詢對應的行為是 LEFT JOIN:

SELECT count(1) FROM s LEFT JOIN t ON (s.key=t.key) WHERE t.key IS NULL;

基本上和 EXISTS 是一樣的,也很有效率。只不過寫成 JOIN 的話,可讀性相對而言就降低了不少,通常用 EXISTS 還是比較容易閱讀的。

IN 語法還是有適合的地方,畢竟寫起來很簡潔。通常我會使用在固定的常數集合,例如,列出大台北地區的據點:”據點縣市” IN (‘台北市’, ‘新北市’);其他動態查詢的情況就最好不要使用比較好。

--

--

古哥
pgsql-tw

解決不了問題,就解決提出問題的人