액세스 기록

이 항목에서는 Snowflake의 사용자 액세스 기록에 대한 개념을 제공합니다.

이 항목의 내용:

개요

Snowflake의 액세스 기록은 사용자 쿼리가 데이터를 읽을 때와 SQL 문이 원본 데이터 오브젝트에서 대상 데이터 오브젝트로 복사하는 COPY 명령의 변형과 함께 INSERT, UPDATE, DELETE와 같은 데이터 쓰기 작업을 수행할 때를 말합니다. 사용자 액세스 기록은 Account Usage ACCESS_HISTORY 뷰를 쿼리하여 찾을 수 있습니다. 사용자(즉, 쿼리 연산자), 쿼리, 테이블 또는 뷰, 열 및 데이터 사이에는 직접 링크가 있기 때문에 이 뷰의 레코드는 규정 준수 감사를 간소화하며 인기 있고 자주 액세스하는 테이블 및 열에 대한 정보를 제공합니다.

ACCESS_HISTORY 뷰의 각 행에는 SQL 문마다 단 하나의 레코드가 포함됩니다. 레코드에는 다음과 같은 종류의 정보가 포함됩니다.

  • 쿼리가 직간접적으로 액세스한 원본 열 (예: 쿼리를 위한 데이터의 출처인 기본 테이블의 열).

  • SELECT 문에 지정된 열과 같이, 쿼리 결과에 표시되는 예상 열.

  • 쿼리 결과를 확인하는 데 사용되지만 예상되지 않는 열(예: 결과를 필터링하기 위한 WHERE 절의 열).

예:

CREATE OR REPLACE VIEW v1 (vc1, vc2) AS
SELECT c1 as vc1,
       c2 as vc2
FROM t
WHERE t.c3 > 0
;
Copy
  • 열 C1과 C2는 뷰가 직접 액세스하는 원본 열로, ACCESS_HISTORY 뷰의 base_objects_accessed 열에 기록됩니다.

  • 열 C3은 뷰에 포함된 행을 필터링하는 데 사용되며, 이는 ACCESS_HISTORY 뷰의 base_objects_accessed 열에 기록됩니다.

  • 열 VC1과 VC2는 사용자가 SELECT * FROM v1; 뷰를 쿼리할 때 보게 되는 예상 열로, ACCESS_HISTORY 뷰의 direct_objects_accessed 열에 기록됩니다.

WHERE 절의 키 열에도 동일한 동작이 적용됩니다. 예:

CREATE OR REPLACE VIEW join_v (vc1, vc2, c1) AS
  SELECT
      bt.c1 AS vc1,
      bt.c2 AS vc2,
      jt.c1
  FROM bt, jt
  WHERE bt.c3 = jt.c1;
Copy
  • 뷰를 생성하려면 두 개의 서로 다른 테이블, 즉 bt (기본 테이블)와 jt (조인 테이블)가 필요합니다.

  • 기본 테이블의 C1, C2, C3 열과 조인 테이블의 C1 열은 모두 ACCESS_HISTORY 뷰의 base_objects_accessed 열에 기록됩니다.

  • 열 VC1, VC2, C1은 사용자가 SELECT * FROM join_v; 뷰를 쿼리할 때 보게 되는 예상 열로, ACCESS_HISTORY 뷰의 direct_objects_accessed 열에 기록됩니다.

참고

Account Usage QUERY_HISTORY 뷰의 레코드가 항상 ACCESS_HISTORY 뷰에 기록되는 것은 아닙니다. SQL 문의 구조에 따라 Snowflake가 ACCESS_HISTORY 뷰에 항목을 기록할지 여부가 결정됩니다.

Snowflake가 ACCESS_HISTORY 뷰에서 지원하는 읽기 및 쓰기 작업에 대한 자세한 내용은 사용법 노트 뷰를 참조하십시오.

읽기 및 쓰기 작업 추적하기

ACCESS_HISTORY 뷰는 다음 열을 포함합니다.

query_id | query_start_time | user_name | direct_objects_accessed | base_objects_accessed | objects_modified | object_modified_by_ddl | policies_referenced | parent_query_id | root_query_id
Copy

마지막 열 objects_modified 가 Snowflake 열, 테이블 및 스테이지와 관련된 데이터 쓰기 정보를 지정하는 동안 처음 5개 열을 통해 읽기 작업이 추적됩니다.

Snowflake의 쿼리, 그리고 데이터베이스 오브젝트가 만들어진 방식에 따라, Snowflake가 direct_objects_accessed, base_objects_accessedobjects_modified 열에 반환하는 정보가 결정됩니다.

마찬가지로, 쿼리가 행 액세스 정책으로 보호되는 오브젝트 또는 마스킹 정책으로 보호되는 열을 참조하는 경우 Snowflake는 정책 정보를 policies_referenced 열에 기록합니다.

object_modified_by_ddl 열은 데이터베이스, 스키마, 테이블, 뷰, 열에 대한 DDL 작업을 기록합니다. 또한 이러한 작업에는 테이블 또는 뷰에 대한 행 액세스 정책, 열에 대한 마스킹 정책 그리고 오브젝트나 열에 대한 태그 업데이트(예: 태그 설정, 태그 값 변경)를 지정하는 문도 포함됩니다.

parent_query_idroot_query_id 열은 다음에 해당하는 쿼리 ID를 기록합니다.

  • 다른 오브젝트에 대해 읽기 또는 쓰기 작업을 수행하는 쿼리.

  • 중첩된 저장 프로시저 호출을 비롯하여, 저장 프로시저를 호출하는 오브젝트에 대해 읽기 또는 쓰기 작업을 수행하는 쿼리. 자세한 내용은 이 항목의 상위 쿼리 를 참조하십시오.

열 세부 정보는 ACCESS_HISTORY 뷰의 섹션을 참조하십시오.

읽기

읽기 쿼리와 ACCESS_HISTORY 뷰가 이 정보를 기록하는 방법을 이해하려면 다음 시나리오를 고려하십시오.

  • 일련의 오브젝트: base_table » view_1 » view_2 » view_3.

  • 다음과 같은 view_2 에 대한 읽기 쿼리:

    select * from view_2;
    
    Copy

