SQL Предизвикателство/Пъзел: Дадено е проследяване на стека - Как да намерите най-горния елемент във всяка точка от времето?

  • Моят реален случай на употреба беше да обединявам вложени диапазони. Начертах някои скици и след това видях приликата между диапазони, започващи и завършващи за стек на PUSH и POP операции. Разбрах, че решаването на този проблем ще реши и първоначалния проблем.

  • Колоната op всъщност може да бъде премахната от въпроса. Когато val е NULL, това е POP операция, в противен случай е PUSH операция.

Пъзелът

Таблица, stack_trace, съдържа следните колони:

  • i - Цяло число, което представлява момент от време.
  • op - 2 възможни операции: I ("in"/"push") и O ("out"/"pop").
  • val - Стойността, въведена от операцията "in"/"push" или NULL за операция "out"/"pop".

    Целта е да се намери каква е била стойността в горната част на стека във всеки момент от време (i).

e.g.

(NULL стойностите са представени тук като празни интервали)

данни:

i   op  val 
--  --  --  
1   I   A   
2   I   B   
3   O
4   I   C
5   O    
6   O   

Необходим резултат:

i   top_of_stack_val
--  ----------------
1   A
2   B
3   A
4   C
5   A
6   

Изисквания

  • Решението трябва да бъде единична SQL заявка (подзаявките са добри).
  • Разрешени са само следните клаузи: SELECT, FROM, WHERE, GROUP BY, HAVING, ПОРЪЧАЙТЕ ОТ.
  • Използването на клауза WITH (CTE - Общ табличен израз) не е разрешено.
  • Използването на T-SQL, PL/SQL и др. не е разрешено.
  • Използването на UDF (дефинирани от потребителя функции) не е разрешено.
  • Използването на променливи не е разрешено.

Примерни данни

create table stack_trace
(
    i       int
   ,op      char(1)
   ,val     char(1)
)
;

insert into stack_trace (i,op,val) values (1,'I','A');
insert into stack_trace (i,op,val) values (2,'I','B');
insert into stack_trace (i,op,val) values (3,'I','C');
insert into stack_trace (i,op,val) values (4,'I','D');
insert into stack_trace (i,op,val) values (5,'I','E');
insert into stack_trace (i,op)     values (6,'O');
insert into stack_trace (i,op)     values (7,'O');
insert into stack_trace (i,op)     values (8,'O');
insert into stack_trace (i,op,val) values (9,'I','F');
insert into stack_trace (i,op)     values (10,'O');
insert into stack_trace (i,op,val) values (11,'I','G');
insert into stack_trace (i,op,val) values (12,'I','H');
insert into stack_trace (i,op)     values (13,'O');
insert into stack_trace (i,op)     values (14,'O');
insert into stack_trace (i,op,val) values (15,'I','I');
insert into stack_trace (i,op,val) values (16,'I','J');
insert into stack_trace (i,op,val) values (17,'I','K');
insert into stack_trace (i,op,val) values (18,'I','L');
insert into stack_trace (i,op,val) values (19,'I','M');
insert into stack_trace (i,op)     values (20,'O');
insert into stack_trace (i,op,val) values (21,'I','N');
insert into stack_trace (i,op)     values (22,'O');
insert into stack_trace (i,op,val) values (23,'I','O');
insert into stack_trace (i,op)     values (24,'O');
insert into stack_trace (i,op,val) values (25,'I','P');
insert into stack_trace (i,op)     values (26,'O');
insert into stack_trace (i,op)     values (27,'O');
insert into stack_trace (i,op,val) values (28,'I','Q');
insert into stack_trace (i,op,val) values (29,'I','R');
insert into stack_trace (i,op)     values (30,'O');
insert into stack_trace (i,op)     values (31,'O');
insert into stack_trace (i,op)     values (32,'O');
insert into stack_trace (i,op)     values (33,'O');
insert into stack_trace (i,op)     values (34,'O');
insert into stack_trace (i,op)     values (35,'O');
insert into stack_trace (i,op,val) values (36,'I','S');
insert into stack_trace (i,op)     values (37,'O');
insert into stack_trace (i,op)     values (38,'O');
insert into stack_trace (i,op,val) values (39,'I','T');
insert into stack_trace (i,op,val) values (40,'I','U');
insert into stack_trace (i,op)     values (41,'O');
insert into stack_trace (i,op,val) values (42,'I','V');
insert into stack_trace (i,op,val) values (43,'I','W');
insert into stack_trace (i,op,val) values (44,'I','X');
insert into stack_trace (i,op)     values (45,'O');
insert into stack_trace (i,op)     values (46,'O');
insert into stack_trace (i,op,val) values (47,'I','Y');
insert into stack_trace (i,op)     values (48,'O');
insert into stack_trace (i,op)     values (49,'O');
insert into stack_trace (i,op,val) values (50,'I','Z');
insert into stack_trace (i,op)     values (51,'O');
insert into stack_trace (i,op)     values (52,'O');

Необходими резултати

