Kingbase ES custom aggregate function and one-time rewrite case

Article summary:

The SQL syntax of KES is temporarily not compatible with the creation syntax and process of Oracle’s custom aggregate function, but it can be rewritten using the syntax that KES already supports.
This article organizes and briefly analyzes the principles of custom aggregate functions and interprets the sample code.
And rewritten according to customer code.

1. A brief analysis of oracle custom aggregate functions

Oracle’s custom aggregate function needs to implement four ODCIAggregate interface functions, which are declared and defined in an object type.
These functions define the operations that need to be implemented within any aggregate function.
These functions are initialization, iteration, merging (called when parallel aggregation enable parallel is defined) and termination. The following figure is the processing flow of a custom aggregate function:

1.Parallel process of custom aggregate function:

|--------------|
               | |
              \|/ |
ODCIAggregateInitialize -------> ODCIAggregateIterate
                        |
                        |
                        | ----->ODCIAGGREGATEMERGE ----->ODCIAGGREGATETERMINATE
                        |
               |-------------|
               | |
              \|/ |
ODCIAggregateInitialize -------> ODCIAggregateIterate

Serial process of custom aggregate function (ODCIAGGREGATEMERGE will not be called in actual measurement):

|--------------|
               | |
              \|/ |
ODCIAggregateInitialize-------> ODCIAggregateIterate -----> ODCIAggregateTERMINATE

As can be seen from the above, ODCIAGGREGATEMERGE will be called in parallel, but ODCIAGGREGATEMERGE will not be called in non-parallel. 

2. Instance analysis of oracle custom aggregate function

Take as an example the calculation of the second most valuable case

Interface definition:

create type SecondMaxImpl as object
(
     --Customize the maximum value saved
     max NUMBER,
     --Customize to save the second largest value
     secmax NUMBER,
     --Initialization function, a method that must be implemented to initialize the context environment at the beginning of the aggregation operation
     static function ODCIAggregateInitialize(sctx IN OUT SecondMaxImpl) return number,
     --Iteration operation function, Oracle performs iteration operation based on this function,
     --The first parameter self is the context of the aggregation operation,
     --The second parameter value is the current value that needs to be processed, which can be number varchar2 and other types.
     --During the iteration, if the current value is null, ignore the iteration
     member function ODCIAggregateIterate(self IN OUT SecondMaxImpl, value IN number) return number,
     --(Oracle will choose to perform this step) This function is used to merge two contexts into one context, and may play a role in both parallel and serial environments.
     member function ODCIAggregateMerge(self IN OUT SecondMaxImpl, ctx2 IN SecondMaxImpl) return number,
     --This function is run in the last step of the aggregation operation and is used to process the results and return the processing results.
     --The first parameter self is the context,
     --The second parameter returnValue is the return value, which can be number, varchar2, etc.
     --The third parameter flags is the identification bit
     member function ODCIAggregateTerminate(self IN SecondMaxImpl, returnValue OUT number, flags IN number) return number
);
/

–Definition and interface implementation of aggregate functions

create or replace type body SecondMaxImpl is
static function ODCIAggregateInitialize(sctx IN OUT SecondMaxImpl)
return number is
begin
   sctx := SecondMaxImpl(0, 0); --Initialization
   return ODCIConst.Success;
end;

member function ODCIAggregateIterate(
    self IN OUT SecondMaxImpl,
    value IN number
)
return number
is
begin
 if value > self.max then
    self.secmax := self.max;
    self.max := value;
 elsif value > self.secmax then
    self.secmax := value;
 end if;
 return ODCIConst.Success;
end;

member function ODCIAggregateMerge(
    self IN OUT SecondMaxImpl, --Merge two contexts (useful for parallel aggregation. Can it only be used for parallel aggregation?)
    ctx2 IN SecondMaxImpl
)
return number
is
begin
   if ctx2.max > self.max then
       if ctx2.secmax > self.secmax then
           self.secmax := ctx2.secmax;
       else
           self.secmax := self.max;
       end if;
       self.max := ctx2.max;
   elsif ctx2.max > self.secmax then
       self.secmax := ctx2.max;
   end if;
   return ODCIConst.Success;