이 예에서 Snowflake는 다음을 반환합니다.

  • 쿼리가 view_2 를 지정하므로 direct_objects_accessed 열의 view_2 를 반환합니다.

  • view_2 에서 데이터의 최초 원본이므로 base_objects_accessed 열의 base_table 을 반환합니다.

view_1view_3direct_objects_accessedbase_objects_accessed 열에 포함되지 않습니다. 이러한 뷰 중 어느 것도 쿼리에 포함되지 않았고, view_2 의 데이터에 대한 소스 역할을 하는 기본 오브젝트가 아니기 때문입니다.

쓰기

쓰기 작업과 ACCESS_HISTORY 뷰가 이 정보를 기록하는 방법을 이해하려면 다음 시나리오를 고려하십시오.

  • 데이터 원본: base_table

  • 데이터 원본에서 테이블 만들기(즉, CTAS):

    create table table_1 as select * from base_table;
    
    Copy

이 예에서 Snowflake는 다음을 반환합니다.

  • 테이블이 직접 액세스되었고 데이터의 원본이므로 base_objects_accesseddirect_objects_accessed 열의 base_table.

  • 테이블을 만들 때 작성된 열이 있는 objects_modified 열의 table_1 을 반환합니다.

열 계보

열 계보(즉, 열에 대한 액세스 기록)는 Account Usage ACCESS_HISTORY 뷰를 확장하여 쓰기 작업에서 데이터가 원본 열에서 대상 열로 흐르는 방식을 지정합니다. Snowflake는 계보 체인의 오브젝트가 삭제되지 않는 한 원본 열의 데이터를 참조하는 모든 후속 테이블 오브젝트(예: INSERT, MERGE, CTAS)를 통해 원본 열의 데이터를 추적합니다. Snowflake는 ACCESS_HISTORY 뷰에서 objects_modified 열을 강화해 열 계보에 액세스할 수 있도록 합니다.

추가적인 세부 정보는 다음을 참조하십시오.

마스킹 및 행 액세스 정책 참조

POLICY_REFERENCED 열은 행 액세스 정책이나 마스킹 정책으로 보호되는 모든 중간 오브젝트를 포함하여 행 액세스 정책이 테이블에 설정되어 있거나 마스킹 정책이 열에 설정되어 있는 오브젝트를 지정합니다. Snowflake는 테이블 또는 열에 적용되는 정책을 기록합니다.

다음 오브젝트를 고려하십시오.

t1 » v1 » v2

여기서

  • t1 은 기본 테이블입니다.

  • v1 은 기본 테이블에서 만든 뷰입니다.

  • v2v1 에서 만든 뷰입니다.

사용자가 v2 를 쿼리하는 경우 policies_referenced 열은 v2 를 보호하는 행 액세스 정책, v2 의 열을 보호하는 각 마스킹 정책 또는 적용 가능한 두 종류의 정책을 모두 기록합니다. 또한 이 열에서는 t1v1 을 보호하는 모든 마스킹 정책 또는 행 액세스 정책을 기록합니다.

이러한 레코드는 데이터 관리자가 정책으로 보호되는 오브젝트에 액세스하는 방법을 이해하는 데 도움이 될 수 있습니다.

policies_referenced 열은 ACCESS_HISTORY 뷰에 추가적인 이점을 제공합니다.

  • 지정된 쿼리에서 사용자가 액세스하는 정책으로 보호되는 오브젝트를 식별합니다.

  • 정책 감사 프로세스를 간소화합니다.

    ACCESS_HISTORY 뷰를 쿼리하면 사용자가 액세스하는 보호된 오브젝트와 보호된 열에 대한 정보를 얻기 위해 다른 Account Usage 뷰(예: POLICY_REFERENCESQUERY_HISTORY)에 대한 복잡한 조인이 필요하지 않습니다.

지원되는 작업

ACCESS_HISTORY 뷰가 지원하는 읽기 및 쓰기 작업에 대한 전체 설명은 ACCESS_HISTORY 뷰 의 사용법 노트 섹션을 참조하십시오.

이점

Snowflake의 액세스 기록은 읽기 및 쓰기 작업과 관련하여 다음과 같은 이점을 제공합니다.

데이터 검색:

미사용 데이터를 발견하여 데이터의 보관 또는 삭제 여부를 결정합니다.

민감한 데이터의 이동 방식 추적:

외부 클라우드 저장소 위치(예: Amazon S3 버킷)에서 대상 Snowflake 테이블로, 또는 그 반대로의 데이터 이동을 추적합니다.

Snowflake 테이블에서 다른 Snowflake 테이블로의 내부 데이터 이동을 추적합니다.

민감한 데이터의 이동을 추적한 후, 데이터를 보호하기 위한 정책(마스킹행 액세스)을 적용하고, 액세스 제어 설정 업데이트하여 스테이지와 테이블에 대한 액세스를 추가로 규제하고, 민감한 데이터가 있는 스테이지, 테이블, 열이 규정 준수 요구 사항에 맞는지 추적할 수 있도록 태그 를 설정합니다.

데이터 유효성 검사:

데이터를 원본과 비교해 추적할 수 있으므로 보고서, 대시보드 그리고 차트 및 그래프와 같은 데이터 시각화 제품의 정확성과 무결성이 검증됩니다.

데이터 관리자가 사용자에게 알린 후 주어진 테이블 또는 뷰를 삭제하거나 변경할 수도 있습니다.

규정 준수 감사:

테이블 또는 스테이지에서 쓰기 작업을 수행한 Snowflake 사용자와 GDPRCCPA 와 같은 규정 준수 규제를 충족하려고 쓰기 작업이 발생한 시간을 식별합니다.

전반적인 데이터 거버넌스 강화:

ACCESS_HISTORY 뷰에서는 액세스한 데이터, 데이터 액세스가 발생한 시간, 액세스한 데이터가 데이터 원본 오브젝트에서 데이터 대상 오브젝트로 이동한 방법을 통합적으로 볼 수 있습니다.

열 계보는 다음과 같은 추가적인 이점을 제공합니다.

파생 오브젝트 보호:

