데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      Data Lineage를 활용한 Data Ontology 출발

      mwkangit 25.07.14
      3,973 8 1
      DEVOTEE 요약
      본 블로그는 데이터 온톨로지와 데이터 계보(Data Lineage)를 활용해 Mart 데이터의 속성을 정의, 추적하는 작업을 소개하며 주요 개념과 방법론을 설명합니다. 데이터 온톨로지는 특정 도메인 내 개체와 관계를 형식적으로 표현한 구조이며, 데이터 계보는 데이터 생성부터 변환, 소비까지의 모든 과정을 추적하는 체계로, JsqlParser 등 도구를 활용해 Table Level Lineage를 추출하는 방법을 제안했습니다. 이를 통해 마케팅, AI 사업 등 다양한 응용 가능성을 기대하고 있습니다.
      DEVOTEE 추천 블로그

      안녕하세요. SK텔레콤 강민우입니다.

      Data Ontology 구축을 위한 기획 및 개발 진행 중이며 Engineering 관점에서 초기 데이터 구성 방법에 대해 공유하려고 합니다.

      Mart 데이터를 대상으로 했으며 여러 방법 중 Data Lineage 생성에 대해 말씀드리겠습니다.


      Data Ontology

      Data Ontology의 정의는 "특정 도메인(분야)에 존재하는 개체들, 그 개체들이 속한 개념(클래스), 속성, 그리고 개념 간의 관계들을 형식적이고 명시적으로 정의한 지식 표현 구조"입니다.

      정의를 정리해보면 도메인을 설명할 수 있는 모든 메타 데이터의 세계로 이해할 수 있습니다.


      데이터 메타화의 기준은 아래와 같이 표현할 수 있습니다.

      • 클래스(Class) : 개념/카테고리

        • ex) 사람, 동물, 회사

      • 개체(Instance) : 클래스에 속하는 실제 대상

        • ex) '강민우'는 사람이라는 클래스의 인스턴스

      • 속성(Property) : 개체나 클래스의 특성

        • ex) 나이, 위치, 소속

      • 관계(Relation) : 개념 간의 연결

        • ex) A는 B의 자식이다, C는 D의 부분이다

      • 공리(Axiom) : 제약 조건 또는 논리적 규칙

        • ex) 모든 고양이는 포유류다

      ETL 과정을 거치는 Mart Data 속성을 반영하여 Data Ontology를 생성을 위해 Data Description, Relationship, Lineage가 필요하다고 판단했으며

      해당 메타를 조합하여 도메인에 대한 새로운 속성을 생성과 insight를 발굴할 수 있을 것으로 보입니다.


      Data Lineage

      Data Lineage의 정의는 "데이터를 생성한 시점부터 소비되는 시점까지의 전체 흐름과 변화를 추적하는 정보"입니다.

      데이터 계보라고도 하며 정리하면 데이터의 출처, 변환 및 가공 과정, 목적지를 보여주는 데이터 흐름의 이력 추적 체계로 이해할 수 있습니다.


      Mart 데이터는 사용자가 요청을 위해 수많은 ETL 과정을 거칩니다.

      이때, 명확한 사용자 요청, ETL 쿼리가 존재한다면 Data Ontology의 속성 및 관계 생성 가능성이 높아집니다.

      image.png

      Mart 데이터가 생성되는 과정을 간단한 예시로 표현한 것이며

      Mart 데이터 결과 값 (명확한 사용자 요청) "고객 서비스 내 주문량", "고객 서비스 접근 경로", ETL 과정에 대한 Data Lineage 만 주어진 상황입니다.

      • 1번과 2번 과정을 서로 다른 Mart 데이터 생성 과정이며 결과적으로 서로 고객, 서비스 테이블을 이용한다는 것을 확인할 수 있습니다.

      • 공통적으로 Table B, C를 이용한다면 B와 C는 고객과 서비스 중 하나이며 고객, 서비스가 연관되는 Mart 데이터를 생성해야하는 경우 B, C를 이용할 확률이 높다는 것을 알 수 있습니다.

      • Table B, C는 1번과 2번 과정에 모두 사용되었기 때문에 주로 함께 사용될 확률이 높다고 할 수 있습니다.

      • 1번 과정의 ETL 노드들은 "주문량" 관계로 볼 수 있습니다.

      • 2번 과정에서 Table D를 사용했으며 결과에 "접근 경로"가 있는 것으로 보아 D에는 경로에 대한 데이터가 포함되어 있을 확률이 존재하며 D의 속성이 "이용 내역"이 아닌 "사용자 이력"이 될 수도 있습니다.

      본 데이터를 활용하여 ETL 최적화, 업무 속도, 마케팅 및 AI 사업 향상을 기대할 수 있을 것으로 예상합니다.


      Extract Table Lineage

      Data Lineage는 Table, Column 레벨로 나누어집니다.

      • Table Level Lineage : Source Table, Target Table, Join Table, Temporary Table

      • Column Level Lineage : Source Column, Target Column, Join Column, Function, Filter

        본 블로그에서는 Table Lineage 추출 방법에 대해 다루겠습니다.


      JsqlParser

      Data Lineage 생성 도구들 중 JsqlParser, Open Lineage, Apache Atlas 에 대해 설명 드리겠습니다.

      JsqlParser

      Open Lineage

      Apache Atlas

      - Java 호환 SQL 파서

      - SQL을 AST로 추출

      - SQL 실행하지 않아도 추출 가능

      - 라이브러리 커스텀 가능

      - 로직으로 데이터 변환 및 가공 가능

      - 디버깅 용이

      -일부 Big Data 엔진 및 표준 SQL 지원

      - Data Lineage, 메타 플랫폼

      - Big Data 엔진에 Listener 추가하여 실행

      - Job 실행 시 추출 가능

      - JSON으로 추출

      - Marquez 연동으로 graphDB 변환 및 시각화 가능

      -Hive, Spark 등 Big Data Job 추출 가능

      - Job 실패 시 Data Lineage 누락 발생 가능

      - 복잡한 쿼리 경우 부정확 데이터 추출 가능

      - Data Lineage, 메타 플랫폼

      - Connector 추가하여 실행

      - Job 실행 시 추출 가능

      -Atlas 고유 시각화 가능

      -Hive, Spark 등 Big Data Job 추출 가능

      - Job 실패 시 Data Lineage 누락 발생 가능

      - 복잡한 쿼리 경우 부정확 데이터 추출 가능

      Mart 현황

      • 수 많은 쿼리가 이미 존재

      • 복잡성 높은 쿼리 다수 존재

      • 누락 없어야하며 Job 재실행 시 영향도 큼

      • 활용 방향성에 따라 Data Lineage 추출 및 변환 사항 변경 가능

      • 쿼리 수정 후 Lineage 추출을 위해 연관된 모든 Job 실행하는 경우 없어야함

      • 데이터 시각화 불필요

      위 사항을 고려하여 JsqlParser 사용하는 것으로 결정했으며 Big Data 파싱 가능하도록 추가 로직 생성했습니다.


      LLM을 활용하여 예시 쿼리를 준비했으며 해당 코드는 Table Level Lineage 추출을 위한 Spark의 Insert Overwrite Table 샘플 코드입니다.

      해당 코드는 Java 21을 사용했습니다.


      Sample Query

      String sql =
          "WITH recent_orders AS (
            SELECT
              o.id AS order_id,
              o.customer_id,
              o.order_date,
              ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.order_date DESC) AS rn
            FROM
              orders o
            WHERE
              o.status = 'SHIPPED'
          ),
          high_value_customers AS (
            SELECT
              c.id,
              c.name,
              SUM(oi.quantity * oi.unit_price) AS total_spent
            FROM
              customers c
              JOIN orders o ON c.id = o.customer_id
              JOIN order_items oi ON o.id = oi.order_id
            GROUP BY
              c.id, c.name
            HAVING
              SUM(oi.quantity * oi.unit_price) > 10000
          )
          INSERT OVERWRITE TABLE SHOP.RESULT
          SELECT
            c.id,
            c.name,
            r.order_id,
            r.order_date,
            h.total_spent,
            CASE
              WHEN h.total_spent > 50000 THEN 'VIP'
              WHEN h.total_spent > 20000 THEN 'Premium'
              ELSE 'Regular'
            END AS customer_tier
          FROM
            high_value_customers h
            JOIN customers c ON c.id = h.id
            JOIN recent_orders r ON r.customer_id = c.id
          WHERE
            r.rn = 1
          ORDER BY
            h.total_spent DESC;"

      ParseStatement

      public class ParseStatement {
          public void createStatement(String sql) throws JsonProcessingException {
              Statement statement = null;
              TableLineageDto tableLineageDto = new TableLineageDto(sql);
          
              try {
                  statement = CCJSqlParserUtil.parse(sql);
              } catch (JSQLParserException e) {
                  throw new RuntimeException(e);
              }
          
              if (statement instanceof Insert) {
                  parseInsertStatement(statement, tableLineageDto, sql);
              }
          }
          
          public void parseInsertStatement(Statement statement, TableLineageDto tableLineageDto, String sql) throws JsonProcessingException {
          Insert insertStatement = (Insert) statement;
      
          List<String> targetNameList = setTargetVariables(insertStatement);
      
          Select select = insertStatement.getSelect();
          List<WithItem<?>> withItemsList = insertStatement.getWithItemsList();
      
          InsertStatementParser insertStatementParser = null;
      
          if (targetNameList.size() == 1) {
              insertStatementParser = new InsertStatementParser(tableLineageDto, sql, null, targetNameList.get(0));
          } else {
              insertStatementParser = new InsertStatementParser(tableLineageDto, sql, targetNameList.get(0), targetNameList.get(1));
          }
      
          if (withItemsList != null) {
              for (int i = 0, limit = withItemsList.size() ; i < limit ; i++) {
                  insertStatementParser.parseWithItem(withItemsList.get(i));
              }
          }
      
          if (select instanceof PlainSelect) {
              PlainSelect plainSelect = select.getPlainSelect();
              insertStatementParser.parsePlainSelect(plainSelect);
          } else if (select instanceof SetOperationList) {
              SetOperationList setOperationList = select.getSetOperationList();
              insertStatementParser.parseSetOperationList(setOperationList);
          }
      
          ObjectMapper objectMapper = new ObjectMapper();
          objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
      
          String jsonString = objectMapper.writeValueAsString(tableLineageDto);
          System.out.println("---------------------RESULT---------------------\n" + jsonString);
      }
      
      public static List setTargetVariables(Insert insertStatement) {
          List<String> nameParts = insertStatement.getTable().getNameParts();
          int namePartsSize = nameParts.size();
      
          List<String> targetNameList = Lists.newArrayList();
      
          if (namePartsSize == 2) {
              String targetTableName = nameParts.get(0);
              String targetDatabaseName = nameParts.get(1);
              targetNameList.add(targetDatabaseName);
              targetNameList.add(targetTableName);
          } else if (namePartsSize == 1) {
              String targetTableName = nameParts.get(0);
              targetNameList.add(targetTableName);
          }
      
          return targetNameList;
      }
      }

      ㄴ

      image.png

      • JsqlParser은 쿼리를 AST로 변경하며 쿼리의 세부사항을 모두 담고 있습니다.

        • insertStatement에 디버깅했으며 위 이미지는 Insert Overwrite 하위 Select 문에 대한 AST입니다.

      • JsqlParser 이용 시 sql을 지원하는 형식으로 변환해야 합니다(MySQL, Oracle, Big Query, Redshift, Snowflake 등).

        • Spark 쿼리 파싱 경우 (java 로직 개발로 해결해야 합니다)

          • TO_UNIX_TIMESTAMP -> UNIX_TIMESTAMP(STR_TO_DATE())

          • SELECT문보다 FROM문이 먼저 나온 경우 -> WITH문이나 SELECT문 이후로 변경

          • ".숫자" 명을 가진 테이블 및 컬럼 경우 -> backtick 추가

          • SPLIT -> (REGEX_SPLIT_TO_ARRAY())[]

          • INSERT OVERWRITE TABLE, INSERT INTO TABLE -> INSERT INTO (위 예시에 적용 필요)

          • CREATE EXTERNAL TABLE -> CREATE TABLE

          • ...

      InsertStatementParser

      public class InsertStatementParser {
      
          protected TableLineageDto tableLineageDto;
          protected String originalSql;
          protected String targetDatabaseName;
          protected String targetTableName;
          private Map<String, Integer> sourceTableNameSequenceMap;
          private String withName = null;
      
          public InsertStatementParser(TableLineageDto tableLineageDto, String originalSql, String targetDatabaseName, String targetTableName) {
              this.tableLineageDto = tableLineageDto;
              this.originalSql = originalSql;
              this.targetDatabaseName = targetDatabaseName;
              this.targetTableName = targetTableName;
              this.sourceTableNameSequenceMap = new HashMap<>();
          }
      
          public void parsePlainSelect(PlainSelect plainSelect) {
              FromItem fromItem = plainSelect.getFromItem();
              List<SelectItem<?>> selectItems = plainSelect.getSelectItems();
              List<Join> joins = plainSelect.getJoins();
              List<WithItem<?>> withItemsList = plainSelect.getWithItemsList();
      
              if (withItemsList != null) {
                  for (int i = 0, limit = withItemsList.size() ; i < limit ; i++) {
                      parseWithItem(withItemsList.get(i));
                  }
              }
      
              if (fromItem instanceof FromItem) {
                  if (fromItem instanceof ParenthesedSelect) {
                      parseParenthesedSelect((ParenthesedSelect) fromItem);
                  } else if (fromItem instanceof Table) {
                      parseTable((Table) fromItem);
                  }
              }
      
              if (joins != null) {
                  for (Join join : joins) {
                      parseJoin(join);
                  }
              }
          }
      
          public void parseWithItem(WithItem withItem) {
              withName = withItem.getAlias().getName();
              parseParenthesedSelect(withItem.getSelect());
              withName = null;
          }
      
          public void parseParenthesedSelect(ParenthesedSelect parenthesedSelect) {
              Select select = parenthesedSelect.getSelect();
              if (select instanceof PlainSelect) {
                  parsePlainSelect(select.getPlainSelect());
              } else if (select instanceof SetOperationList) {
                  parseSetOperationList(select.getSetOperationList());
              }
          }
      
          public void parseSetOperationList(SetOperationList setOperationList) {
              List<Select> selects = setOperationList.getSelects();
      
              if (selects != null) {
                  for (int i = 0, limit = selects.size() ; i < limit ; i++) {
                      parsePlainSelect(selects.get(i).getPlainSelect());
                  }
              }
          }
      
          public void parseTable(Table table) {
              String sourceTableName = null;
              String sourceDatabaseName = null;
      
              List<String> nameParts = table.getNameParts();
              int namePartsSize = nameParts.size();
      
              if (namePartsSize == 2) {
                  sourceTableName = nameParts.get(0);
                  sourceDatabaseName = nameParts.get(1);
              } else if (namePartsSize == 1) {
                  sourceTableName = nameParts.get(0);
              }
      
              String loweredSourceTableName = StringUtils.lowerCase(sourceTableName);
              int sourceTableNameSequence = sourceTableNameSequenceMap.getOrDefault(loweredSourceTableName, 0);
      
              if (sourceTableNameSequence == 0) {
                  sourceTableNameSequenceMap.put(loweredSourceTableName, 1);
      
                  if (withName == null) {
                      addTables(sourceDatabaseName, sourceTableName, 1, targetDatabaseName, targetTableName);
                  } else {
                      addTables(sourceDatabaseName, sourceTableName, 1, null, withName);
                  }
              } else {
                  sourceTableNameSequenceMap.put(loweredSourceTableName, sourceTableNameSequence + 1);
                  if (withName == null) {
                      addTables(sourceDatabaseName, sourceTableName, sourceTableNameSequence + 1, targetDatabaseName, targetTableName);
                  } else {
                      addTables(sourceDatabaseName, sourceTableName, sourceTableNameSequence + 1, targetDatabaseName, withName);
                  }
              }
          }
      
          public void addTables(String sourceDatabaseName, String sourceTableName, int sourceTableNameSequence, String targetDatabaseName, String targetTableName) {
              tableLineageDto.addTableDto(sourceDatabaseName, sourceTableName, sourceTableNameSequence, targetDatabaseName, targetTableName);
          }
      
          public void parseJoin(Join join) {
              FromItem fromItem = join.getFromItem();
      
              if (fromItem instanceof FromItem) {
                  if (fromItem instanceof ParenthesedSelect) {
                      parseParenthesedSelect((ParenthesedSelect) fromItem);
                  } else if (fromItem instanceof Table) {
                      parseTable((Table) fromItem);
                  }
              }
          }
      }

      Result

      {
        "sql" : "WITH recent_orders AS (\n  SELECT\n    o.id AS order_id,\n    o.customer_id,\n    o.order_date,\n    ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.order_date DESC) AS rn\n  FROM\n    orders o\n  WHERE\n    o.status = 'SHIPPED'\n),\nhigh_value_customers AS (\n  SELECT\n    c.id,\n    c.name,\n    SUM(oi.quantity * oi.unit_price) AS total_spent\n  FROM\n    dataset.customers c\n    JOIN orders o ON c.id = o.customer_id\n    JOIN order_items oi ON o.id = oi.order_id\n  GROUP BY\n    c.id, c.name\n  HAVING\n    SUM(oi.quantity * oi.unit_price) > 10000\n)\nINSERT INTO SHOP.RESULT\nSELECT\n  c.id,\n  c.name,\n  r.order_id,\n  r.order_date,\n  h.total_spent,\n  CASE\n    WHEN h.total_spent > 50000 THEN 'VIP'\n    WHEN h.total_spent > 20000 THEN 'Premium'\n    ELSE 'Regular'\n  END AS customer_tier\nFROM\n  high_value_customers h\n  JOIN dataset.customers c ON c.id = h.id\n  JOIN recent_orders r ON r.customer_id = c.id\nWHERE\n  r.rn = 1\nORDER BY\n  h.total_spent DESC;",
        "tableDtoSet" : [ {
          "sourceDatabaseName" : null,
          "sourceTableName" : "orders",
          "sourceTableNameSequence" : 1,
          "targetDatabaseName" : null,
          "targetTableName" : "recent_orders"
        }, {
          "sourceDatabaseName" : null,
          "sourceTableName" : "order_items",
          "sourceTableNameSequence" : 1,
          "targetDatabaseName" : null,
          "targetTableName" : "high_value_customers"
        }, {
          "sourceDatabaseName" : null,
          "sourceTableName" : "orders",
          "sourceTableNameSequence" : 2,
          "targetDatabaseName" : "SHOP",
          "targetTableName" : "high_value_customers"
        }, {
          "sourceDatabaseName" : "dataset",
          "sourceTableName" : "customers",
          "sourceTableNameSequence" : 2,
          "targetDatabaseName" : "SHOP",
          "targetTableName" : "RESULT"
        }, {
          "sourceDatabaseName" : null,
          "sourceTableName" : "high_value_customers",
          "sourceTableNameSequence" : 1,
          "targetDatabaseName" : "SHOP",
          "targetTableName" : "RESULT"
        }, {
          "sourceDatabaseName" : "dataset",
          "sourceTableName" : "customers",
          "sourceTableNameSequence" : 1,
          "targetDatabaseName" : null,
          "targetTableName" : "high_value_customers"
        }, {
          "sourceDatabaseName" : null,
          "sourceTableName" : "recent_orders",
          "sourceTableNameSequence" : 1,
          "targetDatabaseName" : "SHOP",
          "targetTableName" : "RESULT"
        } ]
      }
      • WITH문, INSERT문, JOIN문을 파싱합니다.

      • ParseStatement 클래스에서 INSERT문의 target 테이블, InsertStatementParser 클래스에서 WITH문의 target 테이블과 FROM문의 source 테이블을 추출합니다.

      • Result는 결과 DTO를 출력한 것이며 쿼리 내 source, target 테이블 리스트입니다.

      이상으로 Ontology 생성 과정 중 Table Level Lineage 생성 과정에 대해 알아보았습니다.

      해당 결과를 여러 방향으로 활용할 수 있을 것을 기대합니다.


      감사합니다

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      mwkangit 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기