为什么 MySQL 在 JOIN 加 ORDER 时不使用主键?

Bog*_*scu 5 mysql indexing sql-order-by left-join

这是给你的一个简洁的(显然是 MySQL):

# 设置
如果存在则删除数据库index_test_gutza;
创建数据库index_test_gutza;
使用index_test_gutza;

创建表 customer_order (
    id 中型无符号非空自动递增,
    发票 MEDIUMINT UNSIGNED NOT NULL DEFAULT 0,
    主键(id)
);
插入客户订单
    (身份证、发票)
    价值观
    (1, 1),
    (2, 2),
    (3, 3),
    (4, 4),
    (5, 5);

创建表 customer_invoice (
    id 中型无符号非空自动递增,
    发票号 MEDIUMINT UNSIGNED DEFAULT NULL,
    发票_pdf LONGBLOB,
    主键(id)
);
插入客户发票
    (id,发票号)
    价值观
    (1, 1),
    (2, 2),
    (3, 3),
    (4, 4),
    (5, 5);

# 好的,这是牛肉
解释
    选择 co.id
    来自 customer_order AS co;

解释
    选择 co.id
    来自客户_订单 AS co
    按 co.id 订购;

解释
    选择 co.id、ci.invoice_no
    来自客户_订单 AS co
    LEFT JOIN customer_invoice AS ci ON ci.id=co.invoice;

解释
    选择 co.id、ci.invoice_no
    来自客户_订单 AS co
    LEFT JOIN customer_invoice AS ci ON ci.id=co.invoice
    按 co.id 订购;

底部有四个 EXPLAIN 语句。前两个结果正是您所期望的:

+----+------------+--------+--------+---------------- -+---------+---------+------+------+------------+
| 编号 | 选择类型 | 表| 类型 | 可能的键 | 关键| key_len | 参考| 行 | 额外 |
+----+------------+--------+--------+---------------- -+---------+---------+------+------+------------+
| 1 | 简单| 合作| 索引 | 空| 小学 | 3 | 空| 5 | 使用索引|
+----+------------+--------+--------+---------------- -+---------+---------+------+------+------------+

第三个已经很有趣了——注意 customer_order 中的主键如何不再被使用:

+----+-------------+--------+--------+---------------- --+---------+---------+---------------------------- --+------+-------------+
| 编号 | 选择类型 | 表| 类型 | 可能的键 | 关键| key_len | 参考| 行 | 额外 |
+----+-------------+--------+--------+---------------- --+---------+---------+---------------------------- --+------+-------------+
| 1 | 简单| 合作| 全部 | 空| 空| 空| 空| 5 | |
| 1 | 简单| 词 | eq_ref | 小学 | 小学 | 3 | index_test_gutza.co.invoice | 1 | 使用索引 |
+----+-------------+--------+--------+---------------- --+---------+---------+---------------------------- --+------+-------------+

然而,第四个是令人惊奇的——简单地在主键上添加 ORDER BY就会导致对 customer_order 进行文件排序(这是预料之中的,因为上面已经令人困惑了):

+----+-------------+--------+--------+---------------- --+---------+---------+---------------------------- --+------+----------------+
| 编号 | 选择类型 | 表| 类型 | 可能的键 | 关键| key_len | 参考| 行 | 额外 |
+----+-------------+--------+--------+---------------- --+---------+---------+---------------------------- --+------+----------------+
| 1 | 简单| 合作| 全部 | 空| 空| 空| 空| 5 | 使用文件排序|
| 1 | 简单| 词 | eq_ref | 小学 | 小学 | 3 | index_test_gutza.co.invoice | 1 | 使用索引 |
+----+-------------+--------+--------+---------------- --+---------+---------+---------------------------- --+------+----------------+

文件排序!而且,除了 customer_order 表中用于排序的主键和 customer_invoice 表中用于 JOIN 的主键之外,我从未使用过任何其他内容。那么,以一切美好和正确的名义,为什么它突然切换到文件排序呢?!更重要的是,我该如何避免这种情况?作为记录,我很乐意接受一个记录在案的答案,解释为什么这是无法避免的(如果是这样的话。)

正如您现在可能怀疑的那样,这实际上发生在生产中,尽管表并不大(只有数百条记录),但当我运行时,发票表(包含 PDF 文件)上的文件排序正在杀死服务器类似于上面的查询(我需要这些查询才能知道哪些订单已开具发票,哪些订单没有)。

在你问之前,我设计了数据库,并且我认为我可以安全地将 PDF 文件存储在该表中,因为我从来不需要对其进行任何搜索查询 - 我手边总是有它的主键!

更新(评论概要)

