2010-02-06 11 views
3

J'ai la déclaration SQL suivante, qui fonctionne parfaitement bien. J'espérais voir comment cela pourrait être refaçonné de sorte qu'il ne nécessite pas l'utilisation de RANK/Partition ... si possible.Cette instruction Sql peut-elle être refactorisée pour ne PAS utiliser RANK/PARTITION?

SELECT LogEntryId, FileId, CreatedOn, EventTypeId 
FROM (SELECT a.LogEntryId, a.FileId, a.CreatedOn, a.EventTypeId, 
     RANK() OVER (PARTITION BY ClientName ORDER BY a.CreatedOn DESC) AS MostRecentEventRank 
    FROM LogEntries a 
    WHERE (a.EventTypeId = 2 or a.EventTypeId = 4)) SubQuery 
WHERE MostRecentEventRank = 1 

Qu'est-ce qu'il essaie de faire? Saisissez tous les enregistrements de la table, regroupés par nom de client, puis classés par les plus récents. Page 228

  • Filtrez uniquement les types d'événement # 2 (une connexion) ou # 4 (une déconnexion).
  • Maintenant, pour chaque nom de client, récupérez l'enregistrement le plus récent. Ceci a pour effet de capturer l'événement le plus récent (pour une connexion ou une déconnexion), pour chaque utilisateur unique dans la table. J'aime le RANK/PARTITION, mais j'espérais voir s'il est possible de le faire sans l'utiliser.

  • +0

    Pour toutes les personnes qui ont répondu -> je suis sincèrement sans voix (dans le bon sens) et humilié :) j'adore comment il y a un certain nombre d'approches différentes. C'est pourquoi j'aime SO. Je vais tous les vérifier et voir de quelle façon je veux peaufiner ce chat. Une fois de plus, <3 à toutes les réponses et <3 à SO :) –

    +0

    Avez-vous déjà obtenu ma réponse de travail? Vous avez demandé si un schéma pourrait aider. Je vais quand même heureusement mettre à jour ma requête en réponse à certaines informations de schéma. – ErikE

    +0

    non - j'ai manqué de temps sur l'exigence de travail et j'ai dû faire avec ce qui a fonctionné, etc. sincères appologies. –

    Répondre

    4

    Encore une autre variante: sélectionnez les clients, puis utilisez CROSS APPLY (.. TOP (1) ... ORDER BY ...) pour obtenir l'entrée correspondante.

    SELECT c.ClientName,r.LogEntryId, r.FileId, r.CreatedOn, r.EventTypeId 
    FROM (
    SELECT DISTINCT ClientName 
    FROM LogEntries 
    WHERE EventTypeId IN (2,4)) as c 
    CROSS APPLY (
        SELECT TOP (1) a.LogEntryId, a.FileId, a.CreatedOn, a.EventTypeId 
        FROM LogEntries as a 
        WHERE a.ClientName = c.ClientName 
        AND a.EventTypeId IN (2,4) 
        ORDER BY a.CreatedOn DESC) as r; 
    

    Mise à jour

    Pour parler de la performance sur une requête T-SQL sans connaître le schéma est non-sens. Cette requête est parfaitement optimale sur un schéma correctement conçu pour ses besoins. Depuis l'accès est par ClientName et CreatedOn, alors même un schéma simpliste aurait besoin de prendre cela en considération:

    CREATE TABLE LogEntries (
        LogEntryId int identity(1,1), 
        FileID int, 
        CreatedOn datetime, 
        EventTypeID int, 
        ClientName varchar(30) 
    ); 
    
    create clustered index cdxLogEntries on LogEntries (
        ClientName, CreatedOn DESC); 
    go 
    

    et permet de charger la table avec quelques lignes 2.4M:

    declare @i int; 
    set @i = 0; 
    
    while @i < 1000 
    begin 
        insert into LogEntries (FileId, CreatedOn, EventTypeId, ClientName) 
        select cast(rand()*100 as int), 
         dateadd(minute, -rand()*10000, getdate()), 
         cast(rand() * 5 as int), 
         'Client' + cast(@i as varchar(10)) 
         from master..spt_values; 
        set @i = @i+1; 
    end 
    

    Quelle heure et Est-ce que nous obtenons avec set statistics io on; set statistics time on; sur un cache chauffé?

    (410 row(s) affected) 
    Table 'LogEntries'. Scan count 411, logical reads 14354, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
    
    SQL Server Execution Times: 
        CPU time = 1219 ms, elapsed time = 1932 ms. 
    

    1,9 sec pour obtenir les données de 2.4M entrées sur mon ordinateur portable (qui est de 4 ans et 1Go RAM). Et il y a encore beaucoup de place pour améliorer la conception du schéma. Séparer ClientName dans une table normalisée avec une clé étrangère de confiance de LogEntries dans cela réduirait le temps de manière significative. Des index filtrés corrects sur EntryTypeId IN (2,4) contribueraient également. Nous n'avons même pas commencé l'exploration des possibilités de parallélisme.

    Ceci est SQL, les performances sont obtenues sur la planche à dessin de votre schéma, pas dans l'éditeur de texte de votre requête.

    +0

    Cela sort comme 2 balayages de table. Avez-vous cherché à implémenter la solution de Quassnoi liée par OMG Ponies dans les commentaires d'une autre réponse? – ErikE

    +0

    'ressortant comme 2 analyses de table' ... sur quel schéma exactement? J'ai mis à jour ma question car parler de la performance d'un morceau de texte SQL sans aucun schéma est plutôt stupide. –

    +0

    Je ne t'insultais pas. J'ai simplement énoncé le fait de savoir comment cela s'est passé pour moi. Puisque la table avec laquelle je travaillais n'avait pas d'index, peut-être en ajouter quelques-uns changera les choses. Mon commentaire était censé commencer le dialogue, pas vous frapper sur le visage avec mon gant. – ErikE

    1

    Vous pouvez utiliser une exclusivité left join:

    select  cur.* 
    from  LogEntries cur 
    left join LogEntries next 
    on   next.ClientName = cur.ClientName 
          and next.EventTypeId in (2,4) 
          and next.CreatedOn > cur.CreatedOn    
    where  next.ClientName is null 
          and cur.EventTypeId in (2,4) 
    

    Cette joint la table sur elle-même, la recherche de lignes plus tard dans l'état on. Dans la clause where, vous spécifiez qu'aucune ligne ultérieure ne peut exister. De cette façon, vous filtrez tout sauf la dernière ligne par client.

    +0

    Est-ce que cela fonctionne vraiment? Trick mignon si c'est le cas, mais comment est-ce en termes de vitesse? Il me semble que c'est la même chose que faire une sous-requête par ligne, est-ce la façon dont SQL l'implémente? – Hogan

    +0

    Cela fonctionne vraiment et c'est vraiment rapide, à un coût de lisibilité. Si vous êtes dans la performance, voir la solution «cross apply» à http://explainextended.com/2009/12/01/sql-server-selecting-records-holding-group-wise-maximum-with-ties/ – Andomar

    +0

    @Andomar: Ne devriez-vous pas avoir 'AND AND next.eventtypeid IN (2,4)' pour vous assurer que la comparaison est sur les enregistrements avec la valeur 'eventtypeid' correcte? –

    0

    C'est parti. Peut-être plus rapide ... pas sûr. En outre, cela suppose que ClientName + CreatedOn est unique.

    ;WITH MostRecent AS 
    (
        SELECT ClientName, Max(CreatedOn) AS CreatedOn 
        FROM LogEntries 
        WHERE EventTypeID IN (2,4) 
        GROUP BY ClientName 
    ) 
    SELECT LogEntryId, FileId, CreatedOn, EventTypeId 
    FROM LogEntries L 
    INNER JOIN MostRecent R ON L.ClientName = R.ClientName AND L.CreatedOn = R.CreatedON 
    

    Remarque, je n'ai pas testé pourrait avoir des fautes de frappe.

    +0

    Notez simplement que cette solution ne gérera pas les dates CreatedOn dupliquées pour la même personne. – ErikE

    +0

    @Emtucifor, ne dis-je pas ça en première ligne? – Hogan

    +0

    :) applaudissements. mais le nom du client ET createdOn ne sont pas uniques. .. ce qui était la raison pour laquelle j'ai initialement utilisé le mot-clé PARTITION. –

    2

    Balayage de table unique, pas de fonction de fenêtrage, groupe unique par, pas de problèmes avec les dates en double, performances égales avec les fonctions de fenêtrage, ou même les surpasse avec de très grandes requêtes. (Mise à jour: Je ne sais pas comment il fonctionne par rapport au TOP 1 AVEC TIES/CROSS APPLY méthode Comme il utilise une analyse, il pourrait être plus lent dans certaines situations..)

    SELECT 
        LogEntryID = Convert(int, Substring(Packed, 9, 4)), 
        FileID = Convert(int, Substring(Packed, 13, 4)), 
        CreatedOn = Convert(datetime, Substring(Packed, 1, 8)), 
        EventTypeID = Convert(int, Substring(Packed, 17, 4)) 
    FROM 
        (
         SELECT 
         Packed = Max(
          Convert(binary(8), CreatedOn) 
          + Convert(binary(4), LogEntryID) 
          + Convert(binary(4), FileID) 
          + Convert(binary(4), EventTypeID) 
         ) 
         FROM LogEntries 
         WHERE EventTypeID IN (2,4) 
         GROUP BY ClientName 
        ) X 
    

    Si quelqu'un aimerait voir en action, voici un script de création:

    USE tempdb 
    CREATE TABLE LogEntries (
        LogEntryID int not null identity(1,1), 
        FileID int, 
        CreatedOn datetime, 
        EventTypeID int, 
        ClientName varchar(30) 
    ) 
    
    INSERT LogEntries VALUES (1, GetDate()-20, 2, 'bob') 
    INSERT LogEntries VALUES (1, GetDate()-19, 3, 'bob') 
    INSERT LogEntries VALUES (1, GetDate()-18, 4, 'bob') 
    INSERT LogEntries VALUES (1, GetDate()-17, 3, 'bob') 
    INSERT LogEntries VALUES (1, GetDate()-19.5, 2, 'anna') 
    INSERT LogEntries VALUES (1, GetDate()-18.5, 3, 'anna') 
    INSERT LogEntries VALUES (1, GetDate()-17.5, 4, 'anna') 
    INSERT LogEntries VALUES (1, GetDate()-16.5, 3, 'anna') 
    

    S'il vous plaît noter que cette méthode profite de la représentation des octets interne des types de données données ayant le même ordre que les valeurs du type. Les types de données compressés comme float ou decimal ne fonctionneront PAS: ceux qui nécessitent une conversion en quelque chose de convenable en premier, comme int, bigint ou character.

    En outre, les nouveaux types de données Date et heure dans SQL 2008 ont des représentations différentes qui ne seront pas compressées correctement à utiliser avec cette méthode. Je ne l'ai pas examiné le type de données temps encore, mais pour le type de données Date:

    DECLARE @d date 
    SET @d ='99990101' 
    SELECT Convert(binary(3), @d) -- 0x6EB837 
    

    La valeur réelle est 0x37B86E, il est donc de les stocker dans l'ordre des octets inverse (la date « zéro » est 0001-01- 01).

    +0

    +1 Technique intéressante. Je me demande si les conversions binaires sont plus performantes que l'anti-jointure sur un grand nombre de lignes. – Andomar

    +0

    D'après mon expérience, les conversions binaires sont très peu chères et représentent toujours un bon compromis entre plus d'E/S. La conversion des valeurs en chaîne est lente et coûteuse, mais les conversions binaires sont essentiellement une copie de la mémoire. – ErikE

    +0

    Je voudrais particulièrement savoir comment cela se compare à l'optimisation de Quassnoi avec un CTE récursif et 'CROSS APPLY'; cette solution est bonne mais donnera toujours un scan table/index complet, alors que Quassnoi finit par ressembler un peu au scan skip d'Oracle. – Aaronaught