从子查询到连接:提升数据库查询性能的 7 种方法
一般来说,连接(Join) 的性能通常优于子查询(Subquery),尤其是在涉及多表的复杂查询中。连接允许数据库优化器通过同时处理多个表的数据来创建更高效的执行计划。然而,性能差异可能因以下因素而有所不同:
- • 查询复杂度 
- • 数据量及其分布 
- • 索引 
- • 数据库引擎优化 
为了获得最佳性能,建议遵循以下最佳实践:
- 1. 检查查询执行计划,了解数据库如何处理查询。 
- 2. 使用具有代表性的数据量测试连接和子查询版本的查询。 
- 3. 使用适当的索引来支持查询。 
- 4. 在考虑性能的同时,兼顾查询的可读性和可维护性。 
以下是几种可以将子查询重写为连接的情况,并附上了表创建、数据集以及每个部分的查询链接。
1. 将 IN() 子查询替换为连接
 
 使用 IN 的查询:
SELECT * 
FROM SalesOrderItems 
WHERE ProductID IN (
    SELECT ID 
    FROM Products 
    WHERE Quantity < 20
);使用连接的查询:
SELECT s.* 
FROM SalesOrderItems s
JOIN Products p ON s.ProductID = p.ID
WHERE p.Quantity < 20;
区别:连接直接匹配 SalesOrderItems 和 Products 表中的行,避免了在子查询中创建 ID 列表的中间步骤。这对于大数据集尤其高效。
2. 替换关联子查询
使用关联子查询的查询:
SELECT employee_id, first_name 
FROM employees e
WHERE salary > (
    SELECT AVG(salary) 
    FROM employees e2 
    WHERE e2.department_id = e.department_id
);使用连接和聚合的查询:
WITH AvgSalaries AS (
    SELECT department_id, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
)
SELECT e.employee_id, e.first_name 
FROM employees e
JOIN AvgSalaries a ON e.department_id = a.department_id
WHERE e.salary > a.avg_salary;
区别:通过预先计算每个部门的平均工资,连接避免了为外部查询的每一行执行子查询。
3. 替换 EXISTS 子查询
 
 使用 EXISTS 的查询:
SELECT employee_id, first_name 
FROM employees e
WHERE EXISTS (
    SELECT 1 
    FROM departments d 
    WHERE d.department_id = e.department_id AND d.location = 'New York'
);使用连接的查询:
SELECT DISTINCT e.employee_id, e.first_name 
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE d.location = 'New York';
区别:连接消除了为 employees 表中的每一行评估子查询的需要,同时保持相同的结果。
4. 替换嵌套子查询
使用嵌套子查询的查询:
SELECT employee_id, first_name 
FROM employees 
WHERE salary > (
    SELECT AVG(salary) 
    FROM employees 
    WHERE department_id = (
        SELECT department_id 
        FROM departments 
        WHERE department_name = 'Sales'
    )
);使用连接的查询:
WITH SalesDept AS (
    SELECT department_id 
    FROM departments 
    WHERE department_name = 'Sales'
),
AvgSalary AS (
    SELECT AVG(salary) AS avg_salary 
    FROM employees 
    WHERE department_id IN (SELECT department_id FROM SalesDept)
)
SELECT employee_id, first_name 
FROM employees 
WHERE salary > (SELECT avg_salary FROM AvgSalary);
区别:将嵌套子查询扁平化为 CTE 或连接,简化了执行计划并减少了冗余计算。
5. 替换 SELECT 子句中的子查询
 
 使用子查询的查询:
SELECT c.CategoryName, 
       (SELECT MAX(UnitPrice) FROM Products p 
WHERE p.CategoryId = c.CategoryId) AS MaxPrice
FROM Categories c;使用连接的查询:
WITH MaxPrices AS (
    SELECT CategoryId, MAX(UnitPrice) AS MaxPrice
    FROM Products
    GROUP BY CategoryId
)
SELECT c.CategoryName, m.MaxPrice
FROM Categories c
LEFT JOIN MaxPrices m ON c.CategoryId = m.CategoryId;
区别:预先计算每个类别的最高价格,避免了为 Categories 表中的每一行重复计算。
6. 替换 NOT IN 子查询
 
 使用 NOT IN 的查询:
SELECT P.*
FROM Products P
WHERE P.CategoryId NOT IN (SELECT CategoryId FROM Categories);使用左连接的查询:
SELECT t1.*
FROM Products t1
LEFT JOIN Categories t2 ON t1.CategoryId = t2.CategoryId
WHERE t2.CategoryId IS NULL;区别:左连接直接识别不匹配的行,而无需评估整个子查询结果集,并且可以处理 NULL 值,而 NOT IN 则不能。
7. 替换标量子查询
使用标量子查询的查询:
SELECT o.order_id, o.total_amount,
       (SELECT CompanyName FROM Customers c 
WHERE c.customer_id = o.customer_id) AS CompanyName
FROM Orders o;使用连接的查询:
SELECT o.order_id, o.total_amount, c.CompanyName
FROM Orders o
JOIN Customers c ON o.customer_id = c.customer_id;
区别:直接连接避免了为 Orders 表中的每一行执行子查询,这对于大数据集来说可能非常耗时。
总结
连接更好地利用索引,并通过一步处理数据来减少冗余计算,而不是重复评估子查询。