데이터 관리자는 파생 오브젝트(예: CTAS)를 만든 후 추가 작업을 수행할 필요 없이 민감한 원본 열을 손쉽게 태그 지정 할 수 있습니다. 그 뒤에, 데이터 관리자는 행 액세스 정책 을 사용하여 민감한 열이 포함된 테이블을 보호하거나 마스킹 정책 또는 태그 기반 마스킹 정책 을 사용하여 민감한 열 자체를 보호할 수 있습니다.

민감한 열 복사 빈도:

데이터 개인정보 보호 담당자는 민감한 데이터가 포함된 열의 오브젝트 수(예: 테이블 1개, 뷰 2개)를 재빨리 확인할 수 있습니다. 민감한 데이터가 있는 열이 테이블 오브젝트에 나타나는 횟수를 알면 데이터 개인정보 보호 담당자가 규정 준수 표준(예: 유럽 연합의 일반 데이터 보호 규정(GDPR) 표준을 충족하는 표준)을 충족하는 방법을 입증할 수 있습니다.

근본 원인 분석:

열 계보는 데이터를 해당 원본까지 추적하는 메커니즘을 제공하므로, 데이터 품질이 좋지 않아 발생하는 오류 지점을 정확히 찾아내고 문제 해결 프로세스 중에 분석할 열의 개수를 줄이는 데 도움이 될 수 있습니다.

ACCESS_HISTORY 뷰 쿼리하기

다음 섹션에서는 ACCESS_HISTORY 뷰에 대한 쿼리 예시를 제공합니다.

예시 쿼리 중 일부는 쿼리 성능을 향상하기 위해 query_start_time 열을 필터링함에 유의하십시오. 성능을 향상하기 위한 다른 옵션은 더 좁은 시간 범위에 대하여 쿼리를 수행하는 것입니다.

예: 쿼리 읽기

아래 하위 섹션에서는 다음 사용 사례에 대한 읽기 작업의 ACCESS_HISTORY 뷰를 쿼리하는 방법을 자세히 설명합니다.

  • 특정 사용자에 대한 액세스 기록을 얻습니다.

  • object_id (예: 테이블 ID)를 기반으로 지난 30일 동안의 민감한 데이터 액세스에 대한 규정 준수 감사를 촉진하여 다음 질문에 답합니다.

    • 누가 데이터에 액세스했습니까?

    • 언제 데이터에 액세스했습니까?

    • 어떤 열에 액세스했습니까?

사용자 액세스 기록 반환하기

가장 최근 액세스부터 시작하여 사용자 및 쿼리 시작 시간별로 정렬된 사용자 액세스 기록을 반환합니다.

SELECT user_name
       , query_id
       , query_start_time
       , direct_objects_accessed
       , base_objects_accessed
FROM access_history
ORDER BY 1, 3 desc
;
Copy

규정 준수 감사 촉진하기

다음 예는 규정 준수 감사를 용이하게 하는 데 도움이 됩니다.

  • 다음과 같이 object_id 값을 추가하여, 지난 30일 동안 민감한 테이블에 액세스한 사람을 확인합니다.

    SELECT distinct user_name
    FROM access_history
         , lateral flatten(base_objects_accessed) f1
    WHERE f1.value:"objectId"::int=<fill_in_object_id>
    AND f1.value:"objectDomain"::string='Table'
    AND query_start_time >= dateadd('day', -30, current_timestamp())
    ;
    
    Copy
  • 다음과 같이 object_id32998411400350 을 사용하여, 지난 30일 동안 액세스가 발생한 시간을 확인합니다.

    SELECT query_id
           , query_start_time
    FROM access_history
         , lateral flatten(base_objects_accessed) f1
    WHERE f1.value:"objectId"::int=32998411400350
    AND f1.value:"objectDomain"::string='Table'
    AND query_start_time >= dateadd('day', -30, current_timestamp())
    ;
    
    Copy
  • object_id32998411400350 을 사용하여, 지난 30일 동안 액세스한 열을 확인합니다.

    SELECT distinct f4.value AS column_name
    FROM access_history
         , lateral flatten(base_objects_accessed) f1
         , lateral flatten(f1.value) f2
         , lateral flatten(f2.value) f3
         , lateral flatten(f3.value) f4
    WHERE f1.value:"objectId"::int=32998411400350
    AND f1.value:"objectDomain"::string='Table'
    AND f4.key='columnName'
    ;
    
    Copy

예: 쓰기 작업

아래 하위 섹션에서는 다음 사용 사례에 대한 쓰기 작업의 ACCESS_HISTORY 뷰를 쿼리하는 방법을 자세히 설명합니다.

  • 스테이지에서 테이블로 데이터를 로딩합니다.

  • 테이블에서 스테이지로 데이터를 언로딩합니다.

  • PUT 명령을 사용하여 로컬 파일을 스테이지에 업로드합니다.

  • GET 명령을 사용하여 스테이지에서 로컬 디렉터리로 데이터 파일을 불러옵니다.

  • 민감한 스테이지 데이터 이동 추적.

스테이지에서 테이블로 데이터 로딩하기

외부 클라우드 저장소의 데이터 파일에서 대상 테이블의 열로 값 세트를 로딩합니다.

copy into table1(col1, col2)
from (select t.$1, t.$2 from @mystage1/data1.csv.gz);
Copy

direct_objects_accessedbase_objects_accessed 열은 외부의 명명된 스테이지가 액세스되었음을 지정합니다.

{
  "objectDomain": STAGE
  "objectName": "mystage1",
  "objectId": 1,
  "stageKind": "External Named"
}
Copy

objects_modified 열은 데이터가 테이블의 두 열에 기록되었음을 지정합니다.

{
  "columns": [
     {
       "columnName": "col1",
       "columnId": 1
     },
     {
       "columnName": "col2",
       "columnId": 2
     }
  ],
  "objectId": 1,
  "objectName": "TEST_DB.TEST_SCHEMA.TABLE1",
  "objectDomain": TABLE
}
Copy

테이블에서 스테이지로 데이터 언로딩하기

Snowflake 테이블에서 클라우드 저장소로 값 세트를 언로딩합니다.

copy into @mystage1/data1.csv
from table1;
Copy

direct_objects_accessedbase_objects_accessed 열은 액세스된 테이블 열을 지정합니다.

