2017-09-05 3 views
2

Remarque: J'ai une requête de travail, mais je recherche des optimisations pour l'utiliser sur de grandes tables.ignorer des lignes consécutives après une valeur spécifique

Supposons que j'ai une table comme ceci:

id session_id value 
1  5   7 
2  5   1 
3  5   1 
4  5   12 
5  5   1 
6  5   1 
7  5   1 
8  6   7 
9  6   1 
10  6   3 
11  6   1 
12  7   7 
13  8   1 
14  8   2 
15  8   3 

Je veux que les id de toutes les lignes avec la valeur 1 à une exception près: groupes de saut à valeur 1 qui suivent directement une valeur 7 dans le même session_id.

Fondamentalement, je rechercherais les groupes de valeur 1 qui suivent directement une valeur 7, limitée par l'identificateur de session, et ignorons ces groupes. Je montre ensuite toutes les valeurs restantes 1 lignes.

La sortie souhaitée montrant de l'ID:

5 
6 
7 
11 
13 

j'ai pris un peu d'inspiration de this post et a fini avec ce code:

declare @req_data table (
    id int primary key identity, 
    session_id int, 
    value int 
) 

insert into @req_data(session_id, value) values (5, 7) 
insert into @req_data(session_id, value) values (5, 1) -- preceded by value 7 in same session, should be ignored 
insert into @req_data(session_id, value) values (5, 1) -- ignore this one too 
insert into @req_data(session_id, value) values (5, 12) 
insert into @req_data(session_id, value) values (5, 1) -- preceded by value != 7, show this 
insert into @req_data(session_id, value) values (5, 1) -- show this too 
insert into @req_data(session_id, value) values (5, 1) -- show this too 
insert into @req_data(session_id, value) values (6, 7) 
insert into @req_data(session_id, value) values (6, 1) -- preceded by value 7 in same session, should be ignored 
insert into @req_data(session_id, value) values (6, 3) 
insert into @req_data(session_id, value) values (6, 1) -- preceded by value != 7, show this 
insert into @req_data(session_id, value) values (7, 7) 
insert into @req_data(session_id, value) values (8, 1) -- new session_id, show this 
insert into @req_data(session_id, value) values (8, 2) 
insert into @req_data(session_id, value) values (8, 3) 



select id 
from (
    select session_id, id, max(skip) over (partition by grp) as 'skip' 
    from (
     select tWithGroups.*, 
      (row_number() over (partition by session_id order by id) - row_number() over (partition by value order by id)) as grp 
     from (
      select session_id, id, value, 
       case 
        when lag(value) over (partition by session_id order by session_id) = 7 
         then 1 
        else 0 
       end as 'skip' 
      from @req_data 
     ) as tWithGroups 
    ) as tWithSkipField 
    where tWithSkipField.value = 1 
) as tYetAnotherOutput 
where skip != 1 
order by id 

Cela donne le résultat souhaité, mais avec 4 blocs de sélection I pense que c'est trop inefficace pour utiliser sur de grandes tables.

Existe-t-il un moyen plus propre et plus rapide de faire cela?

+0

Jetez un oeil à LAG: https://docs.microsoft.com/en-us/sql/t-sql/functions/lag-transact-sql Vous pouvez regarder la ligne précédente. – Leonidas199x

+0

Ils ont utilisé 'LAG' dans la requête originale @ Leonidas199x – scsimon

+3

Je pense que cela appartient à la révision de code, pas Stack Exchange. C'est le code de travail. –

Répondre

0
SELECT CRow.id 
FROM @req_data AS CRow 
CROSS APPLY (SELECT MAX(id) AS id FROM @req_data PRev WHERE PRev.Id < CRow.id AND PRev.session_id = CRow.session_id AND PRev.value <> 1) MaxPRow 
LEFT JOIN @req_data AS PRow ON MaxPRow.id = PRow.id 
WHERE CRow.value = 1 AND ISNULL(PRow.value,1) <> 7 
+0

