JavaRush /Java 博客 /Random-ZH /“不必要但必须的工作”引起的 SQL 性能问题

“不必要但必须的工作”引起的 SQL 性能问题

已在 Random-ZH 群组中发布
理解本文所需的知识水平:对数据库和 SQL 有一般了解,有一些 DBMS 的实践经验。
SQL导致的性能问题
要编写有效的 SQL 查询,您可以学习的最重要的事情可能就是索引。然而,紧随其后的第二位是许多 SQL 客户端要求数据库执行大量“不必要但必要的工作”的知识。跟着我重复一遍:
不必要但必须的工作
什么是“不必要但必须做的工作”?正如 Captain Obvious 告诉我们的那样,她:

不必要

让我们的客户端应用程序需要以下数据:
SQL导致的性能问题
没什么不寻常的。我们正在使用一个电影数据库(例如Sakila 数据库),并希望向用户显示所有电影的标题和评级。下面的查询可以给出我们需要的结果:
SELECT title, rating
FROM film
然而,我们的应用程序(或我们的 ORM)改为执行此查询:
SELECT *
FROM film
我们得到什么结果?猜测。我们收到很多无用的信息:
SQL导致的性能问题
在右侧,您甚至可以看到正在加载一些复杂的 JSON:
  • 从磁盘
  • 缓存
  • 电汇
  • 为了纪念客户
  • 最后扔掉[不必要]
是的,我们丢弃了大部分信息。事实证明,为提取此信息而采取的所有行动都是完全无用的。这是真的吗?是真的吗。

强制的

现在 - 最糟糕的部分。尽管优化器现在可以做很多事情,但这些操作对于数据库来说是强制性的。数据库无法知道客户端应用程序不需要 95% 的数据。这只是最简单的例子。想象一下连接多个表...那么,你会说,数据库速度很快吗?让我来启发您一些您可能没有想到的事情。当然,单个请求的执行时间并不会真正影响任何事情。好吧,它跑得慢了一倍半,但我们会挺过去的,对吧?为了方便?有时这是真的。但如果你总是为了方便而牺牲性能,这些小事情就会开始累积起来。我们将不再谈论性能(单个请求的执行速度),而是谈论吞吐量(系统响应时间),然后就会出现严重的问题,而这些问题并不那么容易解决。那就是你失去可扩展性的时候。让我们看一下执行计划,在本例中是 Oracle DBMS:
--------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes |
--------------------------------------------------
|   0 | SELECT STATEMENT  |      |  1000 |   166K|
|   1 |  TABLE ACCESS FULL| FILM |  1000 |   166K|
--------------------------------------------------
和....相比:
--------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes |
--------------------------------------------------
|   0 | SELECT STATEMENT  |      |  1000 | 20000 |
|   1 |  TABLE ACCESS FULL| FILM |  1000 | 20000 |
--------------------------------------------------
运行 SELECT * 查询而不是 SELECT 标题,评级在数据库中使用的内存多出 8 倍。没什么意外的吧?我们知道这会发生。但对于我们的许多请求,我们仍然同意这一点,但我们根本不需要所有这些数据。我们为数据库创建了不必要但强制性的工作,这些工作不断堆积。我们使用的内存是所需内存的 8 倍(当然,倍数会改变)。同时,在所有其他阶段(磁盘 I/O、网络上的数据传输、客户端的内存消耗)问题都是完全相同的,但我将跳过它们并查看......

使用索引

