在對 LIMIT 進行最小更改時,對兩個索引列的 BETWEEN 查詢以指數方式花費更長的時間
這是我的問題的一個最小範例,但我用這種結構重現了這個問題。我有下表:
| column name | type | |----------------+-------------| | id | PRIMARY KEY | | id_start | INT | | id_end | INT | |----------------+-------------|
該表包含大約十億 (1,000,000,000) 行。和
_start
列_end
被索引。這是使用 Btree 索引的 InnoDB。一些背景資訊:這是帶有開始和結束時間戳的感測器數據。時間戳本身保存在另一個表中,該表引用自該表。因此,我只使用
id
這裡的列。以下查詢返回大約 15,000 行:
SELECT * FROM table WHERE 310000 BETWEEN id_start AND id_end;
問題是查詢需要很長時間才能返回任何內容(MySQL Workbench 持續時間 1 秒/獲取 220 秒)。
如果我附加
LIMIT 14000
到查詢中,它幾乎會立即返回。參數越接近LIMIT
預期的 15000 結果行,查詢所需的時間就越長。獲取 14,000 行與 15,000 行所需的時間呈指數增長,這可能是什麼原因?
還有一些我不確定它們是否與這個問題相關的資訊:
- DB 伺服器配置良好(InnoDB 緩衝區等)。
- DB 伺服器的 CPU 功率和記憶體比所需的要多。整個數據集適合記憶體。
- 該表中的所有列都不能為空。
此處要求提供有關表結構和查詢的附加資訊。
CREATE TABLE `table` ( `id` int(11) NOT NULL AUTO_INCREMENT, `id_start` int(11) NOT NULL, `id_end` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `idx_end` (`id_end`), KEY `idx_start` (`id_start`), KEY `idx_start_end` (`id_start`,`id_end`), KEY `idx_end_start` (`id_end`,`id_start`) ) ENGINE=InnoDB;
這裡是查詢的 EXPLAIN 語句:
+----+-------------+-------+------------+-------+-------------------------------------------------+---------------+---------+------+----------+----------+---------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+-------------------------------------------------+---------------+---------+------+----------+----------+---------------+ | 1 | SIMPLE | table | NULL | range | "idx_start,idx_end,idx_start_end,idx_end_start" | idx_end_start | 9 | NULL | 20509120 | 50.00 | "Using where" | +----+-------------+-------+------------+-------+-------------------------------------------------+---------------+---------+------+----------+----------+---------------+
這是低的結果
LIMIT
EXPLAIN
:'{ "query_block": { "select_id": 1, "cost_info": { "query_cost": "27795648.61" }, "table": { "table_name": "table", "access_type": "range", "possible_keys": [ "idx_start", "idx_end", "idx_start_end", "idx_end_start" ], "key": "idx_end_start", "used_key_parts": [ "id_end" ], "key_length": "9", "rows_examined_per_scan": 19854034, "rows_produced_per_join": 9927018, "filtered": "50.00", "index_condition": "(310300 between `table`.`id_start` and `table`.`id_end`)", "cost_info": { "read_cost": "25810244.97", "eval_cost": "1985403.64", "prefix_cost": "27795648.61", "data_read_per_join": "4G" }, "used_columns": [ "id", "id_start", "id_end" ] } } }'
這是高的結果
LIMIT
EXPLAIN
:'{ "query_block": { "select_id": 1, "cost_info": { "query_cost": "27203639.01" }, "table": { "table_name": "table", "access_type": "range", "possible_keys": [ "idx_start", "idx_end", "idx_start_end", "idx_end_start" ], "key": "idx_end_start", "used_key_parts": [ "id_end" ], "key_length": "9", "rows_examined_per_scan": 19431170, "rows_produced_per_join": 9715586, "filtered": "50.00", "index_condition": "(310300 between `table`.`id_start` and `table`.`id_end`)", "cost_info": { "read_cost": "25260521.78", "eval_cost": "1943117.23", "prefix_cost": "27203639.01", "data_read_per_join": "4G" }, "used_columns": [ "id", "id_start", "id_end" ] } } }'
我看到兩個問題:
- 有什麼
LIMIT
影響性能?- (真正的問題。)如何加快對這種表的這種查詢。
這是 MySQL 中經典的“艱鉅任務”——在“開始”和“結束”值之間尋找元素。
我在http://mysql.rjweb.org/doc.php/ipranges中使用了一些額外的複雜性來解決它。但是,它要求範圍不重疊。(目前尚不清楚您的範圍是否會重疊。)
你說這
id_start
真的是指向另一個保存實際時間戳的表的指針嗎?然而你使用WHERE 310000 BETWEEN id_start AND id_end
. 這意味著id_start
並且id_end
是按時間順序排列的,儘管實際上不是“時間”的表示?同上310000
?也許這是使用 4 字節 INT 而不是更大的TIMESTAMP(6)
東西?有了
EXPLAIN FORMAT=JSON SELECT ...
,我們或許能夠弄清楚為什麼更改LIMIT
值會影響性能。時間分佈有多均勻?以下是您所看到的差異的猜測: 對於 的值較低
LIMIT
,優化器的統計數據表明某個查詢計劃會很快找到 14K,但如果值較大,它會推斷它可能需要搜尋很遠才能找到 15K行。優化器沒有“時間範圍”的概念。因此它只能使用INDEX(start)
orINDEX(end)
; 它不知道您知道並可以用來優化查詢的start<end
其他內容。你還需要
id
什麼嗎?表中是否還有其他列? 這兩個都是重要的問題,因為以下優化可能會失敗,具體取決於您的答案。對於這個 3 列表,我會考慮以下替代方案之一:
CREATE TABLE x ( start_id INT UNSIGNED NOT NULL, end_id INT UNSIGNED NOT NULL, PRIMARY KEY(start_id, end_id), -- assumed unique INDEX (end_id, start_id) ENGINE=InnoDB; -- assumes start-end pairs are unique -- with a billion rows; fewer indexes is beneficial CREATE TABLE x ( start_id INT UNSIGNED NOT NULL, end_id INT UNSIGNED NOT NULL, id INT UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(start_id, end_id, id), -- id assures uniqueness INDEX (end_id, start_id), INDEX(id) -- sufficient to keep AUTO_INCREMENT happy ENGINE=InnoDB; -- does not assume start-end pairs are unique -- allows for FK to `id`
筆記:
- 通過讓 PK 開始
start_id
可能有助於提高性能。- start..end 查詢的性能沒有得到解決。
- 如果有更多列,我還沒有評估這些模式的好處。