i   top_of_stack_val
--  ----------------
1   A
2   B
3   C
4   D
5   E
6   D
7   C
8   B
9   F
10  B
11  G
12  H
13  G
14  B
15  I
16  J
17  K
18  L
19  M
20  L
21  N
22  L
23  O
24  L
25  P
26  L
27  K
28  Q
29  R
30  Q
31  K
32  J
33  I
34  B
35  A
36  S
37  A
38  
39  T
40  U
41  T
42  V
43  W
44  X
45  W
46  V
47  Y
48  V
49  T
50  Z
51  T
52  

person David דודו Markovitz    schedule 08.10.2016    source източник
comment
Премахнах несъвместимите тагове на базата данни. Моля, маркирайте въпроса с базата данни, която всъщност използвате.   -  person Gordon Linoff    schedule 08.10.2016
comment
Гордън, Това са базите данни, които използвам и всъщност съм забравил да добавя Teradata.   -  person David דודו Markovitz    schedule 08.10.2016
comment
@MT0 това всъщност е един от любимите ми   -  person David דודו Markovitz    schedule 11.03.2020


Отговори (5)


Лично аз се съмнявам, че в крайна сметка ще намерите SQL, който можете просто да използвате във всички SQL Server, Teradata, Postgres и Oracle и който има приемлива производителност, ако таблицата изобщо е голяма.

Решение на SQL Server (демонстрация) би било както следва

SELECT i,
       SUBSTRING(MAX(FORMAT(i, 'D10') + val) OVER (PARTITION BY Pos ORDER BY i 
                                                     ROWS UNBOUNDED PRECEDING), 11, 8000)
FROM   (SELECT st.*,
               sum(CASE WHEN op = 'I' THEN 1 ELSE -1 END) 
                  OVER (ORDER BY i ROWS UNBOUNDED PRECEDING) AS pos
        FROM   stack_trace st) t1
ORDER  BY i; 
person Martin Smith    schedule 08.10.2016
comment
Тази OVER логика трябва да работи за всичките четири СУБД, това са само второстепенните неща (SUBSTRING & +), които се нуждаят от модифициран синтаксис :-) - person dnoeth; 09.10.2016
comment
@dnoeth - да и подпълването на низа с FORMAT - person Martin Smith; 09.10.2016
comment
да Някаква причина да добавите третия параметър за SUBSTRING (8000)? - person David דודו Markovitz; 09.10.2016
comment
@DuduMarkovitz SQL Server изисква да му кажете дължината на необходимия подниз. Искаме всичко от 11-ия знак нататък, тъй като първите 10 символа са подплатената стойност i. Стойност 1 би била достатъчна за вашите примерни данни, но удължаването означава, че ще продължи да работи, ако в стека се извадят по-дълги стойности. - person Martin Smith; 09.10.2016
comment
Забравих, че SQL Server няма 2 параметъра SUBSTRING - person David דודו Markovitz; 09.10.2016
comment
@MartinSmith, вие сте поканени да проверите моето общо решение - person David דודו Markovitz; 09.10.2016
comment
Идеята е правилна (опитвайки се да прикачите i към val, за да вземете максимума над резултата). Решението обаче трябва да е общо - трябва да работи за стек от стойности от всякакъв тип данни. Конкатенирането на i и val, както е направено тук, зависи от имплицитното преобразуване в низ и - още по-лошо - преобразуването обратно от низ в оригиналния тип данни на val (което вероятно няма да работи в който и да е DB продукт). - person mathguy; 10.10.2016
comment
@mathguy - Работата с всеки тип данни без персонализиране може да е желана функция, но със сигурност не е задължителна. Въпросът пита за таблица с конкретна схема. - person Martin Smith; 10.10.2016

Това е хубав пъзел.

Тъй като основната ми СУБД е Teradata, написах решение за него, използвайки аналитични функции (нуждае се от TD14.10+):

SELECT dt.*,
   -- find the last item in the stack with the same position
   Last_Value(val IGNORE NULLS)
   Over (PARTITION BY pos
         ORDER BY i) AS top_of_stack_val
FROM 
 ( 
   SELECT st.*,
      -- calculate the number of items in the stack
      Sum(CASE WHEN op = 'I' THEN 1 ELSE -1 end) 
      Over (ORDER BY i
            ROWS Unbounded Preceding) AS pos
   FROM stack_trace AS st
 ) AS dt;

Това решение работи и за Oracle, но PostgreSQL & SQL Server не поддържат опцията IGNORE NULLS за LAST_VALUE и емулирането й е доста сложно, например вижте Последният не NULL пъзел

Редактиране: Всъщност не е толкова сложно, забравих второто решение на Itzik, стария трик с връщане ;-)

Подходът на Мартин Смит ще работи и за четирите СУБД.