如今大多数数据库已经认识到覆盖索引的概念。覆盖索引本身并不是一种特殊类型的索引。但它可能会成为特定查询的“特殊索引”,无论是“偶然”还是因为它本来就是这样的。考虑以下查询:
SELECT *
FROM actor
WHERE last_name LIKE 'A%'
就其实施而言,没有什么意外的。这是一个简单的请求。按索引查看范围,访问表 - 就完成了:
-------------------------------------------------------------------
| Id  | Operation                   | Name                | Rows  |
-------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                     |     8 |
|   1 |  TABLE ACCESS BY INDEX ROWID| ACTOR               |     8 |
|*  2 |   INDEX RANGE SCAN          | IDX_ACTOR_LAST_NAME |     8 |
-------------------------------------------------------------------
好计划,不是吗?好吧,如果我们真的需要这个,那就不需要:
SQL导致的性能问题
显然,我们正在浪费内存等。让我​​们将此查询视为替代方案:
SELECT first_name, last_name
FROM actor
WHERE last_name LIKE 'A%'
他的计划是这样的:
----------------------------------------------------
| Id  | Operation        | Name            | Rows  |
----------------------------------------------------
|   0 | SELECT STATEMENT |                 |     8 |
|*  1 |  INDEX RANGE SCAN| IDX_ACTOR_NAMES |     8 |
----------------------------------------------------
我们能够完全消除对表的访问,这要归功于满足我们查询的所有需求的索引的存在……覆盖索引。是不是重要?如何!这种方法允许您将某些查询速度加快一个数量级(或者当索引在某些更改后不再覆盖时将其速度减慢一个数量级)。覆盖索引并不总是可以使用。您必须为索引付费,并且不应该添加太多索引。但在这种情况下,一切都是显而易见的。我们来评估一下性能:
SET SERVEROUTPUT ON
DECLARE
  v_ts TIMESTAMP;
  v_repeat CONSTANT NUMBER := 100000;
BEGIN
  v_ts := SYSTIMESTAMP;

  FOR i IN 1..v_repeat LOOP
    FOR rec IN (
      -- Наихудший вариант requestа: перерасход памяти ПЛЮС доступ к таблице
      SELECT *
      FROM actor
      WHERE last_name LIKE 'A%'
    ) LOOP
      NULL;
    END LOOP;
  END LOOP;

  dbms_output.put_line('Оператор 1 : ' || (SYSTIMESTAMP - v_ts));
  v_ts := SYSTIMESTAMP;

  FOR i IN 1..v_repeat LOOP
    FOR rec IN (
      -- Улучшенный request, но все равно с доступом к таблице
      SELECT /*+INDEX(actor(last_name))*/
        first_name, last_name
      FROM actor
      WHERE last_name LIKE 'A%'
    ) LOOP
      NULL;
    END LOOP;
  END LOOP;

  dbms_output.put_line('Оператор 2 : ' || (SYSTIMESTAMP - v_ts));
  v_ts := SYSTIMESTAMP;

  FOR i IN 1..v_repeat LOOP
    FOR rec IN (
      -- Оптимальный request: покрывающий индекс
      SELECT /*+INDEX(actor(last_name, first_name))*/
        first_name, last_name
      FROM actor
      WHERE last_name LIKE 'A%'
    ) LOOP
      NULL;
    END LOOP;
  END LOOP;

  dbms_output.put_line('Оператор 3 : ' || (SYSTIMESTAMP - v_ts));
END;
/

结果我们得到:


接线员 1:+000000000 00:00:02.479000000

接线员 2:+000000000 00:00:02.261000000

接线员 3:+000000000 00:00:01.857000000

请注意,actor 表只有 4 列,因此语句 1 和 2 之间的性能差异并不大,但仍然很显着。我还要指出,我使用 Oracle 优化器提示让优化器为查询选择一个或另一个特定索引。操作员 3 是我们比赛中无可争议的获胜者。它的性能要好得多,而且我们正在讨论一个极其简单的查询。同样,当我们编写 SELECT * 时,我们为数据库创建了不必要但强制性的工作,而数据库无法优化。她不会选择覆盖索引,因为它的开销比她选择的 LAST_NAME 索引稍高,并且除其他外,她仍然必须访问表以检索无用的 LAST_UPDATE 列。但我们分析 SELECT * 越深入,事情就会变得越糟糕。让我们来谈谈...

