История учебы Васи и конечный автомат на SQL

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

После выступления на PgConf2023 и общению с профессионалами по базам данных, у меня на выходных появилось время на реализацию идеи, как реализовать логику конечного автомата на SQL в PostgreSQL. Идея применима к любой СУБД, поддерживающей агрегатные функции, определяемые пользователем.

Скоро сказка сказывается, да не скоро дело делается... Жил был Вася. И устои в обществе где он жил, были описаны с помощью конечного автомата. А конечный автомат мудрости - finite-state machine (FSM) был задан в виде таблицы переходов, в TSV формате:

0	0	появился на свет
0	1	ходит в детсадик	age>=3 and desire_to_learn
1	2	ученик начальной школы	age>=7 and exams is null and desire_to_learn
2	3	ученик средней школы	age>=11 and exams is null and desire_to_learn
3	4	основное общее образование	age>=15 and exams='выпускные экзамены в 9 классе'
4	5	ученик старших классов	age>=15 and desire_to_learn
5	6	среднее общее образование	age>=17 and exams='выпускные экзамены в 11 классе'
4	7	учащийся техникума	age>=16 and exams='вступительные экзамены в техникум' and desire_to_learn
7	8	среднее профессиональное образование	age>=18 and exams='защита диплома'
6	9	учащийся института	age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn
8	9	учащийся института	age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn
9	10	оконченное высшее образование	age>=22 and exams='защита диплома' and desire_to_learn
10	11	аспирант	age>=22  and exams='экзамены в аспирантуру' and desire_to_learn
11	12	претендент на ученую степень	age>=24 and exams='кандидатский минимум' and desire_to_learn
12	13	кандидат наук	age>=25 and exams='защита диссертации' and desire_to_learn
4	14	больше не обучается	not(desire_to_learn)
6	14	больше не обучается	not(desire_to_learn)
8	14	больше не обучается	not(desire_to_learn)
10	14	больше не обучается	not(desire_to_learn)
13	14	больше не обучается	not(desire_to_learn)

И записали летописцы его житие в виде таблицы в tab separated value формате и загрузили в PostgreSQL:

# CREATE TABLE life( name text, age int, desire_to_learn boolean, exams text, CONSTRAINT pk_life PRIMARY KEY (name,age));

# \d life
                    Table "public.life"
     Column      |  Type   | Collation | Nullable | Default 
-----------------+---------+-----------+----------+---------
 name            | text    |           | not null | 
 age             | integer |           | not null | 
 desire_to_learn | boolean |           |          | 
 exams           | text    |           |          | 
Indexes:
    "pk_life" PRIMARY KEY, btree (name, age)

# \copy life from 'state_machine_example/src/main/resources/life.txt';

# select * from life;              
 name | age | desire_to_learn |             exams              
------+-----+-----------------+--------------------------------
 Вася |   1 | t               | 
 Вася |   2 | t               | 
 Вася |   3 | t               | 
 Вася |   4 | t               | 
 Вася |   5 | t               | 
 Вася |   6 | t               | 
 Вася |   7 | t               | 
 Вася |   8 | t               | 
 Вася |   9 | t               | 
 Вася |  10 | t               | 
 Вася |  11 | t               | 
 Вася |  12 | t               | 
 Вася |  13 | t               | 
 Вася |  14 | t               | 
 Вася |  15 | t               | выпускные экзамены в 9 классе
 Вася |  16 | t               | 
 Вася |  17 | t               | выпускные экзамены в 11 классе
 Вася |  18 | t               | вступительные экзамены в ВУЗ
 Вася |  19 | t               | 
 Вася |  20 | t               | 
 Вася |  21 | t               | 
 Вася |  22 | t               | защита диплома
 Вася |  23 | t               | 
 Вася |  24 | t               | 
 Вася |  25 | t               | 
 Вася |  26 | t               | 
 Вася |  27 | f               | 
 Вася |  28 | f               | 
 Вася |  29 | f               | 
 Вася |  30 | f               | 

Было бы любопытно посмотреть, как выглядит автомат state diagram в UML. Для этого напишу небольшую программу на Java:

package com.github.isuhorukov.statemachine;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StateMachineGenerator {
  
    public static void main(String[] args) throws Exception{
        Set<String> states;
        List<String> transitions;
        String stateMachineSource = StateMachineGenerator2.class.getResource("/education.statemachine").getFile();
        try (Stream<String> lines =  Files.lines(Paths.get(stateMachineSource))){
            List<DualValue> parsingValue = lines.map(line -> { //A   B   STATE_NAME   TRANSITION_RULE
                String[] parts = line.split("\t");
                String stateValue = "state \"" + parts[2] + "\" as state" + parts[1];
                if (parts.length < 4) {
                    return new DualValue(stateValue, null);
                }
                final int state = Integer.parseInt(parts[0]);
                String transitionValue = "state" + parts[0] + " --> state" + parts[1] + " : " + parts[3];
                return new DualValue(stateValue, transitionValue);
            }).collect(Collectors.toList());
            states = parsingValue.stream().map(dualValue -> dualValue.state).collect(Collectors.toSet());
            transitions = parsingValue.stream().map(dualValue -> dualValue.transition).
                    filter(Objects::nonNull).collect(Collectors.toList());
        }
        System.out.println("@startuml\n" + String.join("\n", states) + "\n" +
                                            String.join("\n", transitions) + "\n@enduml");
    }
    private static class DualValue {
        String state;
        String transition;
        String calculatedExpression;

        public DualValue(String state, String transition) {
            this.state = state;
            this.transition = transition;
        }
    }
}

С помощью этой программы я получил текст в формате PlantUML для "конечного автомата мудрости".

@startuml
state "ученик старших классов" as state5
state "ученик средней школы" as state3
state "основное общее образование" as state4
state "среднее общее образование" as state6
state "аспирант" as state11
state "появился на свет" as state0
state "претендент на ученую степень" as state12
state "среднее профессиональное образование" as state8
state "учащийся института" as state9
state "кандидат наук" as state13
state "больше не обучается" as state14
state "ходит в детсадик" as state1
state "оконченное высшее образование" as state10
state "ученик начальной школы" as state2
state "учащийся техникума" as state7
state0 --> state1 : age>=3 and desire_to_learn
state1 --> state2 : age>=7 and exams is null and desire_to_learn
state2 --> state3 : age>=11 and exams is null and desire_to_learn
state3 --> state4 : age>=15 and exams='выпускные экзамены в 9 классе'
state4 --> state5 : age>=15 and desire_to_learn
state5 --> state6 : age>=17 and exams='выпускные экзамены в 11 классе'
state4 --> state7 : age>=16 and exams='вступительные экзамены в техникум' and desire_to_learn
state7 --> state8 : age>=18 and exams='защита диплома'
state6 --> state9 : age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn
state8 --> state9 : age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn
state9 --> state10 : age>=22 and exams='защита диплома' and desire_to_learn
state10 --> state11 : age>=22  and exams='экзамены в аспирантуру' and desire_to_learn
state11 --> state12 : age>=24 and exams='кандидатский минимум' and desire_to_learn
state12 --> state13 : age>=25 and exams='защита диссертации' and desire_to_learn
state4 --> state14 : not(desire_to_learn)
state6 --> state14 : not(desire_to_learn)
state8 --> state14 : not(desire_to_learn)
state10 --> state14 : not(desire_to_learn)
state13 --> state14 : not(desire_to_learn)
@enduml

Это описание легко превращается в графическое предстваление с помощью плагина визуализации:

И путем кодогенерации из того же "конечного автомата мудрости" получил текст функции конечного автомата на SQL для PostgreSQL:

