我应该在加入之前使用子查询来限制表吗?

Dan*_*and 7 performance optimization subquery query-performance

在连接后跟 where 子句的情况下,使用子查询来限制结果,然后进行连接会更好吗?例子:

SELECT * 
FROM Customers 
    NATURAL JOIN Orders 
WHERE shipped=1
Run Code Online (Sandbox Code Playgroud)

在这种情况下,它接缝 DBMS 将整个客户表与整个订单表连接,然后根据 where 子句过滤结果。使用子查询的等效查询是:

SELECT * 
FROM Customers 
    NATURAL JOIN (SELECT * 
                     FROM Orders 
                     WHERE shipped=1) AS O
Run Code Online (Sandbox Code Playgroud)

在这里,可能有一个较小的 Orders 表要加入。同样,如果有限制客户和订单的 where 子句:

SELECT * 
FROM Customers 
    NATURAL JOIN Orders 
WHERE country='US' AND shipped=1
(assuming country attribute belongs to Customers table)
Run Code Online (Sandbox Code Playgroud)

等效的子查询查询:

SELECT * 
FROM (SELECT *
            FROM Customers 
            WHERE country='US') AS C 
    NATURAL JOIN (SELECT * 
                                 FROM Orders 
                                 WHERE shipped=1) AS O
Run Code Online (Sandbox Code Playgroud)

joa*_*olo 7

您的问题的答案取决于您使用的特定数据库和版本。在任何情况下,大多数当前数据库都会优化查询,并最终在所有情况下都具有相同的执行计划。

我会采用最简单的语法,清楚地说明您的目的。它可能是数据库能够进行最佳优化的一个。而且,特别是,您自己或以后修改任何应用程序的任何人都更容易理解它。如果您发现某些查询似乎表现不佳,那么您应该检查执行计划、替代方案,并确定它是否真的可以改进。

您可以使用类似于的语句检查大多数数据库EXPLAIN SELECT * FROM Customers NATURAL JOIN Orders WHERE shipped=1并检查执行计划是什么。

这是使用 PostgreSQL 时的做法:

(模型)表的创建和填充:

 CREATE TABLE Customers
 (
     customer_id integer primary key,
     customer_name text not null
 ) ;

 CREATE TABLE Orders
 (
     order_id integer primary key,
     customer_id integer NOR NULL REFERENCES Customers(customer_id),
     whatever text,
     shipped boolean
 ) ;

 -- We invent 1_000 customers
 INSERT INTO 
     Customers (customer_id, customer_name)
 SELECT
     i, 'Name ' || i
 FROM
     generate_series (1, 1000) as s(i) ;

 -- and 25_000 orders
 INSERT INTO
     Orders (order_id, customer_id, whatever, shipped)
 SELECT
     i AS order_id,  
     1 + 999*random() AS customer_id,
     'a text' AS whatever,
     (random() < 0.85) AS shipped
 FROM
     generate_series(1, 25000) AS s(i) ;
Run Code Online (Sandbox Code Playgroud)

确保数据库具有正确的统计信息:

ANALYZE Orders;
ANALYZE Customers;
Run Code Online (Sandbox Code Playgroud)

检查最简单查询的执行计划:

EXPLAIN ANALYZE
SELECT * FROM Customers NATURAL JOIN Orders WHERE shipped
Run Code Online (Sandbox Code Playgroud)

| 查询计划 |
 | :------------------------------------------------- -------------------------------------------------- ----------------- |
 | Hash Join (cost=28.50..705.05 rows=21131 width=24) (实际时间=0.946..29.305 rows=21131 loops=1) |
 | 哈希条件:(orders.customer_id = customers.customer_id) |
 | -> Seq Scan on orders (cost=0.00..386.00 rows=21131 width=16) (实际时间=0.008..10.089 rows=21131 loops=1) |
 | 过滤器:发货 |
 | 过滤器删除的行数:3869 |
 | -> Hash (cost=16.00..16.00 rows=1000 width=12) (实际时间=0.906..0.906 rows=1000 loops=1) |
 | 存储桶:1024 批次:1 内存使用:52kB |
 | -> 对客户进行 Seq 扫描(成本=0.00..16.00 行=1000 行=12)(实际时间=0.005..0.445 行=1000 次循环=1)|
 | 规划时间:0.419 ms |
 | 执行时间:34.613 毫秒 |
 

