在MySQL表

时间:2016-07-20 08:54:54

标签: mysql sql database-design inner-join

简短的问题

从具有连续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);

让我们加入PossessionPlayers表格快速查看我们的数据库:

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的表中选择两个(或更多)行的有效,可扩展的方法是什么,特别是如果此表与另一个表连接?

我当前的查询肯定是做错了,因为即使十分钟后它也没有完成。我可以在当前查询中更改某些内容,以使其对我更大的实际数据集有用吗?如果没有:我可以使用另一种更好的解决方案吗?

2 个答案:

答案 0 :(得分:0)

我认为问题在于你只在玩家表上有单个字段索引。 MySQL每个连接表只能使用一个索引。

如果玩家表2字段从性能角度来看是关键:

  • playerid,因为它在连接中使用;
  • pos,因为你过滤它。

您似乎在这两个字段上都有独立索引,但这会强制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