Как выполнить подсчет произвольного запроса (возможно, содержащего заказ по) - PullRequest
2 голосов
/ 25 октября 2011

Мне было поручено обновить внутреннюю структуру, которую мы используем внутри компании. Фреймворк выполняет одну из функций: вы передаете ему запрос, и он возвращает количество строк, содержащихся в запросе (фреймворк интенсивно использует DataReaders, поэтому нам нужно общее количество перед обработкой пользовательского интерфейса).

Запрос, по которому необходимо выполнить подсчет, может отличаться от проекта к проекту (SOL-внедрение не является проблемой, запрос не из пользовательского ввода, просто жестко запрограммирован другим программистом, когда они используют инфраструктуру для их проект.) и мне сказали, что просто попросить программистов написать второй запрос на счетчик недопустимо.

В настоящее время решение состоит в том, чтобы сделать следующее (я не писал это, мне просто сказали это исправить).

//executes query and returns record count
public static int RecordCount(string SqlQuery, string ConnectionString, bool SuppressError = false)
{

    //SplitLeft is just myString.Substring(0, myString.IndexOf(pattern)) with some error checking. and InStr is just a wrapper for IndexOf.
    //remove order by clause (breaks count(*))
    if (Str.InStr(0, SqlQuery.ToLower(), " order by ") > -1)
        SqlQuery = Str.SplitLeft(SqlQuery.ToLower(), " order by ");

    try
    {
        //execute query
        using (SqlConnection cnSqlConnect = OpenConnection(ConnectionString, SuppressError))
        using (SqlCommand SqlCmd = new SqlCommand("select count(*) from (" + SqlQuery + ") as a", cnSqlConnect))
        {
            SqlCmd.CommandTimeout = 120;
            return (Int32)SqlCmd.ExecuteScalar();
        }
    }
    catch (Exception ex)
    {
        if (SuppressError == false)
            MessageBox.Show(ex.Message, "Sql.RecordCount()");

        return -1;
    }

}

Однако он разбивается на такие запросы, как (опять же, не мой запрос, мне просто нужно, чтобы он работал)

select [ClientID], [Date], [Balance] 
from [Ledger] 
where Seq = (select top 1 Seq 
             from [Ledger] as l 
             where l.[ClientID] = [Ledger].[ClientID] 
             order by [Date] desc, Seq desc) 
      and Balance <> 0)

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

ОБНОВЛЕНИЕ: Предложение по порядку отброшено, потому что если вы включите его, используя мой метод или CTE, вы получите ошибку The ORDER BY clause is invalid in views, inline functions, derived tables, subqueries, and common table expressions, unless TOP or FOR XML is also specified.

Некоторые дополнительные сведения: Этот каркас используется для написания приложений преобразования. Мы пишем приложения для извлечения данных из старой базы данных клиентов и переноса их в формат нашей базы данных, когда клиент покупает наше программное обеспечение CRM . Часто мы работаем с исходными таблицами, которые плохо написаны и могут иметь размер несколько гигабайт. У нас нет ресурсов для хранения всей таблицы в памяти, поэтому мы используем DataReader для извлечения данных, чтобы все не было в памяти сразу. Однако требование - это индикатор выполнения с общим количеством записей, которые нужно обработать. Эта функция RecordCount используется для определения максимума индикатора выполнения. Он работает довольно хорошо, единственное препятствие - если программисту, пишущему преобразование, нужно упорядочить вывод данных, с условием order by в большинстве внешних разрывов запросов count(*)


Частичное решение: Я придумал это, пытаясь понять, оно не будет работать 100% времени, но я думаю, что оно будет лучше, чем текущее решение

Если я нахожу заказ по предложению, то проверяю, является ли первое, что есть в запросе, это выбор (а не последующие топы), я заменяю этот начальный текст на select top 100 percent. Это работает лучше, но я не публикую это как решение, так как надеюсь на универсальное решение.

Ответы [ 6 ]

1 голос
/ 25 октября 2011

вы бы отправили ответ о том, как сделать это "правильным образом", используя IQueryable

Предположим, у вас был какой-то произвольный запрос:

IQueryable<Ledger> query = myDataContext.Ledgers
  .Where(ledger => ledger.Seq ==
    myDataContext.Ledgers
      .Where(ledger2 => ledger2.ClientId == ledger.ClientId)
      .OrderByDescending(ledger2 => ledger2.Date)
      .ThenByDescending(ledger2 => ledger2.Seq)
      .Take(1).SingleOrDefault().Seq
  )
  .Where(ledger => ledger.Balance != 0);

Тогда выпросто получите количество строк, нет необходимости в каком-либо пользовательском методе или манипулировании запросом.

int theCount = query.Count();

//demystifying the extension method:
//int theCount = System.Linq.Queryable.Count(query);

LinqToSql включит ваше желание подсчитать в текст запроса.

1 голос
/ 25 октября 2011

Вместо изменения существующих предложений запроса - как насчет вставки нового предложения, предложения INTO.

SELECT *
INTO #MyCountTable -- new clause to create a temp table with these records.
FROM TheTable

SELECT @@RowCount
-- or maybe this:
--SELECT COUNT(*) FROM #MyCountTable

DROP TABLE #MyCountTable

Модификация запроса TSql кажется вечной борьбой, чтобы быть самой последней вещью, которая случается.

1 голос
/ 25 октября 2011

Предполагая, что вы не увидите ничего, кроме довольно простых select операторов, я не думаю, что вам не нужен полноценный анализатор SQL, чтобы делать то, что вы хотите.Вы можете разумно предположить, что у вас есть синтаксически правильный SQL.Однако вам необходимо создать токенизатор (лексический анализатор).