检查第二个版本查询的执行计划:

 EXPLAIN ANALYZE
 SELECT * FROM Customers NATURAL JOIN (SELECT * FROM Orders WHERE shipped) AS O
Run Code Online (Sandbox Code Playgroud)

| 查询计划 |
 | :------------------------------------------------- -------------------------------------------------- ----------------- |
 | Hash Join (cost=28.50..705.05 rows=21131 width=24) (实际时间=0.693..24.537 rows=21131 loops=1) |
 | 哈希条件:(orders.customer_id = customers.customer_id) |
 | -> Seq Scan on orders (cost=0.00..386.00 rows=21131 width=16) (actual time=0.007..8.534 rows=21131 loops=1) |
 | 过滤器:发货 |
 | 过滤器删除的行数:3869 |
 | -> Hash (cost=16.00..16.00 rows=1000 width=12) (实际时间=0.676..0.676 rows=1000 loops=1) |
 | 存储桶:1024 批次:1 内存使用:52kB |
 | -> 对客户进行 Seq 扫描(成本=0.00..16.00 行=1000 行=12)(实际时间=0.003..0.312 行=1000 次循环=1)|
 | 规划时间:0.263 ms |
 | 执行时间:29.083 毫秒 |
 

你可以看到,在这种情况下,PostgreSQL的(9.6.2)使用完全一样的执行计划。这是最常发生的事情。

dbfiddle在这里


一个稍微不同的版本,其中定义了一个索引(并且我们决定shipped订单是少数,而不是多数):

CREATE INDEX idx_shipped_orders ON Orders(shipped, order_id) ; 
Run Code Online (Sandbox Code Playgroud)

... 使用不同的计划,但同样,简单和已发送过滤的第一个查询的执行计划相同:

| 查询计划 |
 | :------------------------------------------------- -------------------------------------------------- ------------------------------------- |
 | Hash Join (cost=28.78..214.66 rows=3822 width=24) (实际时间=0.750..5.985 rows=3822 loops=1) |
 | 哈希条件:(orders.customer_id = customers.customer_id) |
 | -> 使用 shipping_orders_idx 对订单进行索引扫描(成本=0.28..133.61 行=3822 宽度=16)(实际时间=0.008..1.873 行=3822 循环=1)|
 | -> Hash (cost=16.00..16.00 rows=1000 width=12) (实际时间=0.730..0.730 rows=1000 loops=1) |
 | 存储桶:1024 批次:1 内存使用:52kB |
 | -> 对客户进行 Seq 扫描(成本=0.00..16.00 行=1000 行=12)(实际时间=0.008..0.345 行=1000 次循环=1)|
 | 规划时间:0.209 ms |
 | 执行时间:6.863 毫秒 |
 

dbfiddle在这里


注意:执行时间的微小差异可能是由于任何外部因素造成的,例如其他进程是否同时使用数据库。仅当您执行具有统计显着性的查询次数并执行足够的显着性统计测试时,才应考虑它们。

注意 2:我NATURAL JOIN像您一样使用了,以免在分析中引入更多元素。在实践中,我宁愿使用ON t1.col = t2.col(与大多数数据库兼容)或USING (col). 两者的含义保持不变。NATURAL JOIN如果向表中添加列,则可以更改含义。例如,您可能决定添加列last_modified_atlast_modified_by以跟踪你的数据的“年龄” ......和你的自然连接停止工作。简而言之:不要使用它们。NATURAL JOIN不是指使用参与外键约束的列进行连接,正如您所想的那样。它们只是引用两个表中具有相同名称的列。这是有风险的。我有很多表格,其中的列称为created_at 的例如,last_modified_at,使用它们来 JOIN 没有任何意义。