Comment effectuer un count sur une requête arbitraire (contenant éventuellement une command par)

J'ai été chargé de mettre à jour notre cadre interne que nous utilisons à l'interne. L'une des choses que le framework fait est de lui passer une requête et il returnnera le nombre de lignes que la requête contient (Le framework fait un usage intensif de DataReaders donc nous avons besoin du total avant les choses de l'interface user).

La requête sur laquelle le count doit être effectué peut être différente d'un projet à l'autre (l'injection SOL n'est pas un problème, la requête ne provient pas d'un user, mais un autre programmeur qui utilise le framework pour son projet). ) et on m'a dit que le simple fait que les programmeurs écrivent une deuxième requête pour le count est inacceptable.

Actuellement, la solution est de faire ce qui suit (je n'ai pas écrit cela, on m'a juste dit de le réparer).

//executes query and returns record count public static int RecordCount(ssortingng SqlQuery, ssortingng ConnectionSsortingng, bool SuppressError = false) { //SplitLeft is just mySsortingng.Subssortingng(0, mySsortingng.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(ConnectionSsortingng, 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; } } 

Cependant, il casse sur des requêtes comme (encore une fois, pas ma requête, j'ai juste besoin de le faire fonctionner)

 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) 

comme il va tout enlever après la order by et rompt la requête. Je pensais que je pouvais passer d'un simple couplage de strings à un parsingur plus compliqué, mais avant cela, je voulais savoir s'il y avait une meilleure façon de procéder.

UPDATE: La clause order by est supprimée car si vous l'incluez en utilisant ma méthode ou un CTE vous obtiendrez l'erreur 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.

Quelques détails supplémentaires: Ce framework est utilisé pour écrire des applications de conversion. Nous écrivons des applications pour extraire des données d'une ancienne database de clients et les transférer dans notre format de database lorsqu'un client achète notre logiciel CRM . Souvent, nous travaillons avec des tables sources qui sont mal écrites et peuvent avoir plusieurs Gigs. Nous n'avons pas les ressources pour conserver toute la table en memory, donc nous utilisons un DataReader pour extraire datatables afin que tout ne soit pas en memory à la fois. Cependant, une exigence est une barre de progression avec le nombre total d'loggings à traiter. Cette fonction RecordCount est utilisée pour représenter le maximum de la barre de progression. Cela fonctionne assez bien, le seul hic est si le programmeur qui écrit la conversion doit order la sortie de données, en ayant une clause order by dans le nombre de ruptures de requête le plus externe count(*)


Solution partielle: Je suis arrivé avec ceci en essayant de le comprendre, cela ne fonctionnera pas 100% du time mais je pense que ce sera mieux que la solution actuelle

Si je trouve une clause order by, je vérifie ensuite si la première chose dans la requête est un select (et pas de Top following). Je remplace ce text début par un select top 100 percent . Cela fonctionne mieux mais je ne publie pas cela comme une solution car j'espère une solution universelle.

En supposant que vous n'allez voir que des instructions select assez ordinaires, je ne pense pas que vous n'avez pas besoin d'un parsingur SQL complet pour faire ce que vous voulez. Vous pouvez raisonnablement faire l'hypothèse que vous avez SQL syntaxiquement valide. Cependant, vous devez build un tokenizer (parsingur lexical).

L'parsing lexicale nécessaire pour Transact SQL est assez simple. La list des jetons se compose de (du haut de ma tête, puisque cela faisait un moment que je devais le faire):

  • espace
  • deux types de commentaires:
    • -- commentaires de style / / `-style commentaires
  • trois types de littéraux cités:
    • littéraux de string (par exemple, «my ssortingng literal»), et
    • deux variantes de citant des mots réservés à utiliser comme noms de colonne ou d'object:
      • Style ANSI / ISO, en utilisant des guillemets (par exemple, "table" )
      • Style Transact-SQL, en utilisant des crochets (par exemple, [table] )
  • littéraux hexadécimaux (par exemple, 0x01A2F )
  • littéraux numériques (p. ex. 757 , -3218 , 5.4 ou -7.6E-32 , 5.0m , $5.3201 etc.)
  • mots, réservés ou non: lettre unicode, trait de soulignement (' '), 'at'-signe (' @ ') ou hash (' # '), suivi de zéro ou plus de lettres unicode, numbers décimaux, trait de soulignement (' ' ) ou les signes at, dollar ou hash ('@', '$' ou '#').
  • opérateurs, y compris les parenthèses.

Cela peut pratiquement être fait avec des expressions régulières. Si vous utilisiez Perl , vous auriez terminé en un jour, facile. Cela prendra probablement un peu plus de time en C #, cependant.

Je traiterais probablement les commentaires comme des espaces et réduirais plusieurs séquences d'espaces et commenterais en un seul jeton d'espace blanc, car cela faciliterait la reconnaissance de constructions telles que l' order by .

La raison pour laquelle vous n'avez pas besoin d'un parsingur est que vous ne vous souciez pas vraiment de l'tree d'parsing. Ce qui vous intéresse, ce sont les parenthèses nestedes. Alors…

  1. Une fois que vous avez un parsingur lexical qui émet un stream de jetons, tout ce que vous devez faire est de manger et de jeter les jetons en comptant les parenthèses ouvertes / fermantes jusqu'à ce que vous voyiez un mot key 'from' entre parenthèses 0.

  2. Écrivez le select count(*) dans votre SsortingngBuilder.

  3. Commencez à append des jetons (y compris le) dans le SsortingngBuilder jusqu'à ce que vous voyiez un "ordre par" à la profondeur entre parenthèses 0. Vous aurez besoin de build une certaine anticipation dans votre lexeur pour le faire (voir ma note précédente concernant l'effondrement de séquences d'espaces et / ou de commentaires en un jeton d'espace unique.)

  4. À ce stade, vous devriez être à peu près terminé. Exécutez la requête.

REMARQUES

  1. Les requêtes paramétrées ne fonctionneront probablement pas.

  2. Les requêtes récursives, avec une clause CTE et une clause with seront probablement interrompues.

  3. Cela supprimera tout ce qui dépasse la clause ORDER BY : si la requête utilise query hint, une clause FOR ou COMPUTE / COMPUTE BY , vos résultats seront probablement différents de la requête d'origine (en particulier avec compute clauses de compute , car les résultats des requêtes sont fragmentés). sets).

  4. Les requêtes UNION nu seront brisées, car quelque chose comme

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

    va se transformer en

      select count(*) from t1 UNION select c1,c2 from t2 
  5. Tout cela n'a pas encore été testé, juste mes pensées basées sur des trucs bizarres que j'ai dû faire au fil des ans.

Au lieu de modifier les clauses existantes de la requête – que diriez-vous d'insert une nouvelle clause, la clause 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 modification de la requête semble être une lutte éternelle pour être la dernière chose qui arrive.

Souhaitez-vous postr une réponse sur la façon de le faire "dans le bon sens" en utilisant IQueryable

Supposons que vous ayez une requête arbitraire:

 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); 

Ensuite, vous obtenez juste le nombre de lignes, pas besoin de toute méthode personnalisée ou manipulation de requête.

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

LinqToSql inclura votre désir de countr dans le text de la requête.

Je suppose que vous voulez supprimer l'ordre par clause pour améliorer les performances. Le cas général est assez complexe et vous aurez besoin d'un parsingur sql complet pour supprimer la clause de command.

Aussi, avez-vous vérifié la performance comparative de

 select count(id) from .... 

contre

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

Le problème est que le a + b devra être évalué dans le dernier, en exécutant essentiellement la requête deux fois.

Si vous voulez une barre de progression parce que la récupération elle-même est lente, cela est complètement contre-productif, car vous passerez presque le même time à estimer le nombre.

Et si l'application est suffisamment complexe pour que datatables puissent changer entre les deux requêtes, vous ne savez même pas à quel point le count est fiable.

Donc: la vraie réponse est que vous ne pouvez pas countr sur une requête arbitraire de manière efficace. Pour une manière non-efficace, si votre jeu de résultats est rembobinable, passez à la fin du jeu de résultats, calculez le nombre de lignes et revenez à la première ligne.

Et si plutôt que d'essayer de rebuild votre requête, vous faites quelque chose comme:

 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; 

Remarque: Je n'ai pas testé cela sur SQL Server 2005, mais cela devrait fonctionner.

Mettre à jour:

Nous avons confirmé que SQL Server 2005 ne prend pas en charge une clause ORDER BY dans un environnement CTE. Cela fonctionne cependant avec Oracle et peut-être d'autres bases de données.

Je n'éditerais pas ou n'essaierais pas d'parsingr le SQL, mais vous devrez peut-être utiliser un CURSEUR EVIL (ne vous inquiétez pas, nous n'interpréterons pas explicitement quoi que ce soit). Ici, je voudrais simplement passer votre SQL ad hoc à un proc qui l'exécute comme un slider, et returnne le nombre de lignes dans le slider. Il peut y avoir quelques optimizations disponibles, mais je l'ai gardé simple, et cela devrait fonctionner pour toute instruction select valide (même les CTE) que vous lui passez. Pas besoin de coder et déboguer votre propre lexer T-SQL ou quoi que ce soit.

 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