SELECT COUNT(*)与使用显式游标提取两次

时间:2008-11-18 02:22:34

标签: oracle plsql database-cursor

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

除非你真的需要知道“点击”的总数,否则不要从表中“选择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;

8 个答案:

答案 0 :(得分:22)

开发人员可能会从PL / SQL程序中的表中执行选择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)他们需要知道是否存在多于一行。

(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非常聪明,只知道它只需要获取一次。

答案 1 :(得分:5)

如果您感兴趣的是两个,请尝试

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

使用手动光标方法所需的代码少, 它可能会更快。

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

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

答案 2 :(得分:3)

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

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

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

这是一种非常低效的做事方式。正如Mark Brady所说,如果你想知道一个罐子是否包含便士,你会计算罐子里的所有便士,还是只确定有1个(或者你的例子中有2个)?

这可以更好地写成:

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

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

答案 3 :(得分:1)

他的意思是打开一个光标,不仅可以获取第一个记录,还可以获取第二个记录,然后您将知道不止一个。

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

那个,以及甲骨文的优化器似乎很差...... - 我会质疑这种技术的相关性。

解决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中使用像这样的整个Universe查询,我也从未遇到过性能问题。但是,我一直在这里和其他网站上发表关于Oracle优化器问题的评论。

我自己对Oracle的经验有not been good

来自OP的评论似乎是说优化器没有很好地处理来自表的完整COUNT(*)。即:

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

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

答案 4 :(得分:1)

在你认为Steven Feuerstein的建议过于严肃之前,只需做一点基准测试。在您的情况下,count(*)明显慢于显式游标吗?没有?然后更好地使用允许简单,可读代码的构造。在大多数情况下,这将是“select count(*)into v_cnt ... if v_cnt&gt; 0 then ...”

PL / SQL允许非常易读的程序。不要浪费那些只是纳米优化。

答案 5 :(得分:0)

根据数据库的不同,可能存在一个存储近似计数的sys表,可以在恒定时间内查询。如果您想知道表格是20行还是20,000或20,000,000,则非常有用。

答案 6 :(得分:-1)

SQL Server:

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'

此外,请勿使用游标。如果你认为你真的需要它们,那就用铲子打自己,直到你改变主意。让古代过去的遗物仍然是古代遗物。

答案 7 :(得分:-2)

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