35

在用 TSQL 或 PLSQL 编写数据库查询时,我们通常可以选择使用游标遍历行来完成任务,或者制作一个一次性完成相同工作的 SQL 语句。

此外,我们可以选择简单地将大量数据拉回我们的应用程序,然后使用 C#、Java 或 PHP 或其他方式逐行处理它。

为什么使用基于集合的查询更好?这种选择背后的理论是什么?什么是基于游标的解决方案及其关系等价物的一个很好的例子?

4

11 回答 11

19

我知道的主要原因是引擎可以通过跨多个线程运行基于集合的操作来优化它们。例如,考虑一个快速排序 - 您可以将要排序的列表分成多个“块”,并在各自的线程中单独排序。SQL 引擎可以在一个基于集合的查询中对大量数据执行类似的操作。

当您执行基于游标的操作时,引擎只能按顺序运行,并且该操作必须是单线程的。

于 2008-08-23T12:08:33.480 回答
16

基于集合的查询(通常)更快,因为:

  1. 他们有更多信息供查询优化器优化
  2. 他们可以从磁盘批量读取
  3. 回滚、事务日志等涉及的日志记录较少。
  4. 占用更少的锁,从而减少开销
  5. 基于集合的逻辑是 RDBMS 的重点,因此它们已经针对它进行了大量优化(通常以牺牲程序性能为代价)

但是,将数据拉出到中间层来处理它可能很有用,因为它消除了数据库服务器的处理开销(这是最难扩展的事情,通常也会做其他事情)。此外,您通常在中间层没有相同的开销(或收益)。诸如事务日志记录、内置锁定和阻塞等 - 有时这些是必要且有用的,有时它们只是浪费资源。

具有过程逻辑与基于集合的示例 (T-SQL) 的简单游标,它将根据电话交换机分配区号:

--Cursor
DECLARE @phoneNumber char(7)
DECLARE c CURSOR LOCAL FAST_FORWARD FOR
   SELECT PhoneNumber FROM Customer WHERE AreaCode IS NULL
OPEN c
FETCH NEXT FROM c INTO @phoneNumber
WHILE @@FETCH_STATUS = 0 BEGIN
   DECLARE @exchange char(3), @areaCode char(3)
   SELECT @exchange = LEFT(@phoneNumber, 3)

   SELECT @areaCode = AreaCode 
   FROM AreaCode_Exchange 
   WHERE Exchange = @exchange

   IF @areaCode IS NOT NULL BEGIN
       UPDATE Customer SET AreaCode = @areaCode
       WHERE CURRENT OF c
   END
   FETCH NEXT FROM c INTO @phoneNumber
END
CLOSE c
DEALLOCATE c
END

--Set
UPDATE Customer SET
    AreaCode = AreaCode_Exchange.AreaCode
FROM Customer
JOIN AreaCode_Exchange ON
    LEFT(Customer.PhoneNumber, 3) = AreaCode_Exchange.Exchange
WHERE
    Customer.AreaCode IS NULL
于 2008-08-23T13:06:58.753 回答
16

除了上面的“让 DBMS 完成工作”(这是一个很好的解决方案)之外,还有其他几个很好的理由将查询留在 DBMS 中:

  • 它(主观上)更容易阅读。稍后查看代码时,您更愿意尝试使用循环和事物来解析复杂的存储过程(或客户端代码),还是更愿意查看简洁的 SQL 语句?
  • 它避免了网络往返。为什么将所有数据推送给客户端,然后再推送更多?如果不需要,为什么要破坏网络?
  • 这很浪费。您的 DBMS 和应用程序服务器将需要缓冲部分/全部数据才能对其进行处理。如果您没有无限的内存,您可能会分页出其他数据;为什么要从内存中剔除可能很重要的东西来缓冲一个几乎没用的结果集?
  • 你为什么不呢?您购买(或正在使用)一个高度可靠、非常快速的 DBMS。你为什么不使用它?
于 2008-08-23T13:42:12.210 回答
9

你想要一些现实生活中的例子。我的公司有一个游标需要 40 多分钟来处理 30,000 条记录(有时我需要更新超过 200,000 条记录)。在没有光标的情况下完成相同的任务需要 45 秒。在另一种情况下,我删除了一个光标并将处理时间从超过 24 小时发送到不到一分钟。一种是使用 values 子句而不是 select 的插入,另一种是使用变量而不是连接的更新。一个好的经验法则是,如果是插入、更新或删除,您应该寻找一种基于集合的方式来执行任务。

游标有它们的用途(或者代码一开始就不是它们的用途),但在查询关系数据库时它们应该非常少见(Oracle 除外,它经过优化以使用它们)。他们可以更快的一个地方是在根据先前记录的值(运行总计)进行计算时。但即使这样也应该进行测试。

