본문 바로가기

프로젝트/APM Web Server 개발

JSONB 타입 다뤄보기 - 1 (24.05.22)

목차

    PostgreSQL의 경우 다른 RDB와 달리, 특별한 타입의 데이터를 저장할 수 있다.

    Attribute로 bigint가 아니고 bigint[]의 배열 형태나, JSONB 등의 조금 더 자유로운 데이터 타입으로 저장할 수 있다는 점이다.

    이것은 실제 DB에서 JDBC를 통해 넘어오는 데이터 중 일부이다.

    [
      {
        "id": 592492460020000000,
        "scope_name": "io.opentelemetry.spring-scheduling-3.1",
        "scope_version": "2.1.0-alpha",
        "trace_id": "E'\\\\\\\\x32058E6FA0A913CCA9D567F003A0AD2A'",
        "span_id": "E'\\\\\\\\x82A02E8383274438'",
        "parent_span_id": "",
        "flag": 1,
        "name": "RealtimeTask.sendRealtime",
        "kind": 1,
        "start_time_unix_nano": 1715924920000000000,
        "end_time_unix_nano": 1715924920000870856,
        "status_is_error": false,
        "attributes": {"thread.id": "70", "thread.name": "pool-8-thread-1", "code.function": "sendRealtime", "code.namespace": "com.sysone.eumaiwacs.task.RealtimeTask"},
        "has_event": false
      }
    ]
    

    (무언가 비범한 데이터 스키마. Oracle이나 MySQL에서는 보지 못했다.)

    create table tbl_apm_trace_span
    (
        id                   BIGINT PRIMARY KEY,
        scope_name           VARCHAR,
        scope_version        VARCHAR,
        trace_id             BYTEA,
        span_id              BYTEA,
        parent_span_id       BYTEA,
        flag                 INT,
        name                 VARCHAR,
        kind                 INT,
        start_time_unix_nano BIGINT,
        end_time_unix_nano   BIGINT,
        status_is_error       boolean, --Error check, Message / Code 배제
        attributes           jsonb,
        has_event            boolean
    );
    CREATE INDEX span_span_id ON tbl_apm_trace_span (span_id);
    CREATE INDEX span_trace_id ON tbl_apm_trace_span (trace_id);
    CREATE INDEX span_parent_span_id ON tbl_apm_trace_span (parent_span_id);
    -- 복합 인덱스 들어갈 자리
    

    그 외에 Double[] 타입 등도 지원한다.

    CREATE TABLE tbl_apm_metric_histogram
    (
        id bigint PRIMARY KEY,
        agent_id BIGINT,
        scope_name varchar,
        scope_version varchar,
        name varchar,
        unit varchar,
        aggregation_temporality_value int,
        attributes jsonb,
        start_time_unix_nano bigint,
        time_unix_nano bigint,
        count int,
        sum double precision,
        bucket_counts bigint[],
        explicit_bounds double precision[],
        flag int,
        min double precision,
        max double precision,
        has_exemplar boolean
    );
    CREATE INDEX tbl_apm_metric_histogram_agent_id ON tbl_apm_metric_histogram(agent_id);
    

    사실 정규화를 강하게 집어넣는다면 이러한 타입의 데이터를 자주 쓸 일이 없을 것이다. 어쩌면 비정규화에 특화된 데이터 타입이 아닐까? 라는 생각이 든다.

    무튼, 중요한 것은 이것이다.

     

    “우리는 Spring 등에서 ‘JPA’를 사용해서 JDBC 쿼리 결과를 매핑해야 한다.”

     

    그런데, JPA에서는 배열 타입(double[], long[]..)이나, Json을 지원하지 않는다.

    @Entity
    @Table(name = "tbl_apm_metric_gauge")
    public class Gauge {
        @Id
        private long id;
        private long agentId;
        private String scopeName;
        private String scopeVersion;
        private String name;
        private String unit;
        }
     }
    

     

    우리가 아는 일반적인 Entity이다. premitive type 정도만 지원하고, 실제로 이렇게 자주 쓰인다.

    그렇기에, psql에서 제공하는 특별한 타입 데이터를 받으려면 이런 식으로 받아야 한다. 혹은 UserType을 다시 받던지..

     

    package com.sysone.apm.entity.metric;
    
    import com.sysone.apm.util.converter.JsonbToMapConverter;
    import org.hibernate.annotations.Type;
    
    import javax.persistence.*;
    import java.util.List;
    import java.util.Map;
    
    @Entity
    @Table(name = "tbl_apm_metric_histogram")
    public class Histogram {
        @Id
        private long id;
        private long agentId;
        private String scopeName;
        private String scopeVersion;
        private String name;
        private String unit;
        private int aggregationTemporalityValue;
        @Convert(converter = JsonbToMapConverter.class)
        private Map<String, String> attributes;
        private long startTimeUnixNano;
        private long timeUnixNano;
        private int count;
        private double sum;
    
        @Type(type = "com.sysone.apm.util.converter.LongArrayType")
        private List<Long> bucketCounts;
    
        @Type(type = "com.sysone.apm.util.converter.DoubleArrayType")
        private List<Double> explicitBounds;
    
        private int flag;
        private double min;
        private double max;
        private boolean hasExemplar;
    }
    
    

     

     

    JSONB 쿼리 함수 사용해보기

    select span_trace_id from tbl_apm_trace limit 1; -- 32058e6f-a0a9-13cc-a9d5-67f003a0ad2a
    select * from tbl_apm_trace_span where trace_id = (select span_trace_id from tbl_apm_trace limit 1);
    
    -------view test
    drop view tbl_apm_test_view;
    CREATE VIEW tbl_apm_test_view AS
        SELECT distinct jsonb_object_keys(attributes) AS attribute_keys
          FROM tbl_apm_trace_span;
    
    select * from tbl_apm_test_view;
    
    SELECT column_name, data_type, is_nullable, column_default
    FROM information_schema.columns
    WHERE table_name = 'tbl_apm_metric_sum';
    
    SELECT DISTINCT
        jsonb_object_keys(attributes) AS key
    FROM tbl_apm_metric_sum;
    
    -- 키 : value
    select distinct tbl_apm_metric_sum.attributes from tbl_apm_metric_sum limit 500;
    
    -- 키 : value
    select distinct attributes from tbl_apm_trace_span limit 500;
    
    -- 키 : value
    select distinct attributes from tbl_apm_metric_histogram limit 500;
    
    select distinct jsonb_object_keys(attributes) as key from tbl_apm_metric_sum;
    
    SELECT
        jsonb_each_text(attributes) AS key_value_pair
    FROM tbl_apm_metric_sum;
    
    ###
    -- 고유한 키 세트를 추출하는 쿼리 : 키셋팅만
    SELECT DISTINCT
        (SELECT array_agg(key ORDER BY key)
         FROM jsonb_each(attributes) as t(key, value)) AS key_set
    FROM
        tbl_apm_metric_sum;
    
    -- 키 : value
    select distinct tbl_apm_metric_sum.attributes from tbl_apm_metric_sum limit 500;
    
    -- histogram
    -- 고유한 키 세트를 추출하는 쿼리
    SELECT DISTINCT
        (SELECT array_agg(key ORDER BY key) --변환된 키-값 쌍에서 키를 추출하여 배열로 집계
         FROM jsonb_each(attributes) -- jsonb_each(attributes): JSONB 객체를 키-값 쌍의 행으로 변환
             as t(key, value)) AS key_set
    FROM
        tbl_apm_metric_histogram limit 10;
    
    select count(*) from tbl_apm_metric_histogram;
    select distinct attributes from tbl_apm_metric_histogram limit 500;
    
    -- 이건 단일 키임
    SELECT jsonb_each(attributes) from tbl_apm_metric_histogram limit 10;
    
    --개선
    SELECT
        id,  -- 각 행을 식별할 수 있는 기본 키 또는 고유 식별자 (필요에 따라 수정)
        array_agg(key) AS keys
    FROM (
             SELECT
                 id,  -- 각 행을 식별할 수 있는 기본 키 또는 고유 식별자 (필요에 따라 수정)
                 jsonb_object_keys(attributes) AS key
             FROM tbl_apm_metric_histogram
         ) AS keys_extracted
    GROUP BY id;
    -- LIMIT 10;
    
    --2차 개선
    SELECT
        distinct array_agg(key) AS keys
    FROM (
             SELECT
                 jsonb_object_keys(attributes) AS key,
                 row_number() OVER () AS rn
             FROM tbl_apm_metric_histogram
         ) AS keys_extracted
    GROUP BY rn
    -- LIMIT 10;
    
    

    최종

    SELECT
        distinct name, array_agg(key) AS keys
    FROM (
             SELECT
                 name,
                 jsonb_object_keys(attributes) AS key,
                 row_number() OVER (PARTITION BY name ORDER BY name) AS rn
             FROM tbl_apm_metric_sum
         ) AS keys_extracted
    GROUP BY name, rn
    LIMIT 10;
    
    

     

    최종적으로는 Jsonb String으로 변경했다.

    --개선
    SELECT
        id,  -- 각 행을 식별할 수 있는 기본 키 또는 고유 식별자 (필요에 따라 수정)
        array_agg(key) AS keys
    FROM (
             SELECT
                 id,  -- 각 행을 식별할 수 있는 기본 키 또는 고유 식별자 (필요에 따라 수정)
                 jsonb_object_keys(attributes) AS key
             FROM tbl_apm_metric_histogram
         ) AS keys_extracted
    GROUP BY id;
    
    --2차 개선
    select * from tbl_apm_metric_sum;
    SELECT
        distinct name, array_agg(key) AS keys
    FROM (
             SELECT
                 name,
                 jsonb_object_keys(attributes) AS key,
                 row_number() OVER (PARTITION BY name ORDER BY name) AS rn
             FROM tbl_apm_metric_sum
         ) AS keys_extracted
    GROUP BY name, rn
    LIMIT 10;
    
    --3차 개선 : String으로 변환 .. 14sec
    select * from tbl_apm_metric_sum;
    SELECT
        distinct name, array_to_string(array_agg(key), ',') AS keys
    FROM (
             SELECT
                 name,
                 jsonb_object_keys(attributes) AS key,
                 row_number() OVER (PARTITION BY name ORDER BY name) AS rn
             FROM tbl_apm_metric_histogram
         ) AS keys_extracted
    GROUP BY name, rn
    order by name;
    
    --4차 개선 : 최적화 .. 7sec
    WITH key_extraction AS (
        SELECT distinct
            name,
            jsonb_object_keys(attributes) AS key
        FROM tbl_apm_metric_histogram
    )
    SELECT
        name,
        array_to_string(array_agg(key), ',') AS keys
    FROM key_extraction
    GROUP BY name
    order by name;
    
    select * from tbl_apm_metric_histogram order by time_unix_nano desc limit 1;
    
    --5차 개선 : where 조건 추가 .. agentid 추가시 4초, 시간 지정시 1초 이내 들어옴
    explain analyse
    WITH key_extraction AS (
        SELECT distinct
            name,
            jsonb_object_keys(attributes) AS key
        FROM tbl_apm_metric_histogram
        WHERE agent_id = 1 and time_unix_nano between 1716388593093536000 and 1716474993093536000
    )
    SELECT
        name,
        array_to_string(array_agg(key), ',') AS keys
    FROM key_extraction
    GROUP BY name
    order by name;