SQL 转换

优化器表现如此之好,是因为它们转换了 SQL 查询(我最近在苏黎世 Voxxed Days 上的演讲中谈到了它的工作原理)。例如,有一个极其强大的“异常JOIN”转换。考虑以下我们必须创建的辅助视图,以避免每次都手动连接所有这些表:
CREATE VIEW v_customer AS
SELECT
  c.first_name, c.last_name,
  a.address, ci.city, co.country
FROM customer c
JOIN address a USING (address_id)
JOIN city ci USING (city_id)
JOIN country co USING (country_id)
该视图只是在 CUSTOMER 客户表和各个表之间执行所有“...对一”连接以获取其地址的一部分。谢谢,正常化。想象一下,在对这个视图进行了一些操作之后,我们已经习惯了它并忘记了它底层的表。现在我们执行以下查询:
SELECT *
FROM v_customer
结果,我们得到了一个非常令人印象深刻的计划:
----------------------------------------------------------------
| Id  | Operation            | Name     | Rows  | Bytes | Cost |
----------------------------------------------------------------
|   0 | SELECT STATEMENT     |          |   599 | 47920 |   14 |
|*  1 |  HASH JOIN           |          |   599 | 47920 |   14 |
|   2 |   TABLE ACCESS FULL  | COUNTRY  |   109 |  1526 |    2 |
|*  3 |   HASH JOIN          |          |   599 | 39534 |   11 |
|   4 |    TABLE ACCESS FULL | CITY     |   600 | 10800 |    3 |
|*  5 |    HASH JOIN         |          |   599 | 28752 |    8 |
|   6 |     TABLE ACCESS FULL| CUSTOMER |   599 | 11381 |    4 |
|   7 |     TABLE ACCESS FULL| ADDRESS  |   603 | 17487 |    3 |
----------------------------------------------------------------
嗯,当然。数据库正在执行所有这些连接和全表扫描,因为这就是我们告诉它要做的事情 - 获取所有这些数据。现在,再次想象我们真正需要的是:
SQL导致的性能问题
什么,说真的,对吧?现在你开始明白我在说什么了。但想象一下我们从过去的错误中学到了一些东西,并执行这个更优化的查询:
SELECT first_name, last_name
FROM v_customer
现在让我们检查一下发生了什么!
------------------------------------------------------------------
| Id  | Operation          | Name        | Rows  | Bytes | Cost  |
------------------------------------------------------------------
|   0 | SELECT STATEMENT   |             |   599 | 16173 |     4 |
|   1 |  NESTED LOOPS      |             |   599 | 16173 |     4 |
|   2 |   TABLE ACCESS FULL| CUSTOMER    |   599 | 11381 |     4 |
|*  3 |   INDEX UNIQUE SCAN| SYS_C007120 |     1 |     8 |     0 |
------------------------------------------------------------------
在执行方面发生了巨大的变化,变得更好。连接已被消除,因为优化器现在可以看到它们是无用的,并且如果它可以看到这一点(并且您没有通过选择 * 来强制执行该工作),那么它根本无法完成所有这些工作。为何在本案中会出现这样的情况呢?主键 ADDRESS.ADDRESS_ID 的外键 CUSTOMER.ADDRESS_ID 保证后者只有一个值,这意味着 JOIN 操作将是“...一对一”联接,不会增加或减少行数。由于我们根本不选择或请求任何行,因此加载它们根本没有意义。删除 JOIN 可能根本不会影响查询的结果。数据库一直在这样做。您可以在几乎任何数据库上运行以下查询:
-- Oracle
SELECT CASE WHEN EXISTS (
  SELECT 1 / 0 FROM dual
) THEN 1 ELSE 0 END
FROM dual

-- Более адекватные диалекты SQL, например, PostgreSQL
SELECT EXISTS (SELECT 1 / 0)
在这种情况下,您可能期望抛出算术异常,就像执行以下查询时一样:
SELECT 1 / 0 FROM dual

