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 - индекс столбца вашего столбца первичного ключа.