以下是下面评论中建议内容的概要,因此您不必阅读所有内容:

  • *您应该在 customer_order.invoice 上添加一个密钥* - 我实际上在生产中尝试过,它没有什么区别(因为它不应该)
  • 你应该使用USE INDEX- 尝试过,没有用。我也尝试过FORCE INDEX——也没有结果(没有任何改变)
  • 您过度简化了用例,我们需要实际的生产查询——我可能在第一次迭代中将其剥离得太多,所以我更新了它(我刚刚, ci.invoice_noSELECT最后几个查询中添加了它)。作为记录,如果有人真的很好奇,这里是生产查询,完全按照原样(这会检索订单的最后一页):
选择
    编码器.id,
    corder.public_id,
    CONCAT(买家.fname," ",买家.lname) AS 买家名称,
    线.状态,
    编码支付,
    corder.保留AS R,
    corder.tracking_id!="" 为 A,
    corder. payment_received 作为 pay_date,
    发票.invoice_no AS inv,
    发票.receipt_no AS 记录,
    发票.public AS pub_inv,
    proforma.proforma_no AS 教授,
    形式.public AS pub_pf,
    评级,
    corder. rating_comments!="" AS got_comment
从
    科德尔
LEFT JOIN 用户作为买家 ON Buyer.id=corder.buyer
LEFT JOIN 发票作为发票 ONinvoice.id=corder.invoice
LEFT JOIN 发票作为形式 ON proforma.id=corder.proforma
订购依据
    编号降序
限制 400、20;

上面的查询(同样,这正是我在生产中运行的查询)大约需要 14 秒才能运行。这是在生产中执行的简化查询,如上面的用例所示:

选择
    编码器.id,
    发票.invoice_no
从
    科德尔
LEFT JOIN 发票 ONinvoice.id=corder.invoice
订购依据
    corder.id DESC
限制 400、20;

这个运行需要 13 秒。请注意,只要我们谈论结果的最后一页(我们就是),LIMIT 就没有任何区别。也就是说,当涉及文件排序时,检索最后 12 个结果或所有 412 个结果之间绝对没有显着差异。

结论

ypercube 的答案不仅是正确的,而且不幸的是它似乎是唯一合法的答案。我尝试进一步将条件与字段分开,因为SELECT * FROM corder如果 corder 本身包含 LONGBLOB,则子查询最终可能会涉及大量数据(并且在子查询中复制主查询中的字段是不优雅的),但不幸的是,它似乎并不工作:

选择
    编码器.id,
    corder.public_id,
    CONCAT(买家.fname," ",买家.lname) AS 买家名称,
    线.状态,
    编码支付,
    corder.保留AS R,
    corder.tracking_id != "" AS A,
    corder. payment_received AS pay_date,
    发票.invoice_no AS inv,
    发票.receipt_no AS 记录,
    发票.public AS pub_inv,
    proforma.proforma_no AS 教授,
    形式.public AS pub_pf,
    评级,
    corder. rating_comments!="" AS got_comment
从
    科德尔
LEFT JOIN 用户作为买家 ON Buyer.id = corder.buyer
LEFT JOIN 发票 AS 发票 ONinvoice.id = corder.invoice
LEFT JOIN 发票 AS 形式 ON proforma.id = corder.proforma
WHERE corder.id IN (
    选择 ID
    来自绳索
    按 ID 描述排序
    限制 400,20
)
订购依据
    corder.id DESC;

此操作失败,并显示以下错误消息:

ERROR 1235 (42000):此版本的 MySQL 尚不支持“LIMIT & IN/ALL/ANY/SOME 子查询”

我使用的是 MySQL 5.1.61,它是 5.1 系列中最新的版本(显然 5.5.x 也不支持它)。

ype*_*eᵀᴹ 5

你能尝试这个版本吗(它基本上首先获取表的 420 行corder,保留其中的 20 行,然后执行 3 个外连接):

SELECT
    corder.id,
    corder.public_id,
    CONCAT(buyer.fname," ",buyer.lname) AS buyer_name,
    corder.status,
    corder.payment,
    corder.reserved AS R,
    corder.tracking_id != "" AS A,
    corder.payment_received AS pay_date,
    invoice.invoice_no AS inv,
    invoice.receipt_no AS rec,
    invoice.public AS pub_inv,
    proforma.proforma_no AS prof,
    proforma.public AS pub_pf,
    corder.rating,
    corder.rating_comments!="" AS got_comment
FROM
    ( SELECT * 
      FROM corder
      ORDER BY
        id DESC 
      LIMIT 400, 20
    )
    AS corder
LEFT JOIN user as buyer ON buyer.id = corder.buyer
LEFT JOIN invoice AS invoice ON invoice.id = corder.invoice
LEFT JOIN invoice AS proforma ON proforma.id = corder.proforma
ORDER BY
    corder.id DESC ;
Run Code Online (Sandbox Code Playgroud)