发生:


ORA-01476: 除数等于零

但这并没有发生。优化器(甚至解析器)可以确保 EXISTS 谓词(SELECT ..)中的选择列表元素不会更改查询结果,因此无需执行它。像这样!

同时...

ORM 最烦人的问题之一是它们很容易编写 SELECT * 查询。事实上,例如在HQL/JPQL中它们一般都是默认使用的。我们可以完全省略 SELECT 子句,因为我们将检索整个实体,对吧?例如:
FROM v_customer
例如,Vlad Mihalcea 是一位使用 Hibernate 进行开发的专家和倡导者,他建议当您确定不想在结账后保存任何更改时,几乎总是使用 [qualified] 查询。ORM 极大地方便了对象图持久化问题的解决。注:坚持。实际修改对象图和保存更改的任务是密不可分的。但如果你不打算这样做,那为什么还要费力提取精华呢?为什么不写一个[精炼]请求?让我们明确一点:从性能的角度来看,专门针对您的特定用例编写查询显然比任何其他选项都要好。您可能不在乎,因为您的数据集很小而且并不重要。伟大的。但是,当您最终需要可扩展性时,重新设计应用程序以使用查询而不是实体图的命令式遍历将非常具有挑战性。没有它你也会有事可做。

计算行数以查明是否存在某些内容

最严重的资源浪费之一是运行 COUNT(*) 查询只是为了查看数据库中是否有某些内容。例如,我们需要查明给定用户是否有订单。我们执行请求:
SELECT count(*)
FROM orders
WHERE user_id = :user_id
初级。如果 COUNT = 0,则没有订单。否则,是的。性能不会那么差,因为我们可能在 ORDERS.USER_ID 列上有一个索引。但是您认为上述查询的性能与以下选项相比如何:
-- Oracle
SELECT CASE WHEN EXISTS (
  SELECT *
  FROM orders
  WHERE user_id = :user_id
) THEN 1 ELSE 0 END
FROM dual

-- Более адекватные диалекты SQL, например, PostgreSQL
SELECT EXISTS (
  SELECT *
  FROM orders
  WHERE user_id = :user_id
)
不需要火箭科学家就能发现,真正的存在谓词一旦找到第一个字符串就会停止搜索其他字符串。因此,如果结果是“无订单”,那么速度将是可比的。但如果结果是“是的,有订单”,那么在不需要清点具体数量的情况下,会更快收到答复。毕竟,我们对确切的数字不感兴趣。然而,我们告诉数据库计算它(不必要的工作),而数据库不知道我们忽略了所有大于 1 的结果(必需的工作)。当然,如果我们在 JPA 支持的集合上调用 list.size() 来获得相同的结果,情况会更糟。 我之前已经在我的博客上写过这一点,并对这两个选项进行了比较测试......

结论

这篇文章陈述了显而易见的事情。不要强迫数据库做不必要但必需的工作。这是不必要的,因为根据要求,您知道不需要完成某些特定的工作。但是,您告诉数据库执行此操作。这是必需的,因为数据库无法确保这项工作是不必要的。该信息仅对客户端可用,对服务器不可用。所以数据库必须执行它。这篇文章重点讨论了 SELECT *,主要是因为它是一个非常方便查看的对象。但这不仅适用于数据库。这适用于所有分布式算法,其中客户端告诉服务器做不必要但必需的工作。在您的平均 AngularJS 应用程序中有多少个N+1任务,其中 UI 循环访问服务 A 的结果,多次调用服务 B,而不是将对 B 的所有调用打包到单个调用中?这是一个非常普遍的现象。解决方案总是相同的。您向执行命令的实体提供的信息越多,它(理论上)执行这些命令的速度就越快。编写最佳查询。总是。您的整个系统都会为此感谢您。 来源文章
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION