Нужно количество строк после оператора SELECT: каков оптимальный подход к SQL?

Я пытаюсь выбрать столбец из одной таблицы (без объединений), и мне нужно подсчитать количество строк, в идеале, прежде чем я начну извлекать строки. Я пришел к двум подходам, которые предоставляют необходимую мне информацию.

Подход 1:

SELECT COUNT( my_table.my_col ) AS row_count
  FROM my_table
 WHERE my_table.foo = 'bar'

потом

SELECT my_table.my_col
  FROM my_table
 WHERE my_table.foo = 'bar'

Или Подход 2

SELECT my_table.my_col, ( SELECT COUNT ( my_table.my_col )
                            FROM my_table
                           WHERE my_table.foo = 'bar' ) AS row_count
  FROM my_table
 WHERE my_table.foo = 'bar'

Я делаю это, потому что мой драйвер SQL (SQL Native Client 9.0) не позволяет мне использовать SQLRowCount в инструкции SELECT, но мне нужно знать количество строк в моем результате, чтобы выделить массив перед назначением ему информации. Использование динамически размещаемого контейнера, к сожалению, не вариант в этой области моей программы.

Я обеспокоен тем, что может произойти следующий сценарий:

  • SELECT для подсчета происходит
  • Происходит другая инструкция, добавляющая или удаляющая строку
  • Выполняется SELECT для данных, и вдруг массив имеет неправильный размер.
    - В худшем случае это попытается записать данные за пределами массива и приведет к сбою моей программы.

Подход 2 запрещает эту проблему?

Кроме того, будет ли один из двух подходов быстрее? Если да, то какие?

Наконец, есть ли лучший подход, который я должен рассмотреть (возможно, способ указать драйверу возвращать количество строк в результате SELECT с помощью SQLRowCount?)

Для тех, кто спрашивал, я использую Native C++ с вышеупомянутым драйвером SQL (предоставленным Microsoft).


person antik    schedule 28.10.2008    source источник
comment
Что вы делаете с этими данными, когда вам нужны как необработанные данные, так и количество строк? Если вам нужны все необработанные данные (которые вы выбираете), разве вы не можете считать их по мере чтения? Если вам не нужны все необработанные данные, не выбирайте их. Подсчет только для целей нумерации страниц?   -  person WW.    schedule 06.01.2009


Ответы (9)


Есть только два способа быть на 100 % уверенными в том, что COUNT(*) и фактический запрос дадут согласованные результаты:

  • Объедините COUNT(*) с запросом, как в вашем подходе 2. Я рекомендую форму, которую вы показываете в своем примере, а не коррелированную форму подзапроса, показанную в комментарии от kogus.
  • Используйте два запроса, как в подходе 1, после запуска транзакции на уровне изоляции SNAPSHOT или SERIALIZABLE.

Использование одного из этих уровней изоляции важно, потому что любой другой уровень изоляции позволяет новым строкам, созданным другими клиентами, стать видимыми в вашей текущей транзакции. Дополнительные сведения см. в документации MSDN по адресу SET TRANSACTION ISOLATION.

person Bill Karwin    schedule 28.10.2008
comment
Не спрашивая, это ответило на другое любопытство, которое у меня было в вашей первой пуле: очевидно, я бы предпочел не выполнять запрос подсчета повторно, если его можно оптимизировать. - person antik; 28.10.2008
comment
Верно; Я не эксперт по оптимизатору MS SQL Server, но я был бы удивлен, если бы он мог оптимизировать такой коррелированный подзапрос. - person Bill Karwin; 28.10.2008

Если вы используете SQL Server, после запроса вы можете выбрать @@RowCount (или, если ваш результирующий набор может содержать более 2 миллиардов строк, используйте функцию RowCount_Big()). Это вернет количество строк, выбранных предыдущим оператором, или количество строк, затронутых оператором вставки/обновления/удаления.

SELECT my_table.my_col
  FROM my_table
 WHERE my_table.foo = 'bar'

SELECT @@Rowcount

Или, если вы хотите включить количество строк в отправляемый результат, аналогичный подходу № 2, вы можете использовать предложение OVER.

SELECT my_table.my_col,
    count(*) OVER(PARTITION BY my_table.foo) AS 'Count'
  FROM my_table
 WHERE my_table.foo = 'bar'

Использование предложения OVER будет иметь гораздо лучшую производительность, чем использование подзапроса для получения количества строк. Использование @@RowCount будет иметь наилучшую производительность, потому что не будет никаких затрат на запрос для оператора select @@RowCount.

Обновление в ответ на комментарий: в приведенном мной примере указано количество строк в разделе, определяемое в данном случае как «PARTITION BY my_table.foo». Значение столбца в каждой строке равно количеству строк с одинаковым значением my_table.foo. Поскольку в вашем примере запроса было предложение «WHERE my_table.foo = 'bar'», все строки в результирующем наборе будут иметь одинаковое значение my_table.foo, и поэтому значение в столбце будет одинаковым для всех строк и равным (в в этом случае) это количество строк в запросе.

Вот лучший/более простой пример того, как включить столбец в каждую строку, которая представляет собой общее количество строк в наборе результатов. Просто удалите необязательное предложение Partition By.

SELECT my_table.my_col, count(*) OVER() AS 'Count'
  FROM my_table
 WHERE my_table.foo = 'bar'
person Adam Porad    schedule 28.10.2008
comment
Я бы предпочел, чтобы результат был в моем наборе результатов. Однако не похоже, что использование OVER, как вы описали, работает, когда я пытаюсь выполнить ваш запрос в своей таблице в SQL. - person antik; 28.10.2008
comment
Примечание. Если вы выберете только строки TOP 10, предложение OVER не будет равно 10. Это будет полный набор результатов, как если бы у вас не было TOP 10 в запросе. - person slayernoah; 07.03.2019

Подход 2 всегда будет возвращать количество, соответствующее вашему набору результатов.

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

SELECT 
  mt.my_row,
 (SELECT COUNT(mt2.my_row) FROM my_table mt2 WHERE mt2.foo = mt.foo) as cnt
FROM my_table mt
WHERE mt.foo = 'bar';
person JosephStyons    schedule 28.10.2008
comment
Это может сделать его коррелированным подзапросом, что означает, что он, вероятно, будет выполнять подзапрос для каждой строки результирующего набора. Некоррелированный подзапрос может быть оптимизирован, поэтому его нужно запустить только один раз. - person Bill Karwin; 28.10.2008
comment
Очень интересно; Я этого не знал. В этом случае я бы предложил использовать параметр, общий для основного запроса и подзапроса. - person JosephStyons; 28.10.2008

Если вас беспокоит, что количество строк, соответствующих условию, может измениться за несколько миллисекунд с момента выполнения запроса и получения результатов, вы можете/должны выполнять запросы внутри транзакции:

BEGIN TRAN bogus

SELECT COUNT( my_table.my_col ) AS row_count
FROM my_table
WHERE my_table.foo = 'bar'

SELECT my_table.my_col
FROM my_table
WHERE my_table.foo = 'bar'
ROLLBACK TRAN bogus

Это всегда будет возвращать правильные значения.

Кроме того, если вы используете SQL Server, вы можете использовать @@ROWCOUNT, чтобы получить количество строк, затронутых последним оператором, и перенаправить вывод реального запроса во временную таблицу или табличную переменную, поэтому можно вернуть все целиком, и не нужна транзакция:

DECLARE @dummy INT

SELECT my_table.my_col
INTO #temp_table
FROM my_table
WHERE my_table.foo = 'bar'

SET @dummy=@@ROWCOUNT
SELECT @dummy, * FROM #temp_table
person Joe Pineda    schedule 28.10.2008
comment
Количество может измениться, если вы используете READ COMMITTED, верно? Или SQL Server в режиме ODBC каким-то образом выполняет транзакции иначе, чем T-SQL? - person binki; 08.01.2016

Вот несколько идей:

  • Перейдите к подходу № 1 и измените размер массива, чтобы он содержал дополнительные результаты, или используйте тип, который автоматически изменяет размер по мере необходимости (вы не указываете, какой язык вы используете, поэтому я не могу быть более конкретным).
  • Вы можете выполнить оба оператора в подходе № 1 в транзакции, чтобы гарантировать, что счетчики будут одинаковыми оба раза, если ваша база данных поддерживает это.
  • Я не уверен, что вы делаете с данными, но если можно обработать результаты, не сохраняя их все сначала, это может быть лучшим методом.
person Robert Gamble    schedule 28.10.2008

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

person BoltBait    schedule 28.10.2008

Почему бы вам не поместить свои результаты в вектор? Таким образом, вам не нужно знать размер заранее.

person jonnii    schedule 28.10.2008
comment
Я должен был упомянуть, что ваше решение пришло мне в голову, но мне не нравится идея копировать мою информацию из базы данных в вектор, получать количество строк, а затем копировать все в векторе в массив. В этом случае я не могу изменить использование простого массива. - person antik; 28.10.2008
comment
Набор результатов из запроса к базе данных может быть огромным - он может даже не поместиться в память - поэтому не рекомендуется принудительно помещать набор результатов в память, прежде чем вы узнаете, поместится ли он. - person Zach Burlingame; 28.10.2008
comment
Если результирующий набор настолько огромен, вы, вероятно, все равно должны пейджинговать его. - person jonnii; 28.10.2008
comment
Дело в том, что вы еще не знаете, насколько велик результирующий набор. Он может быть огромным, а может быть пустым. Есть много случаев, когда знание размера изменит то, как вы обрабатываете результаты (например, сколько памяти вы выделяете размеру клиента, если вы выполняете обработку в памяти или постраничную обработку и т. д.). - person Zach Burlingame; 28.10.2008
comment
Вы не знаете размер набора результатов только в том случае, если у вас нет бизнес-знаний о системе или данных с самого начала. Я надеюсь, что постер имеет некоторое представление о том, на что похожи данные. По общему признанию, опасно привязывать себя к такому ограниченному решению. - person Tom H; 28.10.2008
comment
Не путайте здесь /rowset/ с /result set/. Вы должны знать относительный размер /rowset/ (т.е. varchar(30), int, blob), но вы не можете ожидать, что узнаете размер результирующего набора (т.е. число строк в результате запроса). - person Zach Burlingame; 29.10.2008
comment
Например, насколько большой массив вам нужен для хранения выбора всех идентификаторов и заголовков каждого вопроса в StackOverflow сегодня? Следующая неделя? В следующем году? Скажем, идентификатор 4 байта, а заголовок 300 байт. 27 тыс. строк займут около 7,8 млн. 1M строк займет около 300M. Количество результатов меняется со временем. - person Zach Burlingame; 29.10.2008
comment
Опять же, это зависит от ситуации. Очень возможна ситуация, когда у вас есть очень хорошее представление о том, сколько строк вы получите обратно. Что, если бы массив содержал страны мира? Это число очень незначительно меняется из года в год. Мы не знаем особенностей этой ситуации. - person Tom H; 29.10.2008
comment
Несмотря на это, базовый механизм хранения вектора (обычно) представляет собой массив, который удваивается в размере, когда ему не хватает места. Преобразование Vector‹T› в T[] не должно быть таким уж большим делом. Я сомневаюсь, что разница в производительности при использовании вектора будет такой большой. - person jonnii; 29.10.2008
comment
Для вещей, которые поместятся в память, если вы не можете использовать динамический массив (как он заявил) и вы не знаете /точный/размер, вы не можете безопасно написать какой-либо код для обработки входящего набора результатов как один кусок. Если вы даже не знаете, поместится ли он в память, Vector‹T› vs T[] — немой. - person Zach Burlingame; 29.10.2008
comment
Это достойный ответ. ОП уже заявил, что хочет, чтобы данные хранились в необработанном фиксированном массиве для последующей обработки. Вектор позволяет вам делать это очень хорошо, увеличивая массив по мере необходимости, а затем может предоставлять указатель (.data()) на базовый массив для такой обработки. Если набор результатов слишком велик, то, когда вектор превышает этот известный порог, обработка может быть прекращена. Конечно, быстрая проверка количества строк может избавить вас от ненужной работы в этом маловероятном сценарии. - person jschultz410; 26.04.2018

Возможно, вы захотите подумать о лучшем шаблоне для работы с данными этого типа.

Ни один драйвер SQL с самоподготовкой не скажет вам, сколько строк будет возвращено вашим запросом, прежде чем вернуть строки, потому что ответ может измениться (если только вы не используете транзакцию, которая сама по себе создает проблемы).

Количество строк не изменится - погуглите ACID и SQL.

person dkretz    schedule 28.10.2008
comment
Хорошая инфа по КИСЛОТЕ, не совсем по уважающему себя комментарию. Многие драйверы SQL выполняют сервер запросов, но не возвращают весь набор результатов в одном и том же подпрограммном вызове (т. е. сначала вызывают SQLExecute, а затем SQLFetch для получения результатов). Это часто скрыто от конечного пользователя (например, набор данных .NET). - person Zach Burlingame; 28.10.2008
comment
Я полагаю, что принцип изоляции в рамках концепции ACID в достаточной мере снимает мои опасения по поводу подхода № 2. Если я могу рассчитывать на то, что на эти результаты не повлияют запросы других пользователей к базе данных, я готов использовать этот подход. Спасибо. - person antik; 28.10.2008

Просто добавить это, потому что это лучший результат в Google для этого вопроса. В sqlite я использовал это, чтобы получить количество строк.

WITH temptable AS
  (SELECT one,two
   FROM
     (SELECT one, two
      FROM table3
      WHERE dimension=0
      UNION ALL SELECT one, two
      FROM table2
      WHERE dimension=0
      UNION ALL SELECT one, two
      FROM table1
      WHERE dimension=0)
   ORDER BY date DESC)
SELECT *
FROM temptable
LEFT JOIN
  (SELECT count(*)/7 AS cnt,
                        0 AS bonus
   FROM temptable) counter
WHERE 0 = counter.bonus
person Tschallacka    schedule 09.05.2015