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 会按需计算结果行,因此在读取表时修改表可能会导致结果不一致。您应该首先读取查询的所有结果,或者使用临时表。
| 归档时间: |
|
| 查看次数: |
133 次 |
| 最近记录: |