CREATE OR REPLACE FUNCTION fsm_transition(
  state smallint,
  transition hstore
) RETURNS smallint AS $$
  select CASE
        WHEN state=0 THEN --null
       	CASE
       		WHEN transition->'st0_1'='true' THEN 1 --age>=3 and desire_to_learn
       		ELSE state
       	END
        WHEN state=1 THEN --ходит в детсадик
       	CASE
       		WHEN transition->'st1_2'='true' THEN 2 --age>=7 and exams is null and desire_to_learn
       		ELSE state
       	END
        WHEN state=2 THEN --ученик начальной школы
       	CASE
       		WHEN transition->'st2_3'='true' THEN 3 --age>=11 and exams is null and desire_to_learn
       		ELSE state
       	END
        WHEN state=3 THEN --ученик средней школы
       	CASE
       		WHEN transition->'st3_4'='true' THEN 4 --age>=15 and exams='выпускные экзамены в 9 классе'
       		ELSE state
       	END
        WHEN state=4 THEN --основное общее образование
       	CASE
       		WHEN transition->'st4_5'='true' THEN 5 --age>=15 and desire_to_learn
       		WHEN transition->'st4_7'='true' THEN 7 --age>=16 and exams='вступительные экзамены в техникум' and desire_to_learn
       		WHEN transition->'st4_14'='true' THEN 14 --not(desire_to_learn)
       		ELSE state
       	END
        WHEN state=5 THEN --ученик старших классов
       	CASE
       		WHEN transition->'st5_6'='true' THEN 6 --age>=17 and exams='выпускные экзамены в 11 классе'
       		ELSE state
       	END
        WHEN state=6 THEN --среднее общее образование
       	CASE
       		WHEN transition->'st6_9'='true' THEN 9 --age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn
       		WHEN transition->'st6_14'='true' THEN 14 --not(desire_to_learn)
       		ELSE state
       	END
        WHEN state=7 THEN --учащийся техникума
       	CASE
       		WHEN transition->'st7_8'='true' THEN 8 --age>=18 and exams='защита диплома'
       		ELSE state
       	END
        WHEN state=8 THEN --среднее профессиональное образование
       	CASE
       		WHEN transition->'st8_9'='true' THEN 9 --age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn
       		WHEN transition->'st8_14'='true' THEN 14 --not(desire_to_learn)
       		ELSE state
       	END
        WHEN state=9 THEN --учащийся института
       	CASE
       		WHEN transition->'st9_10'='true' THEN 10 --age>=22 and exams='защита диплома' and desire_to_learn
       		ELSE state
       	END
        WHEN state=10 THEN --оконченное высшее образование
       	CASE
       		WHEN transition->'st10_11'='true' THEN 11 --age>=22  and exams='экзамены в аспирантуру' and desire_to_learn
       		WHEN transition->'st10_14'='true' THEN 14 --not(desire_to_learn)
       		ELSE state
       	END
        WHEN state=11 THEN --аспирант
       	CASE
       		WHEN transition->'st11_12'='true' THEN 12 --age>=24 and exams='кандидатский минимум' and desire_to_learn
       		ELSE state
       	END
        WHEN state=12 THEN --претендент на ученую степень
       	CASE
       		WHEN transition->'st12_13'='true' THEN 13 --age>=25 and exams='защита диссертации' and desire_to_learn
       		ELSE state
       	END
        WHEN state=13 THEN --кандидат наук
       	CASE
       		WHEN transition->'st13_14'='true' THEN 14 --not(desire_to_learn)
       		ELSE state
       	END
       	ELSE state
END
$$ LANGUAGE sql;

Вот этот сгенерированный код и будет выполнять основную работу по определению состояния конечного автомата для каждого года обучения Василия. Но для того чтобы превратить функцию выше в агрегатную, понадобится обвязка из еще двух функций для PostgreSQL. Разобраться как их разрабатывать мне помогла публикация "Пользовательские агрегатные и оконные функции в PostgreSQL и Oracle".

CREATE OR REPLACE FUNCTION fsm_final( state smallint) RETURNS smallint AS $$ select state $$ LANGUAGE sql;

CREATE OR REPLACE AGGREGATE fsm(transition hstore) (
  sfunc     = fsm_transition,
  stype     = smallint,
  finalfunc = fsm_final,
  initcond  = '0'
);

Путем кодогенерации, я также получил вызов функции FSM с параметрами, определяющими переход между состояниями:

String query = "fsm(hstore(ARRAY[" + String.join(", ", calculated) + "]))";
fsm(hstore(ARRAY['st0_1', (age>=3 and desire_to_learn)::text , 
                                        'st1_2', (age>=7 and exams is null and desire_to_learn)::text , 
                                        'st2_3', (age>=11 and exams is null and desire_to_learn)::text , 
                                        'st3_4', (age>=15 and exams='выпускные экзамены в 9 классе')::text , 
                                        'st4_5', (age>=15 and desire_to_learn)::text , 
                                        'st5_6', (age>=17 and exams='выпускные экзамены в 11 классе')::text , 
                                        'st4_7', (age>=16 and exams='вступительные экзамены в техникум' and desire_to_learn)::text , 
                                        'st7_8', (age>=18 and exams='защита диплома')::text , 
                                        'st6_9', (age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn)::text , 
                                        'st8_9', (age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn)::text , 
                                        'st9_10', (age>=22 and exams='защита диплома' and desire_to_learn)::text , 
                                        'st10_11', (age>=22  and exams='экзамены в аспирантуру' and desire_to_learn)::text , 
                                        'st11_12', (age>=24 and exams='кандидатский минимум' and desire_to_learn)::text , 
                                        'st12_13', (age>=25 and exams='защита диссертации' and desire_to_learn)::text , 
                                        'st4_14', (not(desire_to_learn))::text , 
                                        'st6_14', (not(desire_to_learn))::text , 
                                        'st8_14', (not(desire_to_learn))::text , 
                                        'st10_14', (not(desire_to_learn))::text , 
                                        'st13_14', (not(desire_to_learn))::text ]))