{
  "objectDomain": TABLE
  "objectName": "TEST_DB.TEST_SCHEMA.TABLE1",
  "objectId": 123,
  "columns": [
     {
       "columnName": "col1",
       "columnId": 1
     },
     {
       "columnName": "col2",
       "columnId": 2
     }
  ]
}
Copy

objects_modified 열은 액세스된 데이터가 기록된 스테이지를 지정합니다.

{
  "objectId": 1,
  "objectName": "mystage1",
  "objectDomain": STAGE,
  "stageKind": "External Named"
}
Copy

PUT 명령을 사용하여 로컬 파일을 스테이지에 업로드하기

데이터 파일을 내부(즉, Snowflake) 스테이지에 복사합니다.

put file:///tmp/data/mydata.csv @my_int_stage;
Copy

direct_objects_accessedbase_objects_accessed 열은 액세스된 파일의 로컬 경로를 지정합니다.

{
  "location": "file:///tmp/data/mydata.csv"
}
Copy

objects_modified 열은 액세스된 데이터가 기록된 스테이지를 지정합니다.

{
  "objectId": 1,
  "objectName": "my_int_stage",
  "objectDomain": STAGE,
  "stageKind": "Internal Named"
}
Copy

GET 명령을 사용하여 스테이지에서 로컬 디렉터리로 데이터 파일 불러오기

내부 스테이지에서 로컬 컴퓨터의 디렉터리로 데이터 파일을 불러옵니다.

get @%mytable file:///tmp/data/;
Copy

direct_objects_accessedbase_objects_accessed 열은 액세스된 스테이지 및 로컬 디렉터리를 지정합니다.

{
  "objectDomain": Stage
  "objectName": "mytable",
  "objectId": 1,
  "stageKind": "Table"
}
Copy

objects_modified 열은 액세스된 데이터가 기록된 디렉터리를 지정합니다.

{
  "location": "file:///tmp/data/"
}
Copy

민감한 스테이지 데이터 이동 추적하기

시간순으로 실행되는 일련의 쿼리를 통해 민감한 스테이지 데이터의 이동을 추적합니다.

다음 쿼리를 실행합니다. 이들 문 중 5개가 스테이지 데이터에 액세스합니다. 따라서 스테이지 액세스를 위해 ACCESS_HISTORY 뷰를 쿼리할 때 결과 세트에 5개의 행이 포함되어야 합니다.

use test_db.test_schema;
create or replace table T1(content variant);
insert into T1(content) select parse_json('{"name": "A", "id":1}');

-- T1 -> T6
insert into T6 select * from T1;

-- S1 -> T1
copy into T1 from @S1;

-- T1 -> T2
create table T2 as select content:"name" as name, content:"id" as id from T1;

-- T1 -> S2
copy into @S2 from T1;

-- S1 -> T3
create or replace table T3(customer_info variant);
copy into T3 from @S1;

-- T1 -> T4
create or replace table T4(name string, id string, address string);
insert into T4(name, id) select content:"name", content:"id" from T1;

-- T6 -> T7
create table T7 as select * from T6;
Copy

여기서

  • T1, T2T7 은 테이블의 이름을 지정합니다.

  • S1S2 는 스테이지의 이름을 지정합니다.

액세스 기록을 쿼리하여 스테이지 S1 에 대한 액세스를 확인합니다.

direct_objects_accessed, base_objects_accessed, objects_modified 열의 데이터는 다음 테이블에 나와 있습니다.

direct_objects_accessed

base_objects_accessed

objects_modified