end;

member function ODCIAggregateTerminate(
    self IN SecondMaxImpl,
    returnValue OUT number,
    flags IN number
)
return number is
begin
 returnValue := self.secmax; --Get the second largest value
 --returnValue := self.max; --Get the maximum value
 return ODCIConst.Success;
end;
end;
/
---Create aggregate functions
CREATE FUNCTION SecondMax (input NUMBER) RETURN NUMBER
PARALLEL_ENABLE AGGREGATE USING SecondMaxImpl;

–Construct test data to see the running effect

create table tt02(
    id int ,
    namevarchar(20),
    sal number(6,2)
);

insert into tt02 values(12,'aaaa',1253.2);
insert into tt02 values(14,'aass',1965.3);
insert into tt02 values(13,'vvvv',2056.6);
insert into tt02 values(16,'aass',6562.1);
insert into tt02 values(15,'aass',4563.4);
insert into tt02 values(12,'aaaa',2532.2);
insert into tt02 values(14,'aass',1965.3);
insert into tt02 values(13,'vvvv',6556.6);
insert into tt02 values(16,'aass',8965.1);
insert into tt02 values(15,'aass',7854.4);

–Test verification (in line with expectations)

SQL> select id,SecondMax(sal) from tt02 group by id;

    ID SECONDMAX(SAL)
---------- --------------
    12 1253.2
    13 2056.6
    14 1965.3
    16 6562.1

SQL> select /* + parallel */ id,SecondMax(sal) from tt02 group by id;

    ID SECONDMAX(SAL)
---------- --------------
    12 1253.2
    13 2056.6
    14 1965.3
    16 6562.1

–Regenerate data

delete from tt02;

declare
    vv1 int;
    vv2 int;
    vv3 varchar2(20);
begin
    for i in 1..10 loop
        select abs(mod(dbms_random.random,10000)) into vv1 from dual;
        select abs(mod(dbms_random.random,5)) into vv2 from dual;
        select dbms_random.string('u',5) into vv3 from dual;
        insert into tt02 values(vv2,vv3,vv1);
    end loop;
end;

SQL> select * from tt02;

    ID NAME SAL
-------------------------------------------------- ----------
     3 IJTIJ 5978
     4 JUAWT 2087
     0 FPBMN 2218
     0 AMBNZ 618
     2 VFXJD 37
     4 PEXGM 7983
     2 CDQJT 877
     2ENQFX 3359
     2 ICJFI 1220
     0 XCGJX 1397

10 rows selected.

–Second verification (in line with expectations)

SQL> select id,SecondMax(sal) from tt02 group by id;

    ID SECONDMAX(SAL)
---------- --------------
     0 1397
     2 1220
     3 0
     4 2087

SQL> select /* + parallel */ id,SecondMax(sal) from tt02 group by id;

    ID SECONDMAX(SAL)
---------- --------------
     0 1397
     2 1220
     3 0
     4 2087

In fact, as far as rewriting is concerned, we already know the function of this function.

Second, KES syntax rewrites Oracle’s custom aggregate function

1, Oracle’s custom aggregate function definition and its analysis and verification

Oracle’s custom aggregate function sumc2 is as follows. Let’s first perform a simple analysis and functional verification on it:

--Object type header (interface declaration)
CREATE OR REPLACE EDITIONABLE TYPE "TYPESUMVARCHAR2" as object (
  vsum VARCHAR2(4000),
  --Customize the initialization settings of the aggregate function, start an aggregate function from here
  static function ODCIAggregateInitialize(sctx IN OUT TYPESUMVARCHAR2) return number,
  --Customize the aggregate function, the most important step. This function defines what our aggregate function does specifically. In the following example, it is to take the maximum value, minimum value, average value, or perform a connection operation. self is the current aggregate function Pointer, used to associate with previous calculation results
  member function ODCIAggregateIterate(self IN OUT TYPESUMVARCHAR2,value IN varchar2) return number,
  -- Terminate the processing of the aggregate function and return the result of the aggregate function processing
  member function ODCIAggregateTerminate(self IN TYPESUMVARCHAR2,returnValue OUT VARCHAR2, flags IN number) return number,
  --Used to merge the results corresponding to two different pointers of two aggregate functions. Users can merge data with different results, especially when processing parallel query aggregate functions.
  member function ODCIAggregateMerge(self IN OUT TYPESUMVARCHAR2,ctx2 IN TYPESUMVARCHAR2) return number

);

