11

我读过 Steven Feuerstein 和 Bill Pribyl 的书,书名是“Oracle PL SQL Programming”(第 2 版)。在第 99 页,有一点建议是

不要从表中“SELECT COUNT(*)”,除非您确实需要知道“命中”的总数。如果您只需要知道是否有多个匹配项,只需使用显式游标获取两次即可。

您可以通过提供示例向我进一步解释这一点吗?谢谢你。

更新:

正如 Steven Feuerstein 和 Bill Pribyl 建议我们不要使用 SELECT COUNT() 来检查表中的记录是否存在,任何人都可以帮我编辑下面的代码以避免使用显式游标来使用 SELECT COUNT(*) 吗?此代码是在 Oracle 存储过程中编写的。

我有一个表 emp(emp_id, emp_name, ...),所以要检查提供的员工 ID 是否正确:

CREATE OR REPLACE PROCEDURE do_sth ( emp_id_in IN emp.emp_id%TYPE )
IS
v_rows INTEGER;
BEGIN
    ...

    SELECT COUNT(*) INTO v_rows
    FROM emp
    WHERE emp_id = emp_id_in;

    IF v_rows > 0 THEN
        /* do sth */
    END;

    /* more statements */
    ...

END do_sth;
4

8 回答 8

22

开发人员可能会从 PL/SQL 程序中的表中执行 select COUNT(*) 的原因有很多:

1)他们真的需要知道表中有多少行。

在这种情况下,别无选择:选择 COUNT(*) 并等待结果。这在许多桌子上会很快,但在一张大桌子上可能需要一段时间。

2)他们只需要知道一行是否存在。

这并不保证计算表中的所有行。许多技术是可能的:

a) 显式游标方法:

DECLARE
   CURSOR c IS SELECT '1' dummy FROM mytable WHERE ...;
   v VARCHAR2(1);
BEGIN
   OPEN c;
   FETCH c INTO v;
   IF c%FOUND THEN
      -- A row exists
      ...
   ELSE
      -- No row exists
      ...
   END IF;
END;

b) SELECT INTO 方法

DECLARE
   v VARCHAR2(1);
BEGIN
   SELECT '1' INTO v FROM mytable 
   WHERE ... 
   AND ROWNUM=1; -- Stop fetching if 1 found
   -- At least one row exists
EXCEPTION
   WHEN NO_DATA_FOUND THEN
      -- No row exists
END;

c) 使用 ROWNUM 方法选择 COUNT(*)

DECLARE
   cnt INTEGER;
BEGIN
   SELECT COUNT(*) INTO cnt FROM mytable 
   WHERE ... 
   AND ROWNUM=1; -- Stop counting if 1 found
   IF cnt = 0 THEN
      -- No row found
   ELSE
      -- Row found
   END IF;
END;

3)他们需要知道是否存在超过 1 行。

(2) 工作技术的变化:

a) 显式游标方法:

DECLARE
   CURSOR c IS SELECT '1' dummy FROM mytable WHERE ...;
   v VARCHAR2(1);
BEGIN
   OPEN c;
   FETCH c INTO v;
   FETCH c INTO v;
   IF c%FOUND THEN
      -- 2 or more rows exists
      ...
   ELSE
      -- 1 or 0 rows exist
      ...
   END IF;
END;

b) SELECT INTO 方法

DECLARE
   v VARCHAR2(1);
BEGIN
   SELECT '1' INTO v FROM mytable 
   WHERE ... ;
   -- Exactly 1 row exists
EXCEPTION
   WHEN NO_DATA_FOUND THEN
      -- No row exists
   WHEN TOO_MANY_ROWS THEN
      -- More than 1 row exists
END;

c) 使用 ROWNUM 方法选择 COUNT(*)

DECLARE
   cnt INTEGER;
BEGIN
   SELECT COUNT(*) INTO cnt FROM mytable 
   WHERE ... 
   AND ROWNUM <= 2; -- Stop counting if 2 found
   IF cnt = 0 THEN
      -- No row found
   IF cnt = 1 THEN
      -- 1 row found
   ELSE
      -- More than 1 row found
   END IF;
END;

您使用哪种方法很大程度上取决于偏好(以及一些宗教狂热!) Steven Feuerstein 一直更喜欢显式游标而不是隐式游标(SELECT INTO 和游标 FOR 循环);Tom Kyte 喜欢隐式游标(我同意他的观点)。

重要的一点是在不限制 ROWCOUNT 的情况下选择 COUNT(*) 是昂贵的,因此只有在真正需要计数时才应该这样做。

至于您关于如何使用显式游标重写此内容的补充问题:

CREATE OR REPLACE PROCEDURE do_sth ( emp_id_in IN emp.emp_id%TYPE )
IS
v_rows INTEGER;
BEGIN
    ...

    SELECT COUNT(*) INTO v_rows
    FROM emp
    WHERE emp_id = emp_id_in;

    IF v_rows > 0 THEN
        /* do sth */
    END;

    /* more statements */
    ...

END do_sth;

那将是:

CREATE OR REPLACE PROCEDURE do_sth ( emp_id_in IN emp.emp_id%TYPE )
IS
    CURSOR c IS SELECT 1
                FROM emp
                WHERE emp_id = emp_id_in;
    v_dummy INTEGER;
