SELECT COUNT (*) против выборки дважды с явным курсором - PullRequest
11 голосов
/ 18 ноября 2008

Я прочитал книгу под названием «Программирование Oracle PL SQL» (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;

Ответы [ 8 ]

22 голосов
/ 18 ноября 2008

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

б) метод 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;

в) ВЫБЕРИТЕ СЧЕТЧИК (*) методом 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) ВЫБРАТЬ В метод

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

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

5 голосов
/ 18 ноября 2008

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

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

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

Трюк с Rownum означает прекращение выборки строк, когда у него есть два из них.

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

3 голосов
/ 18 ноября 2008

Это происходит от программистов, пишущих код, подобный следующему (это код 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 строки, а затем завершит работу. В предыдущем примере оракул сканировал (индекс или таблицу) ВСЕ строки, а затем заканчивал.

1 голос
/ 19 февраля 2009

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

PL / SQL допускает очень удобочитаемые программы. Не тратьте это просто на нано-оптимизацию.

1 голос
/ 18 ноября 2008

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

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

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

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

0 голосов
/ 05 декабря 2008

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

0 голосов
/ 18 ноября 2008

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'

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

0 голосов
/ 18 ноября 2008

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

...