Object type body (interface definition)

CREATE OR REPLACE EDITIONABLE TYPE BODY "TYPESUMVARCHAR2" IS
    --Customize the initialization settings of the aggregate function, start an aggregate function from here
    STATIC FUNCTION ODCIAGGREGATEINITIALIZE(
        SCTX IN OUT TYPESUMVARCHAR2
    )
    RETURN NUMBER IS
    BEGIN
        SCTX := TYPESUMVARCHAR2('');
        --Initialize vsum
        RETURN ODCICONST.SUCCESS;
    END;
    --Customized aggregate function, the most important step, this function defines what our aggregate function does specifically, the following examples,
    --Whether to take the maximum value, minimum value, average value, or perform a connection operation. self is the pointer of the current aggregate function, used to associate with the previous calculation results
    MEMBER FUNCTION ODCIAGGREGATEITERATE(
        SELF IN OUT TYPESUMVARCHAR2,
        VALUE IN VARCHAR2
    ) RETURN NUMBER IS
    BEGIN
        --SELF.vsum := substr(SELF.vsum || ',' || VALUE,0,2000);
        IF instr(
            SELF.vsum,
            value
        ) = 0
        OR SELF.vsum IS NULL THEN
        SELF.vsum := substr(
            SELF.vsum || ',' || VALUE,
            0,
            2000
        ) ;
        ELSE
        SELF.vsum := substr(
            SELF.vsum || '',
            0,
            2000
        ) ;
        END IF ;
        RETURN ODCICONST.SUCCESS;
    END;
    -- Terminate the processing of the aggregate function and return the result of the aggregate function processing (the code is intercepted)
    MEMBER FUNCTIONODCIAGGREGATETERMINATE(
        SELF IN TYPESUMVARCHAR2,
    RETURNVALUE OUT VARCHAR2,
        FLAGS IN NUMBER
    ) RETURN NUMBER IS
    BEGIN
        RETURNVALUE := SUBSTR(
            SELF.vsum,
            2
        );

        RETURN ODCICONST.SUCCESS;
    END;
    --Used to merge the results corresponding to two different pointers of two aggregate functions. Users can merge data with different results, especially when processing parallel query aggregate functions.
    --No action is taken by default (merging is not performed)
    MEMBER FUNCTION ODCIAGGREGATEMERGE(
        SELF IN OUT TYPESUMVARCHAR2,
        CTX2 IN TYPESUMVARCHAR2
    )
    RETURN NUMBER IS
    BEGIN
        RETURN ODCICONST.SUCCESS;
    END;

END;

–Create a custom aggregate function

CREATE OR REPLACE FUNCTION sumc2(input varchar2)
RETURN varchar2
PARALLEL_ENABLE AGGREGATE
USING TYPESUMVARCHAR2;

According to the actual measurement of the above tt02 table

SQL> select * from tt02;

    ID NAME SAL
-------------------------------------------------- ----------
     3 IJTIJ 5978
     4 JUAWT 2087
     0 FPBMN 2218
     0 AMBNZ 618
     2 VFXJD 37
     4 PEXGM 7983
     2 CDQJT 877
     2ENQFX 3359
     2 ICJFI 1220
     0 XCGJX 1397

SQL> select id,sumc2(name) from tt02 group by id;

ID SUMC2(NAME)
-------------------------------------------------- ----------------------------------
0 FPBMN,XCGJX,AMBNZ
2 VFXJD,ICJFI,ENQFX,CDQJT
3 IJTIJ
4 JUAWT,PEXGM

Actual measurement of replacement table:

--teachers table is as follows:
 teacher_id | teacher_name | age | sal | gender | title | position | department