Конечно было бы удобнее не формировать этот монструозный параметр ключ-значение, а иметь доступ в теле функции ко всей строке(без хардкода типа параметра).

Также сгенерировал словарь для расшифровки числового представления состояния в строковое:

String statesSQL = "(VALUES "+stateName.entrySet().stream().map(stateNameEntry -> "("
                + stateNameEntry.getKey() + ", '"
                + stateNameEntry.getValue() + "')").collect(Collectors.joining(", "))
                + ") AS state_name(state, name)";

Объединив эти части в один запрос мы дешифруем учебу Василия по годам на основе конечного автомата:

# SELECT life_alias.name,life_alias.age,life_alias.desire_to_learn,life_alias.exams, state_name.name state FROM 
            (SELECT *, fsm(hstore(ARRAY['st0_1', (age>=3 and desire_to_learn)::text , 
                                        'st1_2', (age>=7 and exams is null and desire_to_learn)::text , 
                                        'st2_3', (age>=11 and exams is null and desire_to_learn)::text , 
                                        'st3_4', (age>=15 and exams='выпускные экзамены в 9 классе')::text , 
                                        'st4_5', (age>=15 and desire_to_learn)::text , 
                                        'st5_6', (age>=17 and exams='выпускные экзамены в 11 классе')::text , 
                                        'st4_7', (age>=16 and exams='вступительные экзамены в техникум' and desire_to_learn)::text , 
                                        'st7_8', (age>=18 and exams='защита диплома')::text , 
                                        'st6_9', (age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn)::text , 
                                        'st8_9', (age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn)::text , 
                                        'st9_10', (age>=22 and exams='защита диплома' and desire_to_learn)::text , 
                                        'st10_11', (age>=22  and exams='экзамены в аспирантуру' and desire_to_learn)::text , 
                                        'st11_12', (age>=24 and exams='кандидатский минимум' and desire_to_learn)::text , 
                                        'st12_13', (age>=25 and exams='защита диссертации' and desire_to_learn)::text , 
                                        'st4_14', (not(desire_to_learn))::text , 
                                        'st6_14', (not(desire_to_learn))::text , 
                                        'st8_14', (not(desire_to_learn))::text , 
                                        'st10_14', (not(desire_to_learn))::text , 
                                        'st13_14', (not(desire_to_learn))::text ]))              
                                         OVER (PARTITION BY name ORDER BY age) FROM life ORDER BY name,age) life_alias 
INNER JOIN 
( VALUES (0, 'появился на свет'), (1, 'ходит в детсадик'), (2, 'ученик начальной школы'), (3, 'ученик средней школы'), 
         (4, 'основное общее образование'), (5, 'ученик старших классов'), (6, 'среднее общее образование'), 
         (7, 'учащийся техникума'), (8, 'среднее профессиональное образование'), (9, 'учащийся института'), 
         (10, 'оконченное высшее образование'), (11, 'аспирант'), (12, 'претендент на ученую степень'), 
         (13, 'кандидат наук'), (14, 'больше не обучается')) AS state_name(state, name) 
ON state_name.state=life_alias.fsm;
 name | age | desire_to_learn |             exams              |             state             