[
  {
    "columns": [
      {
        "columnId": 68610,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66564,
    "objectName": "TEST_DB.TEST_SCHEMA.T1"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68610,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66564,
    "objectName": "TEST_DB.TEST_SCHEMA.T1"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68611,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66566,
    "objectName": "TEST_DB.TEST_SCHEMA.T6"
  }
]
Copy
[
  {
    "objectDomain": "Stage",
    "objectId": 117,
    "objectName": "TEST_DB.TEST_SCHEMA.S1",
    "stageKind": "External Named"
  }
]
Copy
[
  {
    "objectDomain": "Stage",
    "objectId": 117,
    "objectName": "TEST_DB.TEST_SCHEMA.S1",
    "stageKind": "External Named"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68610,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66564,
    "objectName": "TEST_DB.TEST_SCHEMA.T1"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68610,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66564,
    "objectName": "TEST_DB.TEST_SCHEMA.T1"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68610,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66564,
    "objectName": "TEST_DB.TEST_SCHEMA.T1"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68613,
        "columnName": "ID"
      },
      {
        "columnId": 68612,
        "columnName": "NAME"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66568,
    "objectName": "TEST_DB.TEST_SCHEMA.T2"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68610,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66564,
    "objectName": "TEST_DB.TEST_SCHEMA.T1"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68610,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66564,
    "objectName": "TEST_DB.TEST_SCHEMA.T1"
  }
]
Copy
[
  {
    "objectDomain": "Stage",
    "objectId": 118,
    "objectName": "TEST_DB.TEST_SCHEMA.S2",
    "stageKind": "External Named"
  }
]
Copy
[
  {
    "objectDomain": "Stage",
    "objectId": 117,
    "objectName": "TEST_DB.TEST_SCHEMA.S1",
    "stageKind": "External Named"
  }
]
Copy
[
  {
    "objectDomain": "Stage",
    "objectId": 117,
    "objectName": "TEST_DB.TEST_SCHEMA.S1",
    "stageKind": "External Named"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68614,
        "columnName": "CUSTOMER_INFO"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66570,
    "objectName": "TEST_DB.TEST_SCHEMA.T3"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68610,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66564,
    "objectName": "TEST_DB.TEST_SCHEMA.T1"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68610,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66564,
    "objectName": "TEST_DB.TEST_SCHEMA.T1"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68615,
        "columnName": "NAME"
      },
      {
        "columnId": 68616,
        "columnName": "ID"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66572,
    "objectName": "TEST_DB.TEST_SCHEMA.T4"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68611,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66566,
    "objectName": "TEST_DB.TEST_SCHEMA.T6"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68611,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66566,
    "objectName": "TEST_DB.TEST_SCHEMA.T6"
  }
]
Copy
[
  {
    "columns": [
      {
        "columnId": 68618,
        "columnName": "CONTENT"
      }
    ],
    "objectDomain": "Table",
    "objectId": 66574,
    "objectName": "TEST_DB.TEST_SCHEMA.T7"
  }
]
Copy

쿼리 예제에 대해 다음 사항에 유의하십시오.

  • 재귀 일반 테이블 식 을 사용합니다.

  • USING 절 대신 JOIN 구문을 사용합니다.

    with access_history_flatten as (
        select
            r.value:"objectId" as source_id,
            r.value:"objectName" as source_name,
            r.value:"objectDomain" as source_domain,
            w.value:"objectId" as target_id,
            w.value:"objectName" as target_name,
            w.value:"objectDomain" as target_domain,
            c.value:"columnName" as target_column,
            t.query_start_time as query_start_time
        from
            (select * from TEST_DB.ACCOUNT_USAGE.ACCESS_HISTORY) t,
            lateral flatten(input => t.BASE_OBJECTS_ACCESSED) r,
            lateral flatten(input => t.OBJECTS_MODIFIED) w,
            lateral flatten(input => w.value:"columns", outer => true) c
            ),
        sensitive_data_movements(path, target_id, target_name, target_domain, target_column, query_start_time)
        as
          -- Common Table Expression
          (
            -- Anchor Clause: Get the objects that access S1 directly
            select
                f.source_name || '-->' || f.target_name as path,
                f.target_id,
                f.target_name,
                f.target_domain,
                f.target_column,
                f.query_start_time
            from
                access_history_flatten f
            where
            f.source_domain = 'Stage'
            and f.source_name = 'TEST_DB.TEST_SCHEMA.S1'
            and f.query_start_time >= dateadd(day, -30, date_trunc(day, current_date))
            union all
            -- Recursive Clause: Recursively get all the objects that access S1 indirectly
            select sensitive_data_movements.path || '-->' || f.target_name as path, f.target_id, f.target_name, f.target_domain, f.target_column, f.query_start_time
              from
                 access_history_flatten f
                join sensitive_data_movements
                on f.source_id = sensitive_data_movements.target_id
                    and f.source_domain = sensitive_data_movements.target_domain
                    and f.query_start_time >= sensitive_data_movements.query_start_time
          )
    select path, target_name, target_id, target_domain, array_agg(distinct target_column) as target_columns
    from sensitive_data_movements
    group by path, target_id, target_name, target_domain;
    
    Copy

이 쿼리는 스테이지 S1 데이터 이동과 관련된 다음 결과 세트를 생성합니다.

PATH

TARGET_NAME

TARGET_ID

TARGET_DOMAIN

TARGET_COLUMNS

TEST_DB.TEST_SCHEMA.S1–>TEST_DB.TEST_SCHEMA.T1

TEST_DB.TEST_SCHEMA.T1

66564

테이블

[“CONTENT”]

TEST_DB.TEST_SCHEMA.S1–>TEST_DB.TEST_SCHEMA.T1–>TEST_DB.TEST_SCHEMA.S2

TEST_DB.TEST_SCHEMA.S2

118

스테이지

[]

TEST_DB.TEST_SCHEMA.S1–>TEST_DB.TEST_SCHEMA.T1–>TEST_DB.TEST_SCHEMA.T2

TEST_DB.TEST_SCHEMA.T2

66568

테이블

[“NAME”,”ID”]

TEST_DB.TEST_SCHEMA.S1–>TEST_DB.TEST_SCHEMA.T1–>TEST_DB.TEST_SCHEMA.T4

TEST_DB.TEST_SCHEMA.T4

66572

테이블

[“ID”,”NAME”]

TEST_DB.TEST_SCHEMA.S1–>TEST_DB.TEST_SCHEMA.T3

TEST_DB.TEST_SCHEMA.T3

66570

테이블

[“CUSTOMER_INFO”]

예: 열 계보

다음 예제에서는 ACCESS_HISTORY 뷰를 쿼리하고 FLATTEN 함수를 사용하여 objects_modified 열을 평면화합니다.

대표적인 예로, Snowflake 계정에서 다음 SQL 쿼리를 실행하여 아래 테이블을 생성하며, 여기서 번호가 매겨진 주석은 다음을 나타냅니다.

  • // 1: directSources 필드와 대상 열 사이의 매핑을 가져옵니다.

  • // 2: baseSources 필드와 대상 열 사이의 매핑을 가져옵니다.

// 1

select
  directSources.value: "objectId" as source_object_id,
  directSources.value: "objectName" as source_object_name,
  directSources.value: "columnName" as source_column_name,
  'DIRECT' as source_column_type,
  om.value: "objectName" as target_object_name,
  columns_modified.value: "columnName" as target_column_name
from
  (
    select
      *
    from
      snowflake.account_usage.access_history
  ) t,
  lateral flatten(input => t.OBJECTS_MODIFIED) om,
  lateral flatten(input => om.value: "columns", outer => true) columns_modified,
  lateral flatten(
    input => columns_modified.value: "directSources",
    outer => true
  ) directSources

union

// 2

select
  baseSources.value: "objectId" as source_object_id,
  baseSources.value: "objectName" as source_object_name,
  baseSources.value: "columnName" as source_column_name,
  'BASE' as source_column_type,
  om.value: "objectName" as target_object_name,
  columns_modified.value: "columnName" as target_column_name
from
  (
    select
      *
    from
      snowflake.account_usage.access_history
  ) t,
  lateral flatten(input => t.OBJECTS_MODIFIED) om,
  lateral flatten(input => om.value: "columns", outer => true) columns_modified,
  lateral flatten(
    input => columns_modified.value: "baseSources",
    outer => true
  ) baseSources
;
Copy

반환 결과:

SOURCE_OBJECT_ID

SOURCE_OBJECT_NAME

SOURCE_COLUMN_NAME

SOURCE_COLUMN_TYPE

TARGET_OBJECT_NAME

TARGET_COLUMN_NAME

1

D.S.T0

NAME

BASE

D.S.T1

NAME

2

D.S.V1

NAME

DIRECT

D.S.T1

NAME

예: 행 액세스 정책 참조 추적

행 액세스 정책이 중복 없이 테이블, 뷰 또는 구체화된 뷰에 설정된 경우 다음과 같이 각 인스턴스의 행을 반환합니다.

use role accountadmin;
select distinct
    obj_policy.value:"policyName"::VARCHAR as policy_name
from snowflake.account_usage.access_history as ah
    , lateral flatten(ah.policies_referenced) as obj
    , lateral flatten(obj.value:"policies") as obj_policy
;
Copy

예: 마스킹 정책 참조 추적

마스킹 정책이 중복 없이 열을 보호하는 경우 각 인스턴스의 행을 반환합니다. policies_referenced 열은 테이블의 행 액세스 정책보다 한 수준 더 깊은 열에 마스킹 정책을 지정하므로 추가적인 평면화가 필요합니다.

use role accountadmin;
select distinct
    policies.value:"policyName"::VARCHAR as policy_name
from snowflake.account_usage.access_history as ah
    , lateral flatten(ah.policies_referenced) as obj
    , lateral flatten(obj.value:"columns") as columns
    , lateral flatten(columns.value:"policies") as policies
;
Copy

예: 쿼리에서 적용된 정책 추적

특정 기간에 특정 쿼리에 대해 정책이 업데이트된 시간(POLICY_CHANGED_TIME)과 정책 조건(POLICY_BODY)을 반환합니다.

이 쿼리를 사용하기 전에 WHERE 절 입력값을 업데이트합니다.

where query_start_time > '2023-07-07' and
   query_start_time < '2023-07-08' and
   query_id = '01ad7987-0606-6e2c-0001-dd20f12a9777')
Copy

여기서

query_start_time > '2023-07-07'

시작 타임스탬프를 지정합니다.

query_start_time < '2023-07-08'

종료 타임스탬프를 지정합니다.

query_id = '01ad7987-0606-6e2c-0001-dd20f12a9777'

Account Usage ACCESS_HISTORY 뷰에서 쿼리 식별자를 지정합니다.

쿼리를 실행합니다.

SELECT *
from(
  select j1.*,j2.QUERY_START_TIME as POLICY_CHANGED_TIME, POLICY_BODY
from
(
  select distinct t1.*,
      t4.value:"policyId"::number as PID
  from (select *
      from SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY
      where query_start_time > '2023-07-07' and
         query_start_time < '2023-07-08' and
         query_id = '01ad7987-0606-6e2c-0001-dd20f12a9777') as t1, //
  lateral flatten (input => t1.POLICIES_REFERENCED,OUTER => TRUE) t2,
  lateral flatten (input => t2.value:"columns", OUTER => TRUE) t3,
  lateral flatten (input => t3.value:"policies",OUTER => TRUE) t4
) as j1
left join
(
  select OBJECT_MODIFIED_BY_DDL:"objectId"::number as PID,
      QUERY_START_TIME,
      OBJECT_MODIFIED_BY_DDL:"properties"."policyBody"."value" as POLICY_BODY
      from SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY
      where OBJECT_MODIFIED_BY_DDL is not null and
      (OBJECT_MODIFIED_BY_DDL:"objectDomain" ilike '%masking%' or OBJECT_MODIFIED_BY_DDL:"objectDomain" ilike '%row%')
) as j2
On j1.POLICIES_REFERENCED is not null and j1.pid = j2.pid and j1.QUERY_START_TIME>j2.QUERY_START_TIME) as j3
QUALIFY ROW_NUMBER() OVER (PARTITION BY query_id,pid ORDER BY policy_changed_time DESC) = 1;
Copy

예: UDF

다음 UDF 예에서는 Account Usage ACCESS_HISTORY 뷰가 기록하는 방식을 보여줍니다.

  • get_product 라는 UDF 호출.

  • get_product 함수를 mydb.tables.t1 이라는 테이블로 호출한 결과물을 삽입.

  • 공유 UDF.

UDF 호출

두 숫자의 곱을 계산해 그 값이 mydb.udfs 라는 스키마에 저장되어 있다고 가정하는 다음 SQL UDF를 생각해 보십시오.

CREATE FUNCTION MYDB.UDFS.GET_PRODUCT(num1 number, num2 number)
RETURNS number
AS
$$
    NUM1 * NUM2
$$
;
Copy

get_product 를 직접 호출 하면 direct_objects_accessed 열에 UDF 세부 정보가 기록됩니다.

[
  {
    "objectDomain": "FUNCTION",
    "objectName": "MYDB.UDFS.GET_PRODUCT",
    "objectId": "2",
    "argumentSignature": "(NUM1 NUMBER, NUM2 NUMBER)",
    "dataType": "NUMBER(38,0)"
  }
]
Copy

이 예는 이 항목의 저장 프로시저 호출하기 와 유사합니다.

INSERT DML을 포함한 UDF

mydb.tables.t1 이라는 테이블에서 1과 2라는 열을 업데이트하려면 다음 INSERT 문을 사용해 보십시오.

insert into t1(product)
select get_product(c1, c2) from mydb.tables.t1;
Copy

ACCESS_HISTORY 뷰는 get_product 함수를 다음에 기록합니다.

  • direct_objects_accessed 함수(SQL 문에 함수 이름이 명시적으로 이름이 지정되므로)

  • directSources 배열의 objects_modified 열(함수가 열에 삽입되는 값의 소스이므로)

마찬가지로, t1 테이블은 다음과 같은 동일한 열에 기록됩니다.

direct_objects_accessed

objects_modified

[
  {
    "objectDomain": "FUNCTION",
    "objectName": "MYDB.UDFS.GET_PRODUCT",
    "objectId": "2",
    "argumentSignature": "(NUM1 NUMBER, NUM2 NUMBER)",
    "dataType": "NUMBER(38,0)"
  },
  {
    "objectDomain": "TABLE",
    "objectName": "MYDB.TABLES.T1",
    "objectId": 1,
    "columns":
    [
      {
        "columnName": "c1",
        "columnId": 1
      },
      {
        "columnName": "c2",
        "columnId": 2
      }
    ]
  }
]
Copy
 [
   {
     "objectDomain": "TABLE",
     "objectName": "MYDB.TABLES.T1",
     "objectId": 2,
     "columns":
     [
       {
         "columnId": "product",
         "columnName": "201",
         "directSourceColumns":
         [
           {
             "objectDomain": "Table",
             "objectName": "MYDB.TABLES.T1",
             "objectId": "1",
             "columnName": "c1"
           },
           {
             "objectDomain": "Table",
             "objectName": "MYDB.TABLES.T1",
             "objectId": "1",
             "columnName": "c2"
           },
           {
             "objectDomain": "FUNCTION",
             "objectName": "MYDB.UDFS.GET_PRODUCT",
             "objectId": "2",
             "argumentSignature": "(NUM1 NUMBER, NUM2 NUMBER)",
             "dataType": "NUMBER(38,0)"
           }
         ],
         "baseSourceColumns":[]
       }
     ]
   }
]
Copy

공유 UDF

공유 UDF는 직접 또는 간접적으로 참조할 수 있습니다.

  • 직접 참조는 이 항목의 UDF를 명시적으로 호출하기 와 동일하지만, UDF가 base_objects_accesseddirect_objects_accessed 열에 모두 기록됩니다.

  • 간접 참조의 예는 뷰를 생성하는 UDF를 호출하는 것입니다.

    create view v as
    select get_product(c1, c2) as vc from t;
    
    Copy

    base_objects_accessed 열에는 UDF와 테이블이 기록됩니다.

    direct_objects_accessed 열에는 뷰가 기록됩니다.

예: DDL 작업으로 수정된 오브젝트 추적하기

ALLOWED_VALUES로 태그 만들기

태그 만들기:

create tag governance.tags.pii allowed_values 'sensitive','public';
Copy

열 값:

{
  "objectDomain": "TAG",
  "objectName": "governance.tags.pii",
  "objectId": "1",
  "operationType": "CREATE",
  "properties": {
    "allowedValues": {
      "sensitive": {
        "subOperationType": "ADD"
      },
      "public": {
        "subOperationType": "ADD"
      }
    }
  }
}
Copy

참고

태그 생성 시 허용되는 값을 지정하지 않으면 properties 필드는 빈 배열입니다(즉, {}).

태그 및 마스킹 정책으로 테이블 만들기

열에 대한 마스킹 정책, 열에 태그 및 테이블에 태그를 사용하여 테이블을 만듭니다.

create or replace table hr.data.user_info(
  email string
    with masking policy governance.policies.email_mask
    with tag (governance.tags.pii = 'sensitive')
  )
with tag (governance.tags.pii = 'sensitive');
Copy

열 값:

{
  "objectDomain": "TABLE",
  "objectName": "hr.data.user_info",
  "objectId": "1",
  "operationType": "CREATE",
  "properties": {
    "tags": {
      "governance.tags.pii": {
        "subOperationType": "ADD",
        "objectId": {
          "value": "1"
        },
        "tagValue": {
          "value": "sensitive"
        }
      }
    },
    "columns": {
      "email": {
        objectId: {
          "value": 1
        },
        "subOperationType": "ADD",
        "tags": {
          "governance.tags.pii": {
            "subOperationType": "ADD",
            "objectId": {
              "value": "1"
            },
            "tagValue": {
              "value": "sensitive"
            }
          }
        },
        "maskingPolicies": {
          "governance.policies.email_mask": {
            "subOperationType": "ADD",
            "objectId": {
              "value": 2
            }
          }
        }
      }
    }
  }
}
Copy

태그에 대한 마스킹 정책 설정하기

태그에 대한 마스킹 정책을 설정합니다(즉, 태그 기반 마스킹).

alter tag governance.tags.pii set masking policy governance.policies.email_mask;
Copy

열 값:

{
  "objectDomain": "TAG",
  "objectName": "governance.tags.pii",
  "objectId": "1",
  "operationType": "ALTER",
  "properties": {
    "maskingPolicies": {
      "governance.policies.email_mask": {
        "subOperationType": "ADD",
        "objectId": {
          "value": 2
        }
      }
    }
  }
}
Copy

테이블 바꾸기

t2 라는 테이블을 t3 이라는 테이블로 바꿉니다.

alter table governance.tables.t2 swap with governance.tables.t3;
Copy

뷰에 있는 서로 다른 두 레코드에 주목하십시오.

레코드 1:

{
  "objectDomain": "Table",
  "objectId": 0,
  "objectName": "GOVERNANCE.TABLES.T2",
  "operationType": "ALTER",
  "properties": {
    "swapTargetDomain": {
      "value": "Table"
    },
    "swapTargetId": {
      "value": 0
    },
    "swapTargetName": {
      "value": "GOVERNANCE.TABLES.T3"
    }
  }
}
Copy

레코드 2:

{
  "objectDomain": "Table",
  "objectId": 0,
  "objectName": "GOVERNANCE.TABLES.T3",
  "operationType": "ALTER",
  "properties": {
    "swapTargetDomain": {
      "value": "Table"
    },
    "swapTargetId": {
      "value": 0
    },
    "swapTargetName": {
      "value": "GOVERNANCE.TABLES.T2"
    }
  }
}
Copy

마스킹 정책 삭제하기

마스킹 정책을 삭제합니다.

drop masking policy governance.policies.email_mask;
Copy

열 값:

{
  "objectDomain" : "MASKING_POLICY",
  "objectName": "governance.policies.email_mask",
  "objectId" : "1",
  "operationType": "DROP",
  "properties" : {}
}
Copy

참고

열 값은 대표적인 값이며 태그 및 행 액세스 정책에 대한 DROP 작업에 적용됩니다.

properties 필드는 빈 배열이며 DROP 작업 이전의 정책에 대해서는 어떤 정보도 제공하지 않습니다.

열에서 태그 참조 추적하기

object_modified_by_ddl 열을 쿼리하여 열에 태그가 설정되는 방식을 모니터링합니다.

테이블 관리자로서 열에 태그를 설정하고 태그를 설정 해제하고 다른 문자열 값으로 태그를 업데이트합니다.

alter table hr.tables.empl_info
  alter column email set tag governance.tags.test_tag = 'test';

alter table hr.tables.empl_info
  alter column email unset tag governance.tags.test_tag;

alter table hr.tables.empl_info
  alter column email set tag governance.tags.data_category = 'sensitive';
Copy

데이터 엔지니어로서 태그 값을 변경합니다.

alter table hr.tables.empl_info
  alter column email set tag governance.tags.data_category = 'public';
Copy

ACCESS_HISTORY 뷰를 쿼리하여 변경 사항을 모니터링합니다.

select
  query_start_time,
  user_name,
  object_modified_by_ddl:"objectName"::string as table_name,
  'EMAIL' as column_name,
  tag_history.value:"subOperationType"::string as operation,
  tag_history.key as tag_name,
  nvl((tag_history.value:"tagValue"."value")::string, '') as value
from
  TEST_DB.ACCOUNT_USAGE.access_history ah,
  lateral flatten(input => ah.OBJECT_MODIFIED_BY_DDL:"properties"."columns"."EMAIL"."tags") tag_history
where true
  and object_modified_by_ddl:"objectDomain" = 'Table'
  and object_modified_by_ddl:"objectName" = 'TEST_DB.TEST_SH.T'
order by query_start_time asc;
Copy

반환 결과:

+-----------------------------------+---------------+---------------------+-------------+-----------+-------------------------------+-----------+
| QUERY_START_TIME                  | USER_NAME     | TABLE_NAME          | COLUMN_NAME | OPERATION | TAG_NAME                      | VALUE     |
+-----------------------------------+---------------+---------------------+-------------+-----------+-------------------------------+-----------+
| Mon, Feb. 14, 2023 12:01:01 -0600 | TABLE_ADMIN   | HR.TABLES.EMPL_INFO | EMAIL       | ADD       | GOVERNANCE.TAGS.TEST_TAG      | test      |
| Mon, Feb. 14, 2023 12:02:01 -0600 | TABLE_ADMIN   | HR.TABLES.EMPL_INFO | EMAIL       | DROP      | GOVERNANCE.TAGS.TEST_TAG      |           |
| Mon, Feb. 14, 2023 12:03:01 -0600 | TABLE_ADMIN   | HR.TABLES.EMPL_INFO | EMAIL       | ADD       | GOVERNANCE.TAGS.DATA_CATEGORY | sensitive |
| Mon, Feb. 14, 2023 12:04:01 -0600 | DATA_ENGINEER | HR.TABLES.EMPL_INFO | EMAIL       | ADD       | GOVERNANCE.TAGS.DATA_CATEGORY | public    |
+-----------------------------------+---------------+---------------------+-------------+-----------+-------------------------------+-----------+

예: 저장 프로시저 호출하기

다음 저장 프로시저가 mydb.procedures 라는 스키마에 저장되어 있다고 가정해 보십시오.

create or replace procedure get_id_value(name string)
returns string not null
language javascript
as
$$
  var my_sql_command = "select id from A where name = '" + NAME + "'";
  var statement = snowflake.createStatement( {sqlText: my_sql_command} );
  var result = statement.execute();
  result.next();
  return result.getColumnValue(1);
$$
;
Copy

my_procedure 를 직접 호출 하면 다음과 같이 direct_objects_accessedbase_objects_accessed 열에 모두 프로시저 세부 정보가 기록됩니다.

[
  {
    "objectDomain": "PROCEDURE",
    "objectName": "MYDB.PROCEDURES.GET_ID_VALUE",
    "argumentSignature": "(NAME STRING)",
    "dataType": "STRING"
  }
]
Copy

이 예는 이 항목의 UDF 호출하기 와 유사합니다.

예: 저장 프로시저를 사용한 상위 쿼리

parent_query_idroot_query_id 열을 사용하여 저장 프로시저 호출이 서로 어떻게 관련되어 있는지 이해할 수 있습니다.

세 가지 다른 저장 프로시저 문이 있고 다음 순서로 실행한다고 가정합니다.

CREATE OR REPLACE PROCEDURE myproc_child()
RETURNS INTEGER
LANGUAGE SQL
AS
$$
  BEGIN
  SELECT * FROM mydb.mysch.mytable;
  RETURN 1;
  END
$$;

CREATE OR REPLACE PROCEDURE myproc_parent()
RETURNS INTEGER
LANGUAGE SQL
AS
$$
  BEGIN
  CALL myproc_child();
  RETURN 1;
  END
$$;

CALL myproc_parent();
Copy

ACCESS_HISTORY 뷰에 대한 쿼리를 실행하면 다음과 같은 정보가 기록됩니다.

SELECT
  query_id,
  parent_query_id,
  root_query_id,
  direct_objects_accessed
FROM
  SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY;
Copy
+----------+-----------------+---------------+-----------------------------------+
| QUERY_ID | PARENT_QUERY_ID | ROOT_QUERY_ID | DIRECT_OBJECTS_ACCESSED           |
+----------+-----------------+---------------+-----------------------------------+
|  1       | NULL            | NULL          | [{"objectName": "myproc_parent"}] |
|  2       | 1               | 1             | [{"objectName": "myproc_child"}]  |
|  3       | 2               | 1             | [{"objectName": "mytable"}]       |
+----------+-----------------+---------------+-----------------------------------+
  • 첫 번째 행은 direct_objects_accessed 열에 표시된 대로 myproc_parent 라는 두 번째 프로시저 호출에 해당합니다.

    이 저장 프로시저를 직접 호출했으므로 parent_query_idroot_query_id 열은 NULL을 반환합니다.

  • 두 번째 행은 direct_objects_accessed column 에 표시된 대로 myproc_child 라는 첫 번째 프로시저를 호출하는 쿼리에 해당합니다.

    myproc_child 를 호출하는 쿼리는 사용자가 직접 호출한 myproc_parent 를 호출하는 쿼리를 통해 시작되었으므로, parent_query_id 열과 root_query_id 열은 동일한 쿼리 ID를 반환합니다.

  • 세 번째 행은 direct_objects_accessed 열에 표시된 대로 myproc_child 프로시저에서 mytable 이라는 테이블에 액세스한 쿼리에 해당합니다.

    parent_query_id 열은 myproc_child 호출에 해당하는 mytable 에 액세스한 쿼리의 쿼리 ID를 반환합니다. 해당 저장 프로시저는 root_query_id 열에 표시된 myproc_parent 를 호출하는 쿼리로 시작되었습니다.