액세스 기록¶
이 항목에서는 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
;
열 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;
뷰를 생성하려면 두 개의 서로 다른 테이블, 즉
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
마지막 열 objects_modified
가 Snowflake 열, 테이블 및 스테이지와 관련된 데이터 쓰기 정보를 지정하는 동안 처음 5개 열을 통해 읽기 작업이 추적됩니다.
Snowflake의 쿼리, 그리고 데이터베이스 오브젝트가 만들어진 방식에 따라, Snowflake가 direct_objects_accessed
, base_objects_accessed
및 objects_modified
열에 반환하는 정보가 결정됩니다.
마찬가지로, 쿼리가 행 액세스 정책으로 보호되는 오브젝트 또는 마스킹 정책으로 보호되는 열을 참조하는 경우 Snowflake는 정책 정보를 policies_referenced
열에 기록합니다.
object_modified_by_ddl
열은 데이터베이스, 스키마, 테이블, 뷰, 열에 대한 DDL 작업을 기록합니다. 또한 이러한 작업에는 테이블 또는 뷰에 대한 행 액세스 정책, 열에 대한 마스킹 정책 그리고 오브젝트나 열에 대한 태그 업데이트(예: 태그 설정, 태그 값 변경)를 지정하는 문도 포함됩니다.
parent_query_id
및 root_query_id
열은 다음에 해당하는 쿼리 ID를 기록합니다.
다른 오브젝트에 대해 읽기 또는 쓰기 작업을 수행하는 쿼리.
중첩된 저장 프로시저 호출을 비롯하여, 저장 프로시저를 호출하는 오브젝트에 대해 읽기 또는 쓰기 작업을 수행하는 쿼리. 자세한 내용은 이 항목의 상위 쿼리 를 참조하십시오.
열 세부 정보는 ACCESS_HISTORY 뷰의 열 섹션을 참조하십시오.
읽기¶
읽기 쿼리와 ACCESS_HISTORY 뷰가 이 정보를 기록하는 방법을 이해하려면 다음 시나리오를 고려하십시오.
일련의 오브젝트:
base_table
»view_1
»view_2
»view_3
.다음과 같은
view_2
에 대한 읽기 쿼리:select * from view_2;
이 예에서 Snowflake는 다음을 반환합니다.
쿼리가
view_2
를 지정하므로direct_objects_accessed
열의view_2
를 반환합니다.view_2
에서 데이터의 최초 원본이므로base_objects_accessed
열의base_table
을 반환합니다.
view_1
및 view_3
은 direct_objects_accessed
및 base_objects_accessed
열에 포함되지 않습니다. 이러한 뷰 중 어느 것도 쿼리에 포함되지 않았고, view_2
의 데이터에 대한 소스 역할을 하는 기본 오브젝트가 아니기 때문입니다.
쓰기¶
쓰기 작업과 ACCESS_HISTORY 뷰가 이 정보를 기록하는 방법을 이해하려면 다음 시나리오를 고려하십시오.
데이터 원본:
base_table
데이터 원본에서 테이블 만들기(즉, CTAS):
create table table_1 as select * from base_table;
이 예에서 Snowflake는 다음을 반환합니다.
테이블이 직접 액세스되었고 데이터의 원본이므로
base_objects_accessed
및direct_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
은 기본 테이블에서 만든 뷰입니다.v2
는v1
에서 만든 뷰입니다.
사용자가 v2
를 쿼리하는 경우 policies_referenced
열은 v2
를 보호하는 행 액세스 정책, v2
의 열을 보호하는 각 마스킹 정책 또는 적용 가능한 두 종류의 정책을 모두 기록합니다. 또한 이 열에서는 t1
및 v1
을 보호하는 모든 마스킹 정책 또는 행 액세스 정책을 기록합니다.
이러한 레코드는 데이터 관리자가 정책으로 보호되는 오브젝트에 액세스하는 방법을 이해하는 데 도움이 될 수 있습니다.
policies_referenced
열은 ACCESS_HISTORY 뷰에 추가적인 이점을 제공합니다.
지정된 쿼리에서 사용자가 액세스하는 정책으로 보호되는 오브젝트를 식별합니다.
정책 감사 프로세스를 간소화합니다.
ACCESS_HISTORY 뷰를 쿼리하면 사용자가 액세스하는 보호된 오브젝트와 보호된 열에 대한 정보를 얻기 위해 다른 Account Usage 뷰(예: POLICY_REFERENCES 및 QUERY_HISTORY)에 대한 복잡한 조인이 필요하지 않습니다.
지원되는 작업¶
ACCESS_HISTORY 뷰가 지원하는 읽기 및 쓰기 작업에 대한 전체 설명은 ACCESS_HISTORY 뷰 의 사용법 노트 섹션을 참조하십시오.
이점¶
Snowflake의 액세스 기록은 읽기 및 쓰기 작업과 관련하여 다음과 같은 이점을 제공합니다.
- 데이터 검색:
미사용 데이터를 발견하여 데이터의 보관 또는 삭제 여부를 결정합니다.
- 민감한 데이터의 이동 방식 추적:
외부 클라우드 저장소 위치(예: Amazon S3 버킷)에서 대상 Snowflake 테이블로, 또는 그 반대로의 데이터 이동을 추적합니다.
Snowflake 테이블에서 다른 Snowflake 테이블로의 내부 데이터 이동을 추적합니다.
민감한 데이터의 이동을 추적한 후, 데이터를 보호하기 위한 정책(마스킹 및 행 액세스)을 적용하고, 액세스 제어 설정 업데이트하여 스테이지와 테이블에 대한 액세스를 추가로 규제하고, 민감한 데이터가 있는 스테이지, 테이블, 열이 규정 준수 요구 사항에 맞는지 추적할 수 있도록 태그 를 설정합니다.
- 데이터 유효성 검사:
데이터를 원본과 비교해 추적할 수 있으므로 보고서, 대시보드 그리고 차트 및 그래프와 같은 데이터 시각화 제품의 정확성과 무결성이 검증됩니다.
데이터 관리자가 사용자에게 알린 후 주어진 테이블 또는 뷰를 삭제하거나 변경할 수도 있습니다.
- 규정 준수 감사:
테이블 또는 스테이지에서 쓰기 작업을 수행한 Snowflake 사용자와 GDPR 및 CCPA 와 같은 규정 준수 규제를 충족하려고 쓰기 작업이 발생한 시간을 식별합니다.
- 전반적인 데이터 거버넌스 강화:
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 ;
규정 준수 감사 촉진하기¶
다음 예는 규정 준수 감사를 용이하게 하는 데 도움이 됩니다.
다음과 같이
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()) ;
다음과 같이
object_id
값32998411400350
을 사용하여, 지난 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()) ;
object_id
값32998411400350
을 사용하여, 지난 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' ;
예: 쓰기 작업¶
아래 하위 섹션에서는 다음 사용 사례에 대한 쓰기 작업의 ACCESS_HISTORY 뷰를 쿼리하는 방법을 자세히 설명합니다.
스테이지에서 테이블로 데이터를 로딩합니다.
테이블에서 스테이지로 데이터를 언로딩합니다.
PUT 명령을 사용하여 로컬 파일을 스테이지에 업로드합니다.
GET 명령을 사용하여 스테이지에서 로컬 디렉터리로 데이터 파일을 불러옵니다.
민감한 스테이지 데이터 이동 추적.
스테이지에서 테이블로 데이터 로딩하기¶
외부 클라우드 저장소의 데이터 파일에서 대상 테이블의 열로 값 세트를 로딩합니다.
copy into table1(col1, col2) from (select t.$1, t.$2 from @mystage1/data1.csv.gz);
direct_objects_accessed
및 base_objects_accessed
열은 외부의 명명된 스테이지가 액세스되었음을 지정합니다.
{ "objectDomain": STAGE "objectName": "mystage1", "objectId": 1, "stageKind": "External Named" }
objects_modified
열은 데이터가 테이블의 두 열에 기록되었음을 지정합니다.
{ "columns": [ { "columnName": "col1", "columnId": 1 }, { "columnName": "col2", "columnId": 2 } ], "objectId": 1, "objectName": "TEST_DB.TEST_SCHEMA.TABLE1", "objectDomain": TABLE }
테이블에서 스테이지로 데이터 언로딩하기¶
Snowflake 테이블에서 클라우드 저장소로 값 세트를 언로딩합니다.
copy into @mystage1/data1.csv from table1;
direct_objects_accessed
및 base_objects_accessed
열은 액세스된 테이블 열을 지정합니다.
{ "objectDomain": TABLE "objectName": "TEST_DB.TEST_SCHEMA.TABLE1", "objectId": 123, "columns": [ { "columnName": "col1", "columnId": 1 }, { "columnName": "col2", "columnId": 2 } ] }
objects_modified
열은 액세스된 데이터가 기록된 스테이지를 지정합니다.
{ "objectId": 1, "objectName": "mystage1", "objectDomain": STAGE, "stageKind": "External Named" }
PUT 명령을 사용하여 로컬 파일을 스테이지에 업로드하기¶
데이터 파일을 내부(즉, Snowflake) 스테이지에 복사합니다.
put file:///tmp/data/mydata.csv @my_int_stage;
direct_objects_accessed
및 base_objects_accessed
열은 액세스된 파일의 로컬 경로를 지정합니다.
{ "location": "file:///tmp/data/mydata.csv" }
objects_modified
열은 액세스된 데이터가 기록된 스테이지를 지정합니다.
{ "objectId": 1, "objectName": "my_int_stage", "objectDomain": STAGE, "stageKind": "Internal Named" }
GET 명령을 사용하여 스테이지에서 로컬 디렉터리로 데이터 파일 불러오기¶
내부 스테이지에서 로컬 컴퓨터의 디렉터리로 데이터 파일을 불러옵니다.
get @%mytable file:///tmp/data/;
direct_objects_accessed
및 base_objects_accessed
열은 액세스된 스테이지 및 로컬 디렉터리를 지정합니다.
{ "objectDomain": Stage "objectName": "mytable", "objectId": 1, "stageKind": "Table" }
objects_modified
열은 액세스된 데이터가 기록된 디렉터리를 지정합니다.
{ "location": "file:///tmp/data/" }
민감한 스테이지 데이터 이동 추적하기¶
시간순으로 실행되는 일련의 쿼리를 통해 민감한 스테이지 데이터의 이동을 추적합니다.
다음 쿼리를 실행합니다. 이들 문 중 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;여기서
T1
,T2
…T7
은 테이블의 이름을 지정합니다.
S1
과S2
는 스테이지의 이름을 지정합니다.
액세스 기록을 쿼리하여 스테이지 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" } ] [ { "columns": [ { "columnId": 68610, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66564, "objectName": "TEST_DB.TEST_SCHEMA.T1" } ] [ { "columns": [ { "columnId": 68611, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66566, "objectName": "TEST_DB.TEST_SCHEMA.T6" } ] [ { "objectDomain": "Stage", "objectId": 117, "objectName": "TEST_DB.TEST_SCHEMA.S1", "stageKind": "External Named" } ] [ { "objectDomain": "Stage", "objectId": 117, "objectName": "TEST_DB.TEST_SCHEMA.S1", "stageKind": "External Named" } ] [ { "columns": [ { "columnId": 68610, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66564, "objectName": "TEST_DB.TEST_SCHEMA.T1" } ] [ { "columns": [ { "columnId": 68610, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66564, "objectName": "TEST_DB.TEST_SCHEMA.T1" } ] [ { "columns": [ { "columnId": 68610, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66564, "objectName": "TEST_DB.TEST_SCHEMA.T1" } ] [ { "columns": [ { "columnId": 68613, "columnName": "ID" }, { "columnId": 68612, "columnName": "NAME" } ], "objectDomain": "Table", "objectId": 66568, "objectName": "TEST_DB.TEST_SCHEMA.T2" } ] [ { "columns": [ { "columnId": 68610, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66564, "objectName": "TEST_DB.TEST_SCHEMA.T1" } ] [ { "columns": [ { "columnId": 68610, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66564, "objectName": "TEST_DB.TEST_SCHEMA.T1" } ] [ { "objectDomain": "Stage", "objectId": 118, "objectName": "TEST_DB.TEST_SCHEMA.S2", "stageKind": "External Named" } ] [ { "objectDomain": "Stage", "objectId": 117, "objectName": "TEST_DB.TEST_SCHEMA.S1", "stageKind": "External Named" } ] [ { "objectDomain": "Stage", "objectId": 117, "objectName": "TEST_DB.TEST_SCHEMA.S1", "stageKind": "External Named" } ] [ { "columns": [ { "columnId": 68614, "columnName": "CUSTOMER_INFO" } ], "objectDomain": "Table", "objectId": 66570, "objectName": "TEST_DB.TEST_SCHEMA.T3" } ] [ { "columns": [ { "columnId": 68610, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66564, "objectName": "TEST_DB.TEST_SCHEMA.T1" } ] [ { "columns": [ { "columnId": 68610, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66564, "objectName": "TEST_DB.TEST_SCHEMA.T1" } ] [ { "columns": [ { "columnId": 68615, "columnName": "NAME" }, { "columnId": 68616, "columnName": "ID" } ], "objectDomain": "Table", "objectId": 66572, "objectName": "TEST_DB.TEST_SCHEMA.T4" } ] [ { "columns": [ { "columnId": 68611, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66566, "objectName": "TEST_DB.TEST_SCHEMA.T6" } ] [ { "columns": [ { "columnId": 68611, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66566, "objectName": "TEST_DB.TEST_SCHEMA.T6" } ] [ { "columns": [ { "columnId": 68618, "columnName": "CONTENT" } ], "objectDomain": "Table", "objectId": 66574, "objectName": "TEST_DB.TEST_SCHEMA.T7" } ]쿼리 예제에 대해 다음 사항에 유의하십시오.
재귀 일반 테이블 식 을 사용합니다.
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;이 쿼리는 스테이지
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
;
반환 결과:
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 ;
예: 마스킹 정책 참조 추적¶
마스킹 정책이 중복 없이 열을 보호하는 경우 각 인스턴스의 행을 반환합니다. 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 ;
예: 쿼리에서 적용된 정책 추적¶
특정 기간에 특정 쿼리에 대해 정책이 업데이트된 시간(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')
여기서
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;
예: UDF¶
다음 UDF 예에서는 Account Usage ACCESS_HISTORY 뷰가 기록하는 방식을 보여줍니다.
UDF 호출¶
두 숫자의 곱을 계산해 그 값이 mydb.udfs
라는 스키마에 저장되어 있다고 가정하는 다음 SQL UDF를 생각해 보십시오.
CREATE FUNCTION MYDB.UDFS.GET_PRODUCT(num1 number, num2 number) RETURNS number AS $$ NUM1 * NUM2 $$ ;
get_product
를 직접 호출 하면 direct_objects_accessed
열에 UDF 세부 정보가 기록됩니다.
[ { "objectDomain": "FUNCTION", "objectName": "MYDB.UDFS.GET_PRODUCT", "objectId": "2", "argumentSignature": "(NUM1 NUMBER, NUM2 NUMBER)", "dataType": "NUMBER(38,0)" } ]
이 예는 이 항목의 저장 프로시저 호출하기 와 유사합니다.
INSERT DML을 포함한 UDF¶
mydb.tables.t1
이라는 테이블에서 1과 2라는 열을 업데이트하려면 다음 INSERT 문을 사용해 보십시오.
insert into t1(product) select get_product(c1, c2) from mydb.tables.t1;
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 } ] } ] [ { "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":[] } ] } ]
예: DDL 작업으로 수정된 오브젝트 추적하기¶
ALLOWED_VALUES로 태그 만들기¶
태그 만들기:
create tag governance.tags.pii allowed_values 'sensitive','public';
열 값:
{ "objectDomain": "TAG", "objectName": "governance.tags.pii", "objectId": "1", "operationType": "CREATE", "properties": { "allowedValues": { "sensitive": { "subOperationType": "ADD" }, "public": { "subOperationType": "ADD" } } } }
참고
태그 생성 시 허용되는 값을 지정하지 않으면 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');
열 값:
{ "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 } } } } } } }
태그에 대한 마스킹 정책 설정하기¶
태그에 대한 마스킹 정책을 설정합니다(즉, 태그 기반 마스킹).
alter tag governance.tags.pii set masking policy governance.policies.email_mask;
열 값:
{ "objectDomain": "TAG", "objectName": "governance.tags.pii", "objectId": "1", "operationType": "ALTER", "properties": { "maskingPolicies": { "governance.policies.email_mask": { "subOperationType": "ADD", "objectId": { "value": 2 } } } } }
테이블 바꾸기¶
t2
라는 테이블을 t3
이라는 테이블로 바꿉니다.
alter table governance.tables.t2 swap with governance.tables.t3;
뷰에 있는 서로 다른 두 레코드에 주목하십시오.
레코드 1:
{ "objectDomain": "Table", "objectId": 0, "objectName": "GOVERNANCE.TABLES.T2", "operationType": "ALTER", "properties": { "swapTargetDomain": { "value": "Table" }, "swapTargetId": { "value": 0 }, "swapTargetName": { "value": "GOVERNANCE.TABLES.T3" } } }
레코드 2:
{ "objectDomain": "Table", "objectId": 0, "objectName": "GOVERNANCE.TABLES.T3", "operationType": "ALTER", "properties": { "swapTargetDomain": { "value": "Table" }, "swapTargetId": { "value": 0 }, "swapTargetName": { "value": "GOVERNANCE.TABLES.T2" } } }
마스킹 정책 삭제하기¶
마스킹 정책을 삭제합니다.
drop masking policy governance.policies.email_mask;
열 값:
{ "objectDomain" : "MASKING_POLICY", "objectName": "governance.policies.email_mask", "objectId" : "1", "operationType": "DROP", "properties" : {} }참고
열 값은 대표적인 값이며 태그 및 행 액세스 정책에 대한 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';
데이터 엔지니어로서 태그 값을 변경합니다.
alter table hr.tables.empl_info alter column email set tag governance.tags.data_category = 'public';
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;
반환 결과:
+-----------------------------------+---------------+---------------------+-------------+-----------+-------------------------------+-----------+ | 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); $$ ;
my_procedure
를 직접 호출 하면 다음과 같이 direct_objects_accessed
및 base_objects_accessed
열에 모두 프로시저 세부 정보가 기록됩니다.
[ { "objectDomain": "PROCEDURE", "objectName": "MYDB.PROCEDURES.GET_ID_VALUE", "argumentSignature": "(NAME STRING)", "dataType": "STRING" } ]
이 예는 이 항목의 UDF 호출하기 와 유사합니다.
예: 저장 프로시저를 사용한 상위 쿼리¶
parent_query_id
및 root_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();
ACCESS_HISTORY 뷰에 대한 쿼리를 실행하면 다음과 같은 정보가 기록됩니다.
SELECT query_id, parent_query_id, root_query_id, direct_objects_accessed FROM SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY;+----------+-----------------+---------------+-----------------------------------+ | 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_id
및root_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
를 호출하는 쿼리로 시작되었습니다.