Лексический анализ, необходимый для Transact SQL, довольно прост.Список токенов состоит из (от макушки моей головы, так как прошло много времени с тех пор, как я должен был это сделать):

  • пробел
  • два типа комментариев:
    • -- комментарии в стиле / ... / комментарии в стиле *
  • три типа цитируемых литералов:
    • строковые литералы (например, `'my string literal') и
    • два варианта цитирования зарезервированных слов для использования в качестве имен столбцов или объектов:
      • стиль ANSI / ISO с использованием двойных кавычек (например, "table")
      • Стиль Transact-SQL с использованием квадратных скобок (например, [table])
  • шестнадцатеричные литералы (например,, 0x01A2F)
  • числовые литералы (например, 757, -3218, 5.4 или -7.6E-32, 5.0m, $5.3201 и т. Д.)
  • слова, зарезервированоили нет: буква Unicode, знак подчеркивания (''), знак 'at' ('@') или хэш ('#'), за которым следует ноль или более букв Unicode, десятичных цифр, знака подчеркивания (' ') или знаки at, доллар или хэш (' @ ',' $ 'или' # ').
  • операторы, включаяng скобки.

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

Я бы, вероятно, рассматривал комментарии как пробельные символы и объединял несколько последовательностей пробельных символов и комментировал их в один токен, поскольку это облегчает распознавание таких конструкций, как order by.

Причина, по которой вам не нужен синтаксический анализатор, заключается в том, что вы не особо заботитесь о дереве разбора.Что вас волнует, так это вложенные скобки.Итак ...

  1. После того, как вы получили лексический анализатор, который генерирует поток токенов, все, что вам нужно сделать, это есть и отбрасывать токены, считая открытые / закрывающие скобки, пока не увидитеКлючевое слово 'from' в скобках 0.

  2. Введите select count(*) в ваш StringBuilder.

  3. Начните добавлять токены (включая from) в StringBuilder до тех пор, пока вы не увидите 'order by' на глубине скобок 0. Для этого вам потребуется встроить определенное количество упреждений в свой лексер (см. мою предыдущую заметку о свертывании последовательностей пробелов и/ или комментарии в один пробел.)

  4. На этом этапе вы должны быть в значительной степени готовы.Выполните запрос.

ПРИМЕЧАНИЯ

  1. Параметризованные запросы, скорее всего, не будут работать.

  2. Рекурсивные запросы с условным обозначением CTE и with, вероятно, будут прерваны.

  3. Это отбросит все, что осталось за предложением ORDER BY: если запрос использует запросподсказка, предложение FOR или COMPUTE / COMPUTE BY, ваши результаты, вероятно, будут отличаться от исходного запроса (особенно с любыми предложениями compute, поскольку они разбивают наборы результатов запросов).

  4. Голые UNION запросы будут прерваны, поскольку что-то вроде

          select c1,c2 from t1
    UNION select c1,c2 from t2
    

    превратится в

          select count(*) from t1
    UNION select c1,c2 from t2
    
  5. Все этополностью непроверенные, только мои мысли, основанные на чудаках, которые мне приходилось делать на протяжении многих лет.

0 голосов
/ 25 октября 2011

Я бы не стал редактировать или пытаться анализировать SQL вообще, но вам, возможно, придется использовать EVIL CURSOR (не волнуйтесь, мы не будем явно перебирать что-либо).Здесь я просто передаю ваш специальный SQL-запрос процессу, который запускает его как курсор и возвращает количество строк в курсоре.Может быть доступно несколько оптимизаций, но я оставил их простыми, и это должно работать для любого действительного оператора выбора (даже CTE), который вы передаете ему.Нет необходимости кодировать и отлаживать свой собственный лексер T-SQL или что-либо еще.

create proc GetCountFromSelect (
    @SQL nvarchar(max)
)
as
begin
    set nocount on
    exec ('declare CountCursor insensitive cursor for ' + @SQL + ' for read only')
    open CountCursor
    select @@cursor_rows as RecordCount
    close CountCursor
    deallocate CountCursor
end
go

exec GetCountFromSelect '// Your SQL here'
go
0 голосов
/ 25 октября 2011

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

Кроме того, вы проверили сравнительную производительность

select count(id) from .... 

v / s

select count(*) from (select id, a+b from ....)

Проблема в том, что a + b нужно будет вычислять в последнем, по сути, выполняя запрос дважды.

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

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

Итак: реальный ответ в том, что вы не можете эффективно рассчитать произвольный запрос. Для неэффективного способа, если ваш набор результатов можно перематывать, перейдите к концу набора результатов, выясните количество строк и вернитесь к первой строке.

0 голосов
/ 25 октября 2011

Что если вместо того, чтобы попытаться пересобрать запрос, вы сделаете что-то вроде:

WITH MyQuery AS (
select [ClientID], [Date], [Balance] 
from [Ledger] 
where Seq = (select top 1 Seq 
            from [Ledger] as l 
                where l.[ClientID] = [Ledger].[ClientID] 
            order by [Date] desc, Seq desc) 
      and Balance <> 0)
)
  SELECT COUNT(*) From MyQuery;

Примечание. Я не проверял это на SQL Server 2005, но он должен работать.

Обновление:

Мы подтвердили, что SQL Server 2005 не поддерживает предложение ORDER BY в CTE.Это, однако, работает с Oracle и, возможно, с другими базами данных.

...