Задача/головоломка 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 — Common Table Expression) не допускается.
  • Использование 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 (что, вероятно, не будет работать ни в одном продукте БД). - 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, и его эмуляция довольно сложна, например, см. Последняя ненулевая головоломка

Редактировать: на самом деле это не так сложно, я забыл второе решение Ицика, старый трюк с контрейлерами ;-)

Подход Мартина Смита будет работать для всех четырех СУБД.

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
@ДудуМарковиц . . . Это весело. Используется в подзапросе с равенством. - person Gordon Linoff; 08.10.2016
comment
Плохо, я не видел всю линейку - person David דודו Markovitz; 08.10.2016
comment
@ДудуМарковиц . . . Мне жаль. Я имел в виду, что я оставил это. Спасибо, что заметили это. Правильное место находится в подзапросе, где теперь исправлен код. Кстати, это хорошая проблема. - 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. Но объяснение покажет те же три шага 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