使用游标的另一个有限情况是进行一些批处理。如果您试图以基于集合的方式一次做太多事情,它可能会将表锁定给其他用户。如果你有一个非常大的集合,最好将它分解成更小的基于集合的插入、更新或删除,这些插入、更新或删除不会持有太久的锁,然后使用游标运行这些集合。

游标的第三种用途是通过一组输入值运行系统存储的过程。因为这仅限于一个通常较小的集合,并且没有人应该弄乱系统过程,这对于管理员来说是可以接受的事情。我不建议对用户创建的存储过程执行相同的操作来处理大批量并重用代码。最好编写一个性能更好的基于集合的版本,因为在大多数情况下性能应该胜过代码重用。

于 2008-12-01T17:44:05.927 回答
3

我认为真正的答案是,就像编程中的所有方法一样,它取决于哪个更好。一般来说,基于集合的语言会更有效率,因为这就是它的设计初衷。游标有两个优势:

  1. 您正在更新无法接受锁定行的数据库中的大型数据集(可能在生产时间)。基于集合的更新有可能将表锁定几秒钟(或几分钟),而游标(如果写入正确)则不会。游标可以蜿蜒穿过一次更新一个的行,您不必担心会影响其他任何内容。

  2. 使用 SQL 的优点是大部分优化工作在大多数情况下由数据库引擎处理。借助企业级数据库引擎,设计人员竭尽全力确保系统能够高效地处理数据。缺点是 SQL 是一种基于集合的语言。您必须能够定义一组数据才能使用它。虽然这听起来很容易,但在某些情况下并非如此。查询可能非常复杂,以至于引擎中的内部优化器无法有效地创建执行路径,并猜测会发生什么……您的具有 32 个处理器的超级强大的盒子使用单个线程来执行查询,因为它不知道其他的怎么做,所以你在数据库服务器上浪费了处理器时间,通常只有一个而不是多个应用程序服务器(所以回到原因 1,你遇到了需要在数据库服务器上运行的其他东西的资源争用)。使用基于行的语言(C#、PHP、JAVA 等),您可以更好地控制发生的事情。您可以检索数据集并强制它按照您希望的方式执行。(分离数据集以在多个线程上运行等)。大多数时候,它仍然不会像在数据库引擎上运行它那样高效,因为它仍然必须访问引擎来更新行,但是当你必须进行 1000 多次计算来更新行时(假设您有一百万行),数据库服务器可能会开始出现问题。您遇到需要在数据库服务器上运行的其他东西的资源争用)。使用基于行的语言(C#、PHP、JAVA 等),您可以更好地控制发生的事情。您可以检索数据集并强制它按照您希望的方式执行。(分离数据集以在多个线程上运行等)。大多数时候,它仍然不会像在数据库引擎上运行它那样高效,因为它仍然需要访问引擎来更新行,但是当你必须进行 1000 多次计算来更新一行时(假设您有一百万行),数据库服务器可能会开始出现问题。您遇到需要在数据库服务器上运行的其他东西的资源争用)。使用基于行的语言(C#、PHP、JAVA 等),您可以更好地控制发生的事情。您可以检索数据集并强制它按照您希望的方式执行。(分离数据集以在多个线程上运行等)。大多数时候,它仍然不会像在数据库引擎上运行它那样高效,因为它仍然需要访问引擎来更新行,但是当你必须进行 1000 多次计算来更新一行时(假设您有一百万行),数据库服务器可能会开始出现问题。(分离数据集以在多个线程上运行等)。大多数时候,它仍然不会像在数据库引擎上运行它那样高效,因为它仍然需要访问引擎来更新行,但是当你必须进行 1000 多次计算来更新一行时(假设您有一百万行),数据库服务器可能会开始出现问题。(分离数据集以在多个线程上运行等)。大多数时候,它仍然不会像在数据库引擎上运行它那样高效,因为它仍然需要访问引擎来更新行,但是当你必须进行 1000 多次计算来更新一行时(假设您有一百万行),数据库服务器可能会开始出现问题。

于 2008-08-23T13:29:56.983 回答
1

如前所述,数据库针对集合操作进行了优化。从字面上看,工程师们坐下来长时间调试/调整该数据库。你优化它们的机会非常渺茫。如果你有一组数据可以使用,比如批量磁盘读/写、缓存、多线程,你可以玩各种有趣的技巧。此外,某些操作的开销成本很高,但如果您一次对一堆数据执行此操作,则每条数据的成本很低。如果您一次只工作一行,那么很多这些方法和操作都不会发生。

例如,只看数据库的连接方式。通过查看解释计划,您可以看到几种连接方式。最有可能使用游标在一个表中逐行移动,然后从另一个表中选择所需的值。基本上它就像一个嵌套循环,只是没有循环的紧密性(很可能编译成机器语言并进行了超级优化)。SQL Server 本身有很多加入方式。如果对行进行排序,它将使用某种类型的合并算法,如果一张表很小,它可能会将一张表变成哈希查找表,并通过从一张表到查找表执行 O(1) 查找来进行连接。许多 DBMS 有许多连接策略,它们可以让您在游标中从一个表中查找值。

只需看创建哈希查找表的示例。如果您要连接两个长度为 n 的表和长度为 m 的表,其中 m 是较小的表,则构建表可能需要 m 个操作。每次查找都应该是常数时间,因此是 n 次操作。所以基本上哈希连接的效率大约是 m(设置)+ n(查找)。如果您自己进行并假设没有查找/索引,那么对于 n 行中的每一行,您将必须搜索 m 记录(平均而言,它相当于 m/2 次搜索)。所以基本上操作级别从 m + n (一次加入一堆记录)到 m * n / 2 (通过游标进行查找)。操作也是简化的。根据游标类型,获取游标的每一行可能与从第一个表中进行另一个选择相同。

锁也会杀死你。如果您在表上有游标,则您正在锁定行(在 SQL 服务器中,这对于 static 和 forward_only 游标不太严重......但我看到的大多数游标代码只是打开一个游标而不指定任何这些选项)。如果您在一组中执行操作,这些行仍将被锁定,但时间会更短。优化器也可以看到你在做什么,它可能会决定锁定整个表而不是一堆行或页面更有效。但是,如果您逐行进行,优化器将不知道。

另一件事是,我听说在 Oracle 的情况下,它对游标操作进行了超级优化,因此对于基于集合的操作与 Oracle 中的游标相比,它与 SQL Server 中的惩罚相差甚远。我不是甲骨文专家,所以我不能肯定。但不止一位 Oracle 人员告诉我,游标在 Oracle 中效率更高。因此,如果您为 Oracle 牺牲了您的长子,您可能不必担心游标,请咨询您当地的高薪 Oracle DBA :)

