Tri*_*und 6 sqlite performance subquery query-performance
我在做一些相当轻量级的数据按摩/清洁跑进其中使用相关子查询(可能是错误的)JOIN的一个版本跑了太大的问题很多比我相信这是正确的慢。我不问如何做查询(我相信现在我已经得到了正确的),但我想知道为什么慢版是如此缓慢。
问题
该域是一个相当简单的数据库,用于管理彩票辛迪加(记录会员付款、玩的游戏和获胜)。在转向新引擎 (SQLite) 时,我正在尝试清理数据并改进表的结构。
现有_Winnings
表格记录了赢得的金额和日期以及“游戏类型”(可以玩多个游戏):
CREATE TABLE [_Winnings](
[ID] integer primary key not null,
[WinDate] date,
[Amount] integer,
[GameType] integer references _Games(ID)
);
CREATE INDEX [_WinningsIndex] on _Winnings(GameType) ;
Run Code Online (Sandbox Code Playgroud)
主要问题是没有链接(除了获胜日期)到实际玩的游戏。这些记录已经被迁移,现在保存在一个EventHistory
表中:
CREATE TABLE [EventHistory](
[ID] integer primary key not null,
[EventType] integer references Events(ID),
[GameType] integer references Games(ID),
[EventDate] date
);
CREATE INDEX [EventHistoryEventIndex] on EventHistory(EventType) ;
CREATE INDEX [EventHistoryGameIndex] on EventHistory(GameType) ;
CREATE INDEX [EventHistoryDateIndex] on EventHistory(EventDate) ;
Run Code Online (Sandbox Code Playgroud)
三个表_Games
,Games
并Events
持有游戏/事件的“类型”,基本上具有以下内容:
_Games Games Events
ID GameType ID GameType ID Name
-- --------- -- --------- -- ----------
1 GameName1 1 GameName1 5 Dispersal
2 GameName2 1 GameName2 6 Withdrawal
3 GameName3 1 GameName3 7 GamePlayed
4 GameName4 1 GameName4 8 MissingGame
5 Dispersal
6 Withdrawal
Run Code Online (Sandbox Code Playgroud)
新表将“真实”和“伪”游戏类型拆分为自己的表。
显示迁移过程要求的示例数据是:
_Winnings
ID WinDate Amount GameType (Notes)
--- ---------- ------ -------- -------------------------------
123 2016-04-20 1234 1 A. Ideal match to "game played" record
167 2017-08-20 1000 1 B. "Missing" game
189 2018-12-20 990 1 C. Matches two games
199 2019-02-01 -2000 6 D. A non-game event (withdrawal)
EventHistory
ID EventType GameType EventDate (Notes)
--- --------- -------- --------- -------------------------------
111 7 (Game) 1 2016-04-20 Perfect match for (A)
222 7 (Game) 1 2017-08-15 \ No entry matches (B)
223 7 (Game) 1 2017-08-25 /
333 7 (Game) 1 2018-12-20 \ Two matches for (C)
334 7 (Game) 1 2018-12-20 /
Run Code Online (Sandbox Code Playgroud)
情况 (A) 是“正常”情况:已经进行了一场比赛,并且取得了胜利。我希望新Winnings
条目直接引用匹配的事件记录。
情况 (B) 将表明数据中存在一些错误(可能是错误输入的获胜日期,我想稍后通过在EventHistory
.
案例(C)有效,代表同一天重复入场。将任一记录EventHistory
与新记录相匹配是Winnings
可以接受的。
案例 (D) 是一个“伪”游戏:奖金要么被提取,要么被用来购买额外的线。无论 中是否存在匹配的日期条目EventHistory
,都将创建新的事件记录。
我在查找匹配项的第一次尝试使用日期上的左连接(左连接,因为不能保证日期匹配),但没有考虑 (C) 之类的情况:多个匹配条目EventHistory
给上升到重复的值_Winnings.ID
,我不能有。
select
W.*,
EH.ID as EID,
G.ID as GID
from _Winnings as W
left join EventHistory as EH on W.WinDate = EH.EventDate
left join Games as G on W.GameType = G.ID
Run Code Online (Sandbox Code Playgroud)
因此,我将其更改为使用相关子查询,以确保只使用一条记录EventHistory
(哪条记录并不重要)。在我的第一次尝试中,我错误地留下了对主选择别名 ( EH.EventDate
)的引用:
select
W.*,
EH.ID as EID,
G.ID as GID
from _Winnings as W
left join EventHistory as EH on EH.ID = (
select min(ID) from EventHistory where W.WinDate = EH.EventDate
)
left join Games as G on W.GameType = G.ID
Run Code Online (Sandbox Code Playgroud)
这似乎有效,但非常缓慢。用完整的表名 ( EventHistory.EventDate
)替换别名:
select
W.*,
EH.ID as EID,
G.ID as GID
from _Winnings as W
left join EventHistory as EH on EH.ID = (
select min(ID) from EventHistory where W.WinDate = EventHistory.EventDate
)
left join Games as G on W.GameType = G.ID
Run Code Online (Sandbox Code Playgroud)
大大提高了速度。有 365 条记录_Winnings
,从 494 条记录开始EventHistory
(随着一些新记录的增加增加到 581 条),整体速度(包括执行一些插入)从超过 3 分钟下降到大约 3 秒。
“快速”查询计划:
QUERY PLAN
|--SCAN TABLE _Winnings AS W
|--SEARCH TABLE EventHistory AS EH USING INTEGER PRIMARY KEY (rowid=?)
|--CORRELATED SCALAR SUBQUERY 1
| `--SEARCH TABLE EventHistory USING COVERING INDEX EventHistoryDateIndex (EventDate=?)
`--SEARCH TABLE Games AS G USING INTEGER PRIMARY KEY (rowid=?)
Run Code Online (Sandbox Code Playgroud)
“慢”查询计划
QUERY PLAN
|--SCAN TABLE _Winnings AS W
|--SCAN TABLE EventHistory AS EH USING COVERING INDEX EventHistoryDateIndex
|--CORRELATED SCALAR SUBQUERY 1
| `--SEARCH TABLE EventHistory
`--SEARCH TABLE Games AS G USING INTEGER PRIMARY KEY (rowid=?)
Run Code Online (Sandbox Code Playgroud)
显然,这些是不同的,但我没有能力理解他们在告诉我什么。
我实际上在做的是处理查询返回的每一行,有时在EventHistory
表中创建一个新记录(并且总是在迁移的Winnings
表中创建一行)。大致流程是:
foreach row returned by the query
if EID or GID is empty
-- either there isn't an exact date match (EID="") or
-- the "game-type" is a "pseudo" game (GID=""). In either
-- case, I want to insert a new row in EventHistory.
insert new row in EventHistory table
endif
insert new row in Winnings table
endfor
Run Code Online (Sandbox Code Playgroud)
我最初认为插入到EventHistory
会影响速度,因为当我只对原始查询计时(对结果不做任何事情)时,两个版本之间的速度没有明显差异。
但是,根据CL. 的回答,其中包括“您在表中插入新行对速度没有影响”,我进一步调查,似乎所使用的 SQLite 版本可能是影响速度的最大因素速度差异。
我正在使用Tcl来编写我的更新过程(包括插入)的脚本,这就是我最初看到两个版本的查询之间在速度上的巨大差异的地方。Tcl 有它自己的 SQLite 版本,在我的情况下它有点旧(2014 年 10 月的 3.8.7.1)。
但是,当我第一次只对查询计时时,我使用了新下载的独立 SQLite shell 版本(2019 年 2 月的 3.27.2)。在这个版本中,两个查询的运行速度基本相同(亚秒级)。
当我使用旧版本的 SQLite 在 Tcl 中重复“仅查询”测试时,速度的差异再次显着:根据 Tcl 的time
功能,8 毫秒与 2 分钟。
我的结论是:
这两个值是常量(就子查询而言),因此表的所有行都匹配,或者不匹配。但是查询优化器不够聪明,无法识别这一点,因此它每次都会遍历表的所有行并评估 WHERE 子句。
来自 CL 的回答确实适用于SQLite 3.8.7.1,但不再适用于 SQLite 3.27.2。
(explain query plan
每个查询的输出在两个版本的 SQLite 中都保持不变,但 所显示的 VDBE 步骤explain
在 SQLite 版本之间确实不同)。
不同之处在于相关子查询如何进行搜索。
快速子查询如下所示:
select min(ID)
from EventHistory
where EventHistory.EventDate = ?
-- SEARCH TABLE EventHistory USING COVERING INDEX EventHistoryDateIndex (EventDate=?)
Run Code Online (Sandbox Code Playgroud)
上有一个索引EventDate
,因此数据库可以在该索引中查找匹配的行,然后记住并仅返回最小值ID
。
慢速子查询如下所示:
select min(ID)
from EventHistory
where ? = ?
-- SEARCH TABLE EventHistory
Run Code Online (Sandbox Code Playgroud)
这两个值是恒定的(就子查询而言),因此表中的所有行要么匹配,要么没有。但查询优化器不够聪明,无法识别这一点,因此它每次都会遍历表的所有行并评估 WHERE 子句。
(有MIN/MAX 优化,但只有在没有 WHERE 子句时才有效。)
您向表中插入新行不会影响速度。但是,如果可能的话,SQLite 会按需计算结果行,因此在读取表时修改表可能会导致结果不一致。您应该首先读取查询的所有结果,或者使用临时表。