从具有连续ID的表中选择两个(或更多)行的有效,可扩展的方法是什么,特别是如果此表与另一个表连接?
之前在Stack Overflow上已经提出过相关问题,例如:
这些问题的答案表明了自我加入。我在下面描述的工作示例使用了该建议,但它在较大的数据集上执行得非常非常糟糕。我已经没有想法如何改进它,我真的很感激你的意见。
我们假设我正在开发一个数据库,用于跟踪足球/足球比赛中的球控(请理解我无法透露我的实际应用的目的)。我需要一种高效,可扩展的方式,允许我查询从一个玩家到另一个玩家的持球变化(即通过)。例如,我可能会对从任何防守者到任何前锋的所有传球清单感兴趣。
我的模拟数据库由两个表组成,第一个表Players
存储玩家' Name
列中的POS
列中的名称及其位置(GOA,DEF,MID,FOR守门员,后卫,中场,前锋)
第二张牌Possession
跟踪球的占有情况。每当球控制改变,即球被传递给新球员时,就会在该表中添加一行。主键ID
也表示占有变化的时间顺序:连续的ID表示立即拥有球的顺序。
CREATE TABLE Players(
ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
POS VARCHAR(3) NOT NULL,
Name VARCHAR(7) NOT NULL);
CREATE TABLE Possession(
ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
PlayerID INT NOT NULL);
接下来,我们创建一些索引:
CREATE INDEX POS ON Players(POS);
CREATE INDEX Name ON Players(Name);
CREATE INDEX PlayerID ON Possession(PlayerID);
现在,我们使用一些播放器填充Players
表,并将测试条目添加到Possession
表:
INSERT INTO Players (POS, Name) VALUES
('DEF', 'James'), ('DEF', 'John'), ('DEF', 'Michael'),
('DEF', 'David'), ('MID', 'Charles'), ('MID', 'Thomas'),
('MID', 'Paul'), ('FOR', 'Bob'), ('GOAL', 'Kenneth');
INSERT INTO Possession (PlayerID) VALUES
(1), (8), (2), (5), (3), (8), (3), (9), (6), (4), (7), (9);
让我们加入Possession
和Players
表格快速查看我们的数据库:
SELECT Possession.ID, PlayerID, POS, Name
FROM
Possession
INNER JOIN Players ON Possession.PlayerID = Players.ID
ORDER BY Possession.ID;
看起来不错:
+----+----------+-----+---------+
| ID | PlayerID | POS | Name |
+----+----------+-----+---------+
| 1 | 1 | DEF | James |
| 2 | 8 | FOR | Bob |
| 3 | 2 | DEF | John |
| 4 | 5 | MID | Charles |
| 5 | 3 | DEF | Michael |
| 6 | 8 | FOR | Bob |
| 7 | 3 | DEF | Michael |
| 8 | 9 | GOA | Kenneth |
| 9 | 6 | MID | Thomas |
| 10 | 4 | DEF | David |
| 11 | 7 | MID | Paul |
| 12 | 9 | GOA | Kenneth |
+----+----------+-----+---------+
表格可以这样读:首先,DEFender James传给了前锋鲍勃。然后,Bob传给了DEFender John,后者又传给了MIDfield Charles。经过一些传球后,球以GOAlkeeper Kenneth结束。
我需要一个查询,列出从任何防守者到任何前锋的所有传球。正如我们在上表中看到的那样,有两个实例:在开始时,詹姆斯将球传给鲍勃(第1行到第2行),之后迈克尔将球传给鲍勃(第5行)至ID 6)。
为了在SQL中执行此操作,我为Possession
表创建了一个自连接,第二个实例偏移了一行。为了能够访问玩家'名称和位置,我也将两个Possession
表实例加入Players
表。以下查询执行此操作:
SELECT
M1.ID AS "From",
M2.ID AS "To",
P1.Name AS "Sender",
P2.Name AS "Receiver"
FROM
Possession AS M1
INNER JOIN Possession as M2 ON M2.ID = M1.ID + 1
INNER JOIN Players as P1 ON M1.PlayerId = P1.ID AND P1.POS = "DEF" -- see execution plan
INNER JOIN Players as P2 ON M2.PlayerId = P2.ID AND P2.POS = "FOR"
我们得到预期的输出:
+------+----+---------+----------+
| From | To | Sender | Receiver |
+------+----+---------+----------+
| 1 | 2 | James | Bob |
| 5 | 6 | Michael | Bob |
+------+----+---------+----------+
虽然此查询几乎立即在模拟足球数据库中执行,但此查询的执行计划中似乎存在问题。以下是EXPLAIN
的输出:
+------+-------------+-------+------+------------------+----------+---------+------------+------+-------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+-------+------+------------------+----------+---------+------------+------+-------------------------------------------------+
| 1 | SIMPLE | P2 | ref | PRIMARY,POS | POS | 5 | const | 1 | Using index condition |
| 1 | SIMPLE | M2 | ref | PRIMARY,PlayerID | PlayerID | 4 | MOCK.P2.ID | 1 | Using index |
| 1 | SIMPLE | P1 | ALL | PRIMARY,POS | NULL | NULL | NULL | 9 | Using where; Using join buffer (flat, BNL join) |
| 1 | SIMPLE | M1 | ref | PlayerID | PlayerID | 4 | MOCK.P1.ID | 1 | Using where; Using index |
+------+-------------+-------+------+------------------+----------+---------+------------+------+-------------------------------------------------+
我必须承认,我不太擅长解释查询执行计划。但在我看来,第三行表示上面查询中标记的连接的瓶颈:显然,对P1
别名表进行了完整扫描,即使{{1并且主键可用,而POS
部分也非常可疑。我不知道这意味着什么,但我通常不能通过正常连接找到它。
也许由于这个瓶颈,查询无法在我的真实数据库的任何可接受的时间范围内完成。我对模拟join buffer (flat, BNL join)
表的实际等价物有大约60,000行,而Players
等价物有〜1,160,000行。我通过Possession
监控了查询的执行情况。超过600秒后,该过程仍被标记为SHOW PROCESSLIST
,此时我终止了该过程。
此较大数据集的查询计划与小型模拟数据集的查询计划非常相似。第三个连接似乎有问题,没有使用密钥,正在执行全表扫描,以及我不太了解的连接缓冲区部分:
Sending data
我尝试在上面显示的查询中使用+------+-------------+-------+------+---------------+----------+---------+------------------+-------+-------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+-------+------+---------------+----------+---------+------------------+-------+-------------------------------------------------+
| 1 | SIMPLE | P2 | ref | POS | POS | 1 | const | 1748 | Using index condition |
| 1 | SIMPLE | M2 | ref | PlayerId | PlayerId | 2 | REAL.P2.PlayerId | 7 | |
| 1 | SIMPLE | P1 | ALL | POS | NULL | NULL | NULL | 61917 | Using where; Using join buffer (flat, BNL join) |
| 1 | SIMPLE | M1 | ref | PlayerId | PlayerId | 2 | REAL.P1.PlayerId | 7 | Using where |
+------+-------------+-------+------+---------------+----------+---------+-----------------------+-------+-------------------------------------------------+
而不是P1
强制别名表Players AS P1 FORCE INDEX (POS)
的索引。此更改确实会影响执行计划。如果我强制Players AS P1
用作键,则POS
输出中的第三行与第一行非常相似。唯一的区别是行数,仍然非常高(30912)。即使这个修改过的查询在600秒后也没有完成。
我不认为这是配置问题。我已经为MySQL服务器提供了18 GB的RAM,服务器将此内存用于其他查询。对于当前查询,内存消耗不超过2 GB RAM。
感谢您保留这个有点冗长的解释到目前为止!
让我们回到最初的问题:从具有连续ID的表中选择两个(或更多)行的有效,可扩展的方法是什么,特别是如果此表与另一个表连接?
我当前的查询肯定是做错了,因为即使十分钟后它也没有完成。我可以在当前查询中更改某些内容,以使其对我更大的实际数据集有用吗?如果没有:我可以使用另一种更好的解决方案吗?
答案 0 :(得分:0)
我认为问题在于你只在玩家表上有单个字段索引。 MySQL每个连接表只能使用一个索引。
如果玩家表2字段从性能角度来看是关键:
您似乎在这两个字段上都有独立索引,但这会强制MySQL选择是使用索引来连接2个表还是根据where条件进行过滤。
我会在playerid,pos字段(按此顺序)上创建一个多列索引,它可以同时满足join和where。这样MySQL可以使用单个索引来满足连接和where。
我还会使用显式连接而不是以逗号分隔的表格列表和连接条件,以便更好地阅读。
答案 1 :(得分:0)
这是一个总体计划:
SELECT
@n := @n + 1 AS N, -- Now the rows will be numbered 1,2,3,...
...
FROM ( SELECT @n := 0 ) AS init
JOIN tbl
ORDER BY ... -- based on your definition of 'consecutive'
然后,您可以将该查询用作其他地方的子查询。
SELECT ...
FROM ( the above query ) AS x
GROUP BY ceiling(N/2) -- 1&2 will be grouped together; 3&4; etc
你可以将`IF((N%2)= 1,...,...)用于不同的事情,每对中的第一项与第二项。
您向另一个表提到了JOINing
。如果可能,请避免在JOIN
之前执行SELECT
。