于 2008-12-01T18:20:29.380 回答
1

我认为归结为使用数据库是为了使用而设计的。关系数据库服务器经过专门开发和优化,可以最好地响应以集合逻辑表达的问题。

从功能上讲,游标的惩罚因产品而异。一些(大多数?)rdbmss 至少部分构建在 isam 引擎之上。如果问题是适当的,并且单板足够薄,那么实际上使用游标可能同样有效。但就您的 dbms 品牌而言,这是您在尝试之前应该非常熟悉的事情之一。

于 2008-12-01T18:04:36.870 回答
0

倾向于在查询中进行工作的想法是数据库引擎可以通过重新格式化来优化它。这也是为什么您要在查询上运行 EXPLAIN 以查看数据库实际在做什么的原因。(例如,利用索引、表大小,有时甚至是关于列中值分布的知识。)

也就是说,要在您的实际具体案例中获得良好的性能,您可能不得不改变或打破规则。

哦,另一个原因可能是约束:如果在所有更新后检查约束,则将唯一列增加一个可能没问题,但如果逐个完成,则会产生冲突。

于 2008-08-23T12:16:59.967 回答
0

基于集合在一个操作游标中完成与游标的行集一样多的操作

于 2008-08-23T12:41:07.210 回答
0

真正的答案是去找一本EF Codd的书,复习一下关系代数然后得到一本关于大 O 表示法的好书。经过近二十年的 IT 生涯,恕我直言,这是现代 MIS 或 CS 学位的一大悲剧:很少有人真正学习计算。你知道……“计算机”的“计算”部分吗?结构化查询语言(及其所有超集)只是关系代数的实际应用。是的,RDBMS 优化了内存管理和读/写,但对于过程语言也可以这样说。当我读到它时,最初的问题不是关于 IDE、软件,而是关于一种计算方法与另一种计算方法的效率。

即使快速熟悉大 O 表示法,也将开始阐明为什么在处理数据集时,迭代比声明性语句更昂贵。

于 2009-05-13T20:05:57.120 回答
0

简而言之,在大多数情况下,让数据库为您完成它会更快/更容易。

数据库在生活中的目的是以设定的格式存储/检索/操作数据并且非常快。您的 VB.NET/ASP.NET 代码可能远没有专用数据库引擎那么快。利用这一点是对资源的明智使用。

于 2009-07-13T13:54:07.353 回答