Cela semble être la solution ayant le plus faible impact sur le serveur. Jusqu'à présent, je n'ai pas été en mesure de trouver des lacunes dans la production, donc c'est bien. :) EDIT: Désolé, j'ai eu mes commentaires dans le mauvais sens .. –

0

Vous pouvez utiliser la requête suivante:

select id, session_id, value, 
      coalesce(sum(case when value <> 1 then 1 end) 
        over (partition by session_id order by id), 0) as grp 
from @req_data 

pour obtenir:

id session_id value grp 
---------------------------- 
1 5   7  1 
2 5   1  1 
3 5   1  1 
4 5   12  2 
5 5   1  2 
6 5   1  2 
7 5   1  2 
8 6   7  1 
9 6   1  1 
10 6   3  2 
11 6   1  2 
12 7   7  1 
13 8   1  0 
14 8   2  1 
15 8   3  2 

Ainsi, cette requête détecte les îles de consécutifs 1 enregistrements qui appartiennent au même groupe, tel que spécifié par le premier ligne précédente avec value <> 1.

Vous pouvez à nouveau utiliser une fonction de fenêtre pour détecter tous les îlots 7. Si vous enveloppez cela dans un deuxième cte, vous pouvez enfin obtenir le résultat souhaité en filtrant toutes les îles 7:

;with session_islands as (
    select id, session_id, value, 
      coalesce(sum(case when value <> 1 then 1 end) 
        over (partition by session_id order by id), 0) as grp 
    from @req_data 
), islands_with_7 as (
    select id, grp, value, 
      count(case when value = 7 then 1 end) 
      over (partition by session_id, grp) as cnt_7 
    from session_islands 
) 
select id 
from islands_with_7 
where cnt_7 = 0 and value = 1 
+0

@NicoKempe Je suis heureux d'avoir pu aider et accueillir Stack Overflow. Veuillez cocher cette case ou toute autre réponse comme acceptée si elle vous a aidé à résoudre votre problème. –

+0

Cela fonctionne, mais cela prend environ 15 ms sur une table avec 1000 enregistrements. Pour comparaison, d'autres scripts prennent 3-12ms. Selon le plan d'exécution réel, il y a deux actions de tri qui prennent chacune 32%. EDIT: Désolé, j'ai eu mes commentaires dans le mauvais sens .. –

2

Ce qui suit devrait bien fonctionner pour cela.

WITH 
    cte_ControlValue AS (
     SELECT 
      rd.id, rd.session_id, rd.value, 
      ControlValue = ISNULL(CAST(SUBSTRING(MAX(bv.BinVal) OVER (PARTITION BY rd.session_id ORDER BY rd.id), 5, 4) AS INT), 999) 
     FROM 
      @req_data rd 
      CROSS APPLY (VALUES (CAST(rd.id AS BINARY(4)) + CAST(NULLIF(rd.value, 1) AS BINARY(4)))) bv (BinVal) 
     ) 
SELECT 
    cv.id, cv.session_id, cv.value 
FROM 
    cte_ControlValue cv 
WHERE 
    cv.value = 1 
    AND cv.ControlValue <> 7; 

... Résultats

id   session_id value 
----------- ----------- ----------- 
5   5   1 
6   5   1 
7   5   1 
11   6   1 
13   8   1 

Edit: Comment et pourquoi cela fonctionne ... Le principe de base est tiré de Itzik Ben-Gan's "The Last non NULL Puzzle".

Essentiellement, nous comptons 2 comportements différents que la plupart des gens ne pensent généralement pas sur les ...

1) NULL = NULL + quoi que ce soit. 2) Vous pouvez CAST ou CONVERTER un INT en un type de données BINARY de longueur fixe et il continuera à trier comme un INT (par opposition au tri comme une chaîne de texte).

Ceci est plus facile à voir lorsque les étapes intermittentes sont ajoutées à la requête dans le CTE ...

