为什么使用相关子查询的不正确 JOIN 会慢得多

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)

三个表_GamesGamesEvents持有游戏/事件的“类型”,基本上具有以下内容:

_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 版本之间确实不同)。

CL.*_*CL. 2

不同之处在于相关子查询如何进行搜索。

快速子查询如下所示:

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