------------ + -------------- + ----- + ---------- + ----- --- + ---------- + ------------ + ------------
  10001001 | Chen Siyu | 46 | 15689.00 | Male | Special Teacher | Principal | Principal's Office
  10001002 | Wen Qiang | 44 | 29942.00 | Male | Special Teacher | Vice Principal | Principal’s Office
  10001003 | Wu Ling | 41 | 29142.00 | Female | Senior Teacher | Office Director | Office
  10001004 | Zhang Li | 41 | 28242.00 | Female | Senior Teacher | Director of Academic Affairs Office | Academic Affairs Office
  10001005 | Zhang Zhidong | 41 | 28242.00 | Male | Senior Teacher | Director of Finance Department | Finance Department
  10001006 | Xiong Haoyu | 49 | 28356.00 | Female | First-level teacher | Director of Admissions Office | Admissions Office
  10001007 | Zhu Wen | 49 | 24016.00 | Female | First-level teacher | Admissions office assistant | Admissions office
  10001008 | Zhang Zhiqiang | 49 | 23964.00 | Female | First-level teacher | Assistant to Finance Department | Finance Department
  10001009 | Zhu Guobin | 49 | 21974.00 | Male | Second level teacher | Assistant to Finance Department | Finance Department

oracle test results

select age,sumc2(title) from teachers group by age;
41 senior teachers
44 Special Teacher
46 Special Teacher
49 first-level teacher, second-level teacher

Through the actual measurement of the code and actual effects, the aggregation function sumc2 is used to remove duplication and obtain the splicing result after deduplication.
After understanding the customer’s native code and Oracle’s custom aggregation functions,

2, Basic principles of aggregate functions supported by KES

The syntax of aggregate functions (PLPGSQL) supported by KES is as follows:

CREATE AGGREGATE name ( [ argmode ] [ argname ] arg_data_type [ , ... ] ) (
  SFUNC = sfunc, ---iteration function, each row of data iteratively calls the calculation result
  STYPE = state_data_type ----The data type returned by the aggregate function
  [ , SSPACE = state_data_size ]
  [ , FINALFUNC = ffunc ] ----The final calculation function of each group, optional
  [ , FINALFUNC_EXTRA ]
  [, COMBINEFUNC = combinefunc] ---Aggregation COMBINEFUNC function, after enabling it, PARALLEL can be enabled in parallel
  [ , SERIALFUNC = serialfunc ]
  [ , DESERIALFUNC = deserialfunc ]
  [ , INITCOND = initial_condition ] ---INITCOND is the value passed to the first parameter when calling SFUNC for the first time and does not need to be written.
  [ , MSFUNC = msfunc ]
  [ , MINVFUNC = minvfunc ]
  [ , MSTYPE = mstate_data_type ]
  [ , MSSPACE = mstate_data_size ]
  [ , MFINALFUNC = mffunc ]
  [ , MFINALFUNC_EXTRA ]
  [ , MINITCOND = minimal_condition ]
  [ , SORTOP = sort_operator ]
  [ , PARALLEL = { SAFE | RESTRICTED | UNSAFE } ] ----SAFE parallel aggregation
)

The key points to mention are:

The aggregate function is calculated independently for each group. For example, based on the age aggregation (GROUP BY age) of the above teachers table, it will be divided into 4 groups, and the 4 groups will be calculated internally.

--teachers table is as follows:
 teacher_id | teacher_name | age | sal | gender | title | position | department
