SELECT COUNT (*) по сравнению с извлечением дважды с явным курсором

Я прочитал книгу, название которой - "Oracle PL SQL Programming" (2-е изд.) Стивена Фейерштейна и Билла Прибыля. На стр. 99 существует точка, предполагающая, что

Не удаляйте "SELECT COUNT (*)" из таблицы, если вам действительно не нужно знать общее количество "хитов". Если вам нужно только знать, существует ли более одного совпадения, просто дважды выберите с явным курсором.

Не могли бы вы кому-нибудь объяснить этот момент больше, предоставив пример? Спасибо.

Обновление:

Как Стивен Фюерштейн и Билл Прибыл рекомендуют нам не использовать SELECT COUNT() для проверки наличия записей в таблице или нет, может ли кто-нибудь помочь мне отредактировать код ниже, чтобы избежать использования SELECT COUNT (*), используя явные вместо этого курсор? Этот код написан в хранимой процедуре Oracle.

У меня есть таблица emp (emp_id, emp_name,...), поэтому, чтобы проверить предоставленный идентификатор сотрудника или нет:

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;

Ответ 1

Существует ряд причин, по которым разработчики могут выполнять выбор COUNT (*) из таблицы в программе PL/SQL:

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) SELECT COUNT (*) с помощью метода ROWNUM

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) SELECT COUNT (*) с помощью метода ROWNUM

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;

Какой метод вы используете, во многом зависит от предпочтения (и некоторого религиозного фанатизма!) Стивен Фейерштейн всегда предпочитал явные курсоры по неявным (SELECT INTO и курсоры FOR FOR); Том Ките предпочитает неявные курсоры (и я согласен с ним).

Важным моментом является то, что выбор COUNT (*) без ограничения ROWCOUNT является дорогостоящим и поэтому должен выполняться только в том случае, если счет необходим.

Что касается вашего дополнительного вопроса о том, как переписать его с помощью явного курсора:

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 достаточно умен, чтобы знать, что его нужно только один раз извлечь.

Ответ 2

Если вам все интересно, попробуйте

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

Это займет меньше кода, чем метод ручного курсора, и это, вероятно, будет быстрее.

трюк rownum означает прекратить выборки строк, если у них есть два из них.

Если вы не указали какой-либо лимит на счет (*), для завершения может потребоваться много времени, в зависимости от количества строк, которые у вас есть. В этом случае использование цикла курсора для чтения двух строк из таблицы будет выполняться быстрее.

Ответ 3

Это происходит от программистов, которые пишут код, похожий на следующий (это код psuedo!).

Вы хотите проверить, есть ли у клиента несколько заказов:

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 строки, а затем закончить. Предыдущий пример заставит оракул сканировать (индекс или таблицу) для ВСЕХ строк, а затем закончить.

Ответ 4

Он означает открыть курсор и выбрать не только первую запись, но вторую, и тогда вы узнаете, что ее больше.

Так как мне никогда не нужно знать, что SELECT COUNT(*) есть >= 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(*) из таблиц плохо обрабатывается оптимизатором. то есть:.

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

Это, для меня, приведет к менее читабельному, менее портативному и менее обслуживаемому коду.

Ответ 5

Прежде чем принимать предложения Стивена Фейерштейна слишком серьезно, просто сделайте небольшой тест. Является ли счетчик (*) заметно медленнее, чем явный курсор в вашем случае? Нет? Затем лучше используйте конструкцию, которая позволяет использовать простой, читаемый код. Что в большинстве случаев было бы "select count (*) в v_cnt... если v_cnt > 0 then..."

PL/SQL позволяет использовать очень читаемые программы. Не тратьте это, чтобы нано оптимизировать.

Ответ 6

В зависимости от БД может быть таблица sys, которая хранит приблизительный счетчик и может быть запрошена в постоянное время. Полезно, если вы хотите узнать, имеет ли таблица 20 строк или 20 000 или 20 000 000.

Ответ 7

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'

Кроме того, никогда не используйте курсоры. Если вы думаете, что действительно действительно нуждаетесь в них, побейте себя лопатой, пока вы не передумаете. Пусть реликвии древнего прошлого остаются реликтами древнего прошлого.

Ответ 8

Если вы хотите получить количество строк в таблице, пожалуйста, не используйте count (*), я бы предложил count (0), что 0 - индекс столбца вашего столбца первичного ключа.