person dnoeth    schedule 08.10.2016
comment
Успях да опростя отговора си доста на фона на това +1 - person Martin Smith; 09.10.2016
comment
Дитер, човекът и легендата :-) Благодаря ви за работата във форумите на Teradata. Между другото, има друго решение за Teradata, което използва патентован синтаксис. Бихте ли искали да опитате? - person David דודו Markovitz; 09.10.2016
comment
@DuduMarkovitz: Опитвам се да избягвам RESET WHEN :-) Разбира се, това опростява заявка (едно вместо две или три нива на OLAP функции), но планът е абсолютно същият. Ако имате нужда само от един OLAP, това е добре, в противен случай обикновено могат да се добавят допълнителни OLAP функции към съществуващо ниво без нова стъпка в Обяснение. - person dnoeth; 09.10.2016

Въпреки че изисква допълнителна стъпка -

Общо решение за Hive, Teradata, Oracle, SQL Server и PostgreSQL

select      s.i

           ,min (s.val) over
            (
                partition by    s.depth
                               ,s.depth_val_seq
            )                                   as top_of_stack_val            

from       (select      s.i
                       ,s.val
                       ,s.depth
                        
                       ,count (s.val) over 
                        (
                            partition by    s.depth
                            order by        s.i
                            rows            between unbounded preceding and current row
                        )                                                                   as depth_val_seq
                         
            from       (select      s.i
                                   ,s.val
                                    
                                   ,sum (case s.op when 'I' then 1 else -1 end)   over
                                    (
                                        order by        s.i
                                        rows            between unbounded preceding and current row
                                    )                                                                   as depth
             
                        from        stack_trace s
                        )
                        s
            )
            s
 
order by    i
;
person David דודו Markovitz    schedule 09.10.2016
comment
хубаво. Използването на depth_val_seq за групиране заедно на натискащия ред със съответния изскачащ ред е доста умно. - person Martin Smith; 09.10.2016

Това всъщност е интересен проблем. Това, което бих направил, е да следя позицията на всеки елемент в стека. Можете да направите това, като използвате кумулативна сума:

select st.*,
       sum(case when op = 'I' then 1 else -1 end) over (order by i) as pos
from stack_trace st;

Уви, в този момент мисля, че имате нужда от доста сложно съединение или подзаявка, за да разберете най-новата стойност, към която се отнася pos. Ето един метод:

with st as (
      select st.*,
             sum(case when op = 'I' then 1 else -1 end) over (order by i) as pos
      from stack_trace st
     )
select st.*,
       (select val
        from st st2
        where st2.i <= st.id and st2.pos = st.pos and
              st2.val is not null
        order by i desc
        fetch first 1 row only
       ) as top_of_stack_val
from st;
person Gordon Linoff    schedule 08.10.2016
comment
Приятно :-) Искате ли да опитате и да го решите, като се позовавате на базовата таблица само веднъж? - person David דודו Markovitz; 08.10.2016
comment
Между другото, забравихте да използвате колоната pos :-) - person David דודו Markovitz; 08.10.2016
comment
@DuduMarkovitz . . . Това е забавно. Използва се в подзаявката с равенство. - person Gordon Linoff; 08.10.2016
comment
Лошото ми, не съм видял цялата линия - person David דודו Markovitz; 08.10.2016
comment
@DuduMarkovitz . . . Съжалявам. Имах предвид, че съм го пропуснал. Благодаря ви, че го забелязахте. Правилното място е в подзаявката, където кодът вече е коригиран. Между другото, това е добър проблем. - person Gordon Linoff; 09.10.2016

Терадата

select      s.i

           ,first_value (s.val) over 
            (
                partition by    s.depth
                order by        s.i
                reset when      s.op = 'I'
            )                                as top_of_stack_val

from       (select      s.i
                       ,s.val
                       ,s.op

                       ,sum (case s.op when 'I' then 1 else -1 end)   over
                        (
                            order by        s.i
                            rows            unbounded preceding
                        )                                                   as depth

            from        stack_trace s
            )
            s
            
order by    i
;
person David דודו Markovitz    schedule 15.10.2016
comment
Когато използвате MIN вместо FIRST_VALUE, той ще работи преди TD14.10. Но Explain ще покаже същите три STAT стъпки като вашето общо решение. LAST_VALUE се нуждае само от две STAT-стъпки :-) - person dnoeth; 17.10.2016
comment
@dnoeth LAST_VALUE също беше първият ми избор. решението RESET е само за забавление. Допълнителната стъпка STAT тук е доста изненадваща, тъй като нулирането се извършва на скалар, а не на допълнителна аналитична функция. Изглежда като неоптимално внедряване на оптимизатора. - person David דודו Markovitz; 17.10.2016
comment
Никога няма да намерите по-добър план, като използвате RESET WHEN, той винаги е същият като ръчния вложен OLAP на две или три нива (като CASE срещу COALESCE, просто прост синтаксис). Ако има две функции с един и същи прозорец, планът може да е още по-лош. Когато синтаксисът беше внедрен в 13.10, го тествах много и бях доста разочарован. - person dnoeth; 17.10.2016
comment
@dnoth, преди няколко години отворих ER, предлагайки изпълнение на множество клаузи RESET WHEN с една и съща дефиниция на прозорец в една стъпка :-) - person David דודו Markovitz; 17.10.2016