------------ + -------------- + ----- + ---------- + ----- --- + ---------- + ------------ + ------------
  10001001 | Chen Siyu | 46 | 15689.00 | Male | Special Teacher | Principal | Principal's Office
  10001002 | Wen Qiang | 44 | 29942.00 | Male | Special Teacher | Vice Principal | Principal’s Office
  10001003 | Wu Ling | 41 | 29142.00 | Female | Senior Teacher | Office Director | Office
  10001004 | Zhang Li | 41 | 28242.00 | Female | Senior Teacher | Director of Academic Affairs Office | Academic Affairs Office
  10001005 | Zhang Zhidong | 41 | 28242.00 | Male | Senior Teacher | Director of Finance Department | Finance Department
  10001006 | Xiong Haoyu | 49 | 28356.00 | Female | First-level teacher | Director of Admissions Office | Admissions Office
  10001007 | Zhu Wen | 49 | 24016.00 | Female | First-level teacher | Admissions office assistant | Admissions office
  10001008 | Zhang Zhiqiang | 49 | 23964.00 | Female | First-level teacher | Assistant to Finance Department | Finance Department
  10001009 | Zhu Guobin | 49 | 21974.00 | Male | Second level teacher | Assistant to Finance Department | Finance Department

1), SFUNC iteration function, assuming it is customized as follows:

CREATE OR REPLACE FUNCTION YOUR_SFUNC_NAME (numeric, numeric, numeric......)
RETURNS numeric
as
begin
    .......(omitted)
end

This function is the iteration function of each row of data

Parameter 1: $1, the calculation result of the last iteration;
Parameter two: $2, the first parameter of YOUR_AGGREGATE_NAME (if the aggregate function passes in a table column, it represents the current row data)
Parameter three: $3, the second parameter of YOUR_AGGREGATE_NAME (if it is a fixed value, such as the value 2, pass in 2 each time)
Parameter four: $4, the third parameter of YOUR_AGGREGATE_NAME

………..

2), FINALFUNC final function, assuming it is customized as follows:

CREATE OR REPLACE FUNCTION YOUR_FINALFUNC_NAME (numeric) ---There is only one parameter, the SFUNC function returns numeric, so here is numeric
RETURNS numeric ---Aggregation return type, so here is numeric
as
begin
    .......(omitted)
end

After each group is calculated through the YOUR_SFUNC_NAME function, it is called one last time, which means that one final rule calculation can be performed.

3), YOUR_AGGREGATE_NAME aggregate function
The definition of the aggregate function will refer to the above-mentioned YOUR_SFUNC_NAME and YOUR_FINALFUNC_NAME functions to ultimately achieve the calculation of a specific goal.

A typical non-parallel usage example;

CREATE AGGREGATE YOUR_AGGREGATE_NAME(numeric, numeric) ---Accepts two parameters, which will be used as the last two parameters of the SFUNC function
(
        INITCOND = xxxx, ---INITCOND is the first time the YOUR_SFUNC_NAME function is called, and the value xxxx is passed to the first parameter, which does not need to be written.
        STYPE = numeric, ---The data type numeric returned by the aggregate function
        SFUNC = YOUR_SFUNC_NAME, ---Custom iteration function for each group
        FINALFUNC = YOUR_FINALFUNC_NAME --- Summary function for each group
);

After mastering the above basic syntax information, you can now write custom aggregate functions.

Referenced to this article:

Postgresql custom aggregate function entry case_pgsql custom aggregate function_Gao Mingjie’s blog-CSDN blog

DROP AGGREGATE myavg(integer);
CREATE TABLE t_taxi(trip_id int, km numeric);

insert into t_taxi values (1, 3.4);
insert into t_taxi values (1, 5.3);
insert into t_taxi values (1, 2.9);
insert into t_taxi values (2, 9.3);
insert into t_taxi values (2, 1.6);
insert into t_taxi values (2, 4.3);

 trip_id | km
--------- + -----
       1 | 3.4
       1 | 5.3
       1 | 2.9
       2 | 9.3
       2 | 1.6
       2 | 4.3
----------------

CREATE OR REPLACE FUNCTION taxi_accum (numeric, numeric, numeric)
RETURNS numeric AS
$$
BEGIN
    RAISE NOTICE 'prev:[%] curr:(%) outer:(%) return:(%)', $1, $2, $3, $1 + $2 * $3;
    RETURN $1 + $2 * $3;
END;
$$
LANGUAGE 'plpgsql';


CREATE OR REPLACE FUNCTION taxi_final (numeric)
RETURNS numeric AS
$$
BEGIN
    RAISE NOTICE 'final:(%) return:(%)', $1, round($1 + 5, -1);
    RETURN round($1 + 5, -1);