SELECT 
    rd.id, rd.session_id, rd.value, 
    bv.BinVal, 
    SmearedBinVal = MAX(bv.BinVal) OVER (PARTITION BY rd.session_id ORDER BY rd.id), 
    SecondHalfAsINT = CAST(SUBSTRING(MAX(bv.BinVal) OVER (PARTITION BY rd.session_id ORDER BY rd.id), 5, 4) AS INT), 
    ControlValue = ISNULL(CAST(SUBSTRING(MAX(bv.BinVal) OVER (PARTITION BY rd.session_id ORDER BY rd.id), 5, 4) AS INT), 999) 
FROM 
    #req_data rd 
    CROSS APPLY (VALUES (CAST(rd.id AS BINARY(4)) + CAST(NULLIF(rd.value, 1) AS BINARY(4)))) bv (BinVal) 

... Résultats

id   session_id value  BinVal    SmearedBinVal  SecondHalfAsINT ControlValue 
----------- ----------- ----------- ------------------ ------------------ --------------- ------------ 
1   5   7   0x0000000100000007 0x0000000100000007 7    7 
2   5   1   NULL    0x0000000100000007 7    7 
3   5   1   NULL    0x0000000100000007 7    7 
4   5   12   0x000000040000000C 0x000000040000000C 12    12 
5   5   1   NULL    0x000000040000000C 12    12 
6   5   1   NULL    0x000000040000000C 12    12 
7   5   1   NULL    0x000000040000000C 12    12 
8   6   7   0x0000000800000007 0x0000000800000007 7    7 
9   6   1   NULL    0x0000000800000007 7    7 
10   6   3   0x0000000A00000003 0x0000000A00000003 3    3 
11   6   1   NULL    0x0000000A00000003 3    3 
12   7   7   0x0000000C00000007 0x0000000C00000007 7    7 
13   8   1   NULL    NULL    NULL   999 
14   8   2   0x0000000E00000002 0x0000000E00000002 2    2 
15   8   3   0x0000000F00000003 0x0000000F00000003 3    3 

En regardant la colonne BinVal, nous voyons une valeur hexadécimale de 8 octets pour tous [valeur] = non 1 lignes et NULLS où [valeur] = 1. .. Les 4 premiers octets sont l'identifiant (utilisé pour la commande) et les 2 derniers octets sont [valeur] (utilisé pour définir la "valeur précédente non-1" ou définir le tout sur NULL.)

La 2ème étape est de "maculer" les valeurs non-NULL dans les valeurs NULL en utilisant la fonction MAX encadrée par la fenêtre, partitionnée par session_id et ordonnée par l'ID

La troisième étape consiste à analyser les 4 derniers octets et à les reconvertir en un type de données INT (SecondHalfAsINT) et à traiter tous les NULL résultant de l'absence de valeur précédente non-1 (ControlValue). Comme nous ne pouvons pas référencer une fonction fenêtrée dans la clause WHERE, nous devons lancer la requête dans un CTE (une table dérivée fonctionnerait aussi bien) afin que nous puissions utiliser le nouveau ControlValue dans la clause where.

+2

Ceci est une solution intelligente, mais il a vraiment besoin d'une explication. – GarethD

+2

Ceci est une variation de la solution d'Itzik Ben-Gan au dernier casse-tête non-nul. http: //sqlmag.com/t-sql/last-non-null-puzzle ... Dans ce cas, le Idead est, nous voulons toutes les lignes avec une [valeur] = 1, mais seulement si le non-1 précédent value is not = 7 ... Donc, en traitant la valeur [value] = 1 comme nulle, on peut utiliser la méthode d'Itzik pour obtenir la valeur "last non-null" ... Une fois que nous avons cela, la clause finale where devient vraiment facile à écrire. Je vais aller de l'avant et mettre à jour la réponse une meilleure explication. –

+0

Une solution intelligente avec une explication très utile, merci. En termes de performances, il semble que ce soit en deuxième position, mais je ne suis pas expert en la matière. (J'ai utilisé "SET STATISTICS TIME ON" et "SET STATISTICS IO ON" pour obtenir quelques statistiques) –