BEGIN
    ...

    OPEN c;    
    FETCH c INTO v_dummy;
    IF c%FOUND > 0 THEN
        /* do sth */
    END;
    CLOSE c;

    /* more statements */
    ...

END do_sth;

但实际上,在您的示例中,这并没有好坏之分,因为您正在选择主键,而 Oracle 足够聪明,知道它只需要获取一次。

于 2008-11-18T10:28:41.627 回答
5

如果您只对两个感兴趣,请尝试

SELECT 'THERE ARE AT LEAST TWO ROWS IN THE TABLE'
FROM DUAL
WHERE 2 =
(
    SELECT COUNT(*)
    FROM TABLE
    WHERE ROWNUM < 3
)

与手动游标方法相比,它需要的代码更少,而且可能更快。

rownum 技巧意味着一旦有两个行就停止获取行。

如果您不对计数(*)设置某种限制,则可能需要很长时间才能完成,具体取决于您拥有的行数。在这种情况下,使用游标循环手动从表中读取 2 行会更快。

于 2008-11-18T03:52:30.053 回答
3

这来自编写类似于以下代码的程序员(这是伪代码!)。

您想检查客户是否有多个订单:

if ((select count(*) from orders where customerid = :customerid) > 1)
{
    ....
}

这是一种非常低效的做事方式。正如马克布雷迪所说,如果你想知道一个罐子里是否有便士,你会数出罐子里所有的便士,还是只确定有 1 个(在你的例子中是 2 个)?

这可以更好地写成:

if ((select 1 from (select 1 from orders where customerid = :customerid) where rownum = 2) == 1)
{
    ....
}

这可以防止“计算所有硬币”的困境,因为 Oracle 将获取 2 行,然后完成。前面的示例将导致 oracle 扫描(索引或表)所有行,然后完成。

于 2008-11-18T03:13:12.177 回答
1

他的意思是打开一个游标,不仅获取第一条记录,还获取第二条记录,然后你就会知道有不止一个。

因为我似乎从来不需要知道SELECT COUNT(*)is >= 2,所以我不知道为什么这在任何 SQL 变体中都是一个有用的习惯用法。要么没有记录,要么至少有一个,当然,但不是两个或更多。无论如何,总会有EXISTS

那,以及 Oracle 的优化器似乎很差的事实...... - 我会质疑该技术的相关性。

要解决 TheSoftwareJedi 的评论:

WITH CustomersWith2OrMoreOrders AS (
    SELECT CustomerID
    FROM Orders
    GROUP BY CustomerID
    HAVING COUNT(*) >= 2
)
SELECT Customer.*
FROM Customer
INNER JOIN CustomersWith2OrMoreOrders
    ON Customer.CustomerID = CustomersWith2OrMoreOrders.CustomerID

经过适当的索引,即使在 SQL Server 中使用这样的全域查询,我也从未遇到过性能问题。但是,我一直在这里和其他站点上遇到有关 Oracle 优化器问题的评论。

我自己对 Oracle 的体验并不好

来自 OP 的评论似乎是说COUNT(*)优化器不能很好地处理表中的完整内容。IE:

IF EXISTS (SELECT COUNT(*) FROM table_name HAVING COUNT(*) >= 2)
BEGIN
END

(当存在主键时,可以简化为简单的索引扫描 - 在极端优化的情况下,可以简单地查询 sysindexes.rowcnt 中的索引元数据 - 查找条目数 - 全部没有游标)是通常应避免有利于:

DECLARE CURSOR c IS SELECT something FROM table_name;
BEGIN
    OPEN c
    FETCH c INTO etc. x 2 and count rows and handle exceptions
END;

IF rc >= 2 THEN BEGIN
END

对我来说,这会导致代码的可读性、可移植性和可维护性降低。

于 2008-11-18T02:36:28.507 回答
1

在你把 Steven Feuerstein 的建议看得太严肃之前,先做一个小基准测试。在您的情况下,count(*) 是否明显比显式游标慢?不?然后更好地使用允许简单、可读代码的结构。在大多数情况下,这将是“选择 count(*) 进入 v_cnt ...如果 v_cnt>0 则 ...”

PL/SQL 允许非常可读的程序。不要仅仅为了纳米优化而浪费它。

于 2009-02-19T13:05:16.803 回答
0

根据数据库的不同,可能有一个 sys 表,其中存储了一个近似计数,并且可以在恒定时间内查询。如果您想知道表有 20 行、20,000 行还是 20,000,000 行,这很有用。

于 2008-11-18T03:22:13.253 回答
-1

SQL 服务器:

if 2 = (
    select count(*) from (
        select top 2 * from (
            select T = 1 union
            select T = 2 union
            select T = 3 ) t) t)
    print 'At least two'

另外,永远不要使用游标。如果你认为你真的需要它们,用铁锹打败自己,直到你改变主意。让远古的遗物成为远古的遗物。

于 2008-11-18T04:00:23.627 回答
-2

如果要获取表中的行数,请不要使用count(*),我建议count(0) 0 是主键列的列索引。

于 2008-12-05T09:27:04.290 回答