END;
$$
LANGUAGE 'plpgsql';

CREATE AGGREGATE taxi(numeric, numeric)
(
        SFUNC = taxi_accum,
        STYPE = numeric,
        FINALFUNC = taxi_final,
        INITCOND = 3.50
);


--test
test=# SELECT trip_id, taxi(km, 2.20), 3.50 + sum(km)*2.2 AS manual FROM t_taxi GROUP BY trip_id;
test-#/
NOTICE: prev:[3.50] curr:(3.4) outer:(2.20) return:(10.980)
NOTICE: prev:[10.980] curr:(5.3) outer:(2.20) return:(22.640)
NOTICE: prev:[22.640] curr:(2.9) outer:(2.20) return:(29.020)--Perform SFUNC function iterative calculation for the group with trip_id 1
NOTICE: prev:[3.50] curr:(9.3) outer:(2.20) return:(23.960)
NOTICE: prev:[23.960] curr:(1.6) outer:(2.20) return:(27.480)
NOTICE: prev:[27.480] curr:(4.3) outer:(2.20) return:(36.940)--Perform SFUNC function iterative calculation for the group with trip_id 2
NOTICE: final:(29.020) return:(30)--Finally calculate the result returned by the SFUNC function (the first value of the parameter) for the group with trip_id 1
NOTICE: final:(36.940) return:(40)--Finally calculate the result returned by the SFUNC function (the first value of the parameter) for the group with trip_id 2
 trip_id | taxi | manual
--------- + ------ + --------
       1 | 30 | 29.02
       2 | 40 | 36.94
(2 rows)

Time: 1.775 ms

--You can see that the basic aggregate function (non-parallel) process is
--1, use the SFUNC function to iteratively calculate each set of data
--2. After each set of data is calculated, use FINALFUNC to convert the SFUNC results of each set

3, KES custom aggregate function rewritten

After rewriting using plpgsql syntax (corresponding to the sumc2 function of Oracle above):

--Define the iteration function for each row of data
CREATE OR REPLACE FUNCTION agg(v1 varchar,v2 varchar)
RETURNS varchar IMMUTABLE
AS
$$
BEGIN
    if v1 is null or LENGTH(v1) = 0 then
        v1 := substr(v2, 0, 2000);
    ELSIF v2 is null and v1 is not null then
        v1 := substr(v1, 0, 2000);
    ELSIF instr( v1, v2 ) = 0 then ---If it does not contain a relationship (then splice)
        v1 := substr(v1 || ',' || v2 , 0 , 2000) ;
    else
        v1 := substr(v1, 0, 2000);
    end if ;
    return v1;
end;
$$LANGUAGE 'plpgsql';

--Define the final function for each group
CREATE OR REPLACE FUNCTION agg_final (varchar)
RETURNS varchar AS
$$
BEGIN
    RETURN $1; ---This example does not require any additional processing
END;
$$
LANGUAGE 'plpgsql';

---Define aggregate functions
CREATE OR REPLACE AGGREGATE sumc2 (varchar)
(
    SFUNC = agg, --- Custom iteration function for each group
    STYPE = varchar, ---The data type returned by the aggregate function
    FINALFUNC = agg_final, ---The final function of each group
    INITCOND ='' --INITCOND is the first time the agg function is called, passing the value of the first parameter.
 );

Verify the basic functions of the above rewritten aggregate function

kes test:

--Create table
CREATE table teachers (
  teacher_id number(8) not null,
  teacher_name varchar2 (40) not null,
  age number(2),
  sal number(8,2) default 0,
  gender varchar2(10),
  title varchar2(20),
  POSITION varchar2(20),
  department varchar2 (40) not null,
  constraint teach_ch_gender check ( gender in ('male','female')),
  constraint teach_ch_title check (title in ('Third-level teacher','Second-level teacher','First-level teacher','Senior teacher','Special teacher')),
  primary key(teacher_id,teacher_name)
);
--Create data
insert into teachers values(10001001,'Chen Siyu',46,15689.00,'male','special teacher','principal','principal's office');
insert into teachers values(10001002,'Wen Qiang',44,14971.00,'Male','Special Teacher','Vice Principal','Principal's Office');
insert into teachers values(10001003,'Wu Ling',41,14571.00,'female','senior teacher','office director','office');
insert into teachers values(10001004,'Zhang Li',41,14121.00,'female','senior teacher','director of the academic affairs office','academic affairs office');
insert into teachers values(10001005,'Zhang Zhidong',41,14121.00,'Male','Senior Teacher','Director of Finance Department','Finance Department');
insert into teachers values(10001006,'Xiong Haoyu',49,14178.00,'female','first-level teacher','admissions office director','admissions office');
insert into teachers values(10001007,'Zhu Wen',49,12008.00,'female','first-level teacher','admissions office assistant','admissions office');
insert into teachers values(10001008,'Zhang Zhiqiang',49,11982.00,'Female','First-level teacher','Finance Department Assistant','Finance Department');
insert into teachers values(10001009,'Zhu Guobin',49,10987.00,'Male','Second Teacher','Finance Department Assistant','Finance Department');

--View table
test=# select * from teachers;
test-#/

 teacher_id | teacher_name | age | sal | gender | title | position | department
------------ + -------------- + ----- + ---------- + ----- --- + ---------- + ------------ + ------------
  10001001 | Chen Siyu | 46 | 15689.00 | Male | Special Teacher | Principal | Principal's Office
  10001002 | Wen Qiang | 44 | 29942.00 | Male | Special Teacher | Vice Principal | Principal’s Office
  10001003 | Wu Ling | 41 | 29142.00 | Female | Senior Teacher | Office Director | Office
  10001004 | Zhang Li | 41 | 28242.00 | Female | Senior Teacher | Director of Academic Affairs Office | Academic Affairs Office
  10001005 | Zhang Zhidong | 41 | 28242.00 | Male | Senior Teacher | Director of Finance Department | Finance Department
  10001006 | Xiong Haoyu | 49 | 28356.00 | Female | First-level teacher | Director of Admissions Office | Admissions Office
  10001007 | Zhu Wen | 49 | 24016.00 | Female | First-level teacher | Admissions office assistant | Admissions office
  10001008 | Zhang Zhiqiang | 49 | 23964.00 | Female | First-level teacher | Assistant to Finance Department | Finance Department
  10001009 | Zhu Guobin | 49 | 21974.00 | Male | Second-level teacher | Assistant to the Finance Department | Finance Department

(9 rows)


--test
test=# select age,sumc2(title) from teachers group by age;
test-#/
 age|sumc2
----- + -------------------
  49 | First-level teacher, second-level teacher
  44 | Special Teacher
  46 | Special Teacher
  41 | Senior Teacher
(4 rows)

Time: 1.517 ms

Create a new test table and test again

drop table tt02;

create table tt02 (
  id int,
  namevarchar2(200)
);

--kes:
declare
  v int;
begin
   for i in 1..100000 loop
        select ceil(random()*10000 + 1) into v from dual;
        insert into tt02 values(v,'special teacher');
        select ceil(random()*10000 + 1) into v from dual;
        insert into tt02 values(v,'senior teacher');
   end loop;
end;

--oracle:
declare
  v int;
begin
  for i in 1..1000 loop
      select dbms_random.value(0,100000) into v from dual;
      insert into tt02 values(v,'special teacher');
      select dbms_random.value(0,100000) into v from dual;
      insert into tt02 values(v,'senior teacher');
  end loop;
end;

test=# select * from tt02;

 id|name
---- + ----------
 1 | Special Teacher
 2 |
 3 | Special Teacher
 4 | Intermediate Teacher
 5 | Special Teacher
 6 |
 7 | Special Teacher
 8 | Intermediate Teacher
(8 rows)

test=# select sumc2(name) from tt02;
test-#/

    sumc2
-------------------
 special teacher,intermediate teacher

(1 row)

Therefore, the test results are consistent with Oracle and meet basic expectations.

Note: The above code content does not consider parallel conditions, and will be explained in alignment later.