------+-----+-----------------+--------------------------------+-------------------------------
 Вася |   1 | t               |                                | появился на свет
 Вася |   2 | t               |                                | появился на свет
 Вася |   3 | t               |                                | ходит в детсадик
 Вася |   4 | t               |                                | ходит в детсадик
 Вася |   5 | t               |                                | ходит в детсадик
 Вася |   6 | t               |                                | ходит в детсадик
 Вася |   7 | t               |                                | ученик начальной школы
 Вася |   8 | t               |                                | ученик начальной школы
 Вася |   9 | t               |                                | ученик начальной школы
 Вася |  10 | t               |                                | ученик начальной школы
 Вася |  11 | t               |                                | ученик средней школы
 Вася |  12 | t               |                                | ученик средней школы
 Вася |  13 | t               |                                | ученик средней школы
 Вася |  14 | t               |                                | ученик средней школы
 Вася |  15 | t               | выпускные экзамены в 9 классе  | основное общее образование
 Вася |  16 | t               |                                | ученик старших классов
 Вася |  17 | t               | выпускные экзамены в 11 классе | среднее общее образование
 Вася |  18 | t               | вступительные экзамены в ВУЗ   | учащийся института
 Вася |  19 | t               |                                | учащийся института
 Вася |  20 | t               |                                | учащийся института
 Вася |  21 | t               |                                | учащийся института
 Вася |  22 | t               | защита диплома                 | оконченное высшее образование
 Вася |  23 | t               |                                | оконченное высшее образование
 Вася |  24 | t               |                                | оконченное высшее образование
 Вася |  25 | t               |                                | оконченное высшее образование
 Вася |  26 | t               |                                | оконченное высшее образование
 Вася |  27 | f               |                                | больше не обучается
 Вася |  28 | f               |                                | больше не обучается
 Вася |  29 | f               |                                | больше не обучается
 Вася |  30 | f               |                                | больше не обучается
(30 rows)

Time: 13,711 ms

Выразительность этого метода в том, что можно его применять как в оконных функциях, так и в обычных агрегациях данных.

Для всего набора данных:

# SELECT life_alias.name, state_name.name state FROM 
            (SELECT name, fsm(hstore(ARRAY['st0_1', (age>=3 and desire_to_learn)::text , 
                                        'st1_2', (age>=7 and exams is null and desire_to_learn)::text , 
                                        'st2_3', (age>=11 and exams is null and desire_to_learn)::text , 
                                        'st3_4', (age>=15 and exams='выпускные экзамены в 9 классе')::text , 
                                        'st4_5', (age>=15 and desire_to_learn)::text , 
                                        'st5_6', (age>=17 and exams='выпускные экзамены в 11 классе')::text , 
                                        'st4_7', (age>=16 and exams='вступительные экзамены в техникум' and desire_to_learn)::text , 
                                        'st7_8', (age>=18 and exams='защита диплома')::text , 
                                        'st6_9', (age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn)::text , 
                                        'st8_9', (age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn)::text , 
                                        'st9_10', (age>=22 and exams='защита диплома' and desire_to_learn)::text , 
                                        'st10_11', (age>=22  and exams='экзамены в аспирантуру' and desire_to_learn)::text , 
                                        'st11_12', (age>=24 and exams='кандидатский минимум' and desire_to_learn)::text , 
                                        'st12_13', (age>=25 and exams='защита диссертации' and desire_to_learn)::text , 
                                        'st4_14', (not(desire_to_learn))::text , 
                                        'st6_14', (not(desire_to_learn))::text , 
                                        'st8_14', (not(desire_to_learn))::text , 
                                        'st10_14', (not(desire_to_learn))::text , 
                                        'st13_14', (not(desire_to_learn))::text ])  ORDER BY age)
                                         FROM life  GROUP BY name) life_alias 
INNER JOIN 
( VALUES (0, 'появился на свет'), (1, 'ходит в детсадик'), (2, 'ученик начальной школы'), (3, 'ученик средней школы'), 
         (4, 'основное общее образование'), (5, 'ученик старших классов'), (6, 'среднее общее образование'), 
         (7, 'учащийся техникума'), (8, 'среднее профессиональное образование'), (9, 'учащийся института'), 
         (10, 'оконченное высшее образование'), (11, 'аспирант'), (12, 'претендент на ученую степень'), 
         (13, 'кандидат наук'), (14, 'больше не обучается')) AS state_name(state, name) 
ON state_name.state=life_alias.fsm;  

 name |        state        
------+---------------------
 Вася | больше не обучается
(1 row)

Только для строчек где age<21:

# SELECT life_alias.name, state_name.name state FROM 
            (SELECT name, fsm(hstore(ARRAY['st0_1', (age>=3 and desire_to_learn)::text , 
                                        'st1_2', (age>=7 and exams is null and desire_to_learn)::text , 
                                        'st2_3', (age>=11 and exams is null and desire_to_learn)::text , 
                                        'st3_4', (age>=15 and exams='выпускные экзамены в 9 классе')::text , 
                                        'st4_5', (age>=15 and desire_to_learn)::text , 
                                        'st5_6', (age>=17 and exams='выпускные экзамены в 11 классе')::text , 
                                        'st4_7', (age>=16 and exams='вступительные экзамены в техникум' and desire_to_learn)::text , 
                                        'st7_8', (age>=18 and exams='защита диплома')::text , 
                                        'st6_9', (age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn)::text , 
                                        'st8_9', (age>=18 and exams='вступительные экзамены в ВУЗ' and desire_to_learn)::text , 
                                        'st9_10', (age>=22 and exams='защита диплома' and desire_to_learn)::text , 
                                        'st10_11', (age>=22  and exams='экзамены в аспирантуру' and desire_to_learn)::text , 
                                        'st11_12', (age>=24 and exams='кандидатский минимум' and desire_to_learn)::text , 
                                        'st12_13', (age>=25 and exams='защита диссертации' and desire_to_learn)::text , 
                                        'st4_14', (not(desire_to_learn))::text , 
                                        'st6_14', (not(desire_to_learn))::text , 
                                        'st8_14', (not(desire_to_learn))::text , 
                                        'st10_14', (not(desire_to_learn))::text , 
                                        'st13_14', (not(desire_to_learn))::text ])  ORDER BY age)
                                         FROM life where age<21 GROUP BY name) life_alias 
INNER JOIN 
( VALUES (0, 'появился на свет'), (1, 'ходит в детсадик'), (2, 'ученик начальной школы'), (3, 'ученик средней школы'), 
         (4, 'основное общее образование'), (5, 'ученик старших классов'), (6, 'среднее общее образование'), 
         (7, 'учащийся техникума'), (8, 'среднее профессиональное образование'), (9, 'учащийся института'), 
         (10, 'оконченное высшее образование'), (11, 'аспирант'), (12, 'претендент на ученую степень'), 
         (13, 'кандидат наук'), (14, 'больше не обучается')) AS state_name(state, name) 
ON state_name.state=life_alias.fsm;  

 name |       state        
------+--------------------
 Вася | учащийся института
(1 row)

Жаль, что в ClickHouse, эту задачу не нашел как вообще можно решить без executable/Python на чистом SQL, хоть пусть и автосгенерированном. Без длительной разработки на C++ для одной из самых популярных open source аналитических баз данных!

Ничего другого в БД, способного хранить состояние между вызовами и выполнять вычисление состояний по условиям, мне в голову пока не пришло. Возможно, есть способ записать код компактнее. Хотел бы спросить совет@erogov и коллег: может есть более простой способ и без использования агрегатных функций и кодогенерации?

В результате, получил еще один инструмент для разметки набора данных "не выходя" из базы данных. Может быть полезно аналитикам, дата саенс, в бизнес-отчетности и разметке бизнес-процессов на основе конечного автомата и перехода между состояниями, описанными в виде логических выражений на SQL.

Источник: https://habr.com/ru/articles/728196/


Интересные статьи

Интересные статьи

Моя GTD система в Notion стала очень умной, большой и неудобной. В прошлой статье я писал, о том какие в ней крутые инструменты декомпозиции и прочие достоинства, но вместе с тем, чем больше обязатель...
В XXI веке в западных странах формируется порочная мода держать обезьян в качестве домашних любимцев. По одной из оценок «National Geographic», таких животных около 5000 в одной только Великобритании....
Вы узнаете, как создавался PowerPoint, какие возможности были в ранних версиях программы и почему сервис Keynote стал ее первым серьезным конкурентом. Мы уже рассказали вам о пер...
В наши дни в интернете огромное количество историй самых разных стартапов, особенно зарубежных. Часто их довольно интересно читать, но не приукрашенных рассказов об опыте создания проектов в Руне...
Enemies of Reality by 12f-2 В конце апреля, пока белые ходоки осаждали Винтерфелл, у нас произошло кое-что поинтереснее, мы сделали не совсем обычную выкатку. В принципе мы постоянно катим н...