접근 이력(Access History)
접근 이력(Access History)
Access History는 Enterprise Edition(이상)이 필요해요. 업그레이드 문의는 Snowflake Support에 연락해 주세요.
이 주제는 Snowflake의 사용자 접근 이력에 대한 개념을 제공해요.
출처: Snowflake 문서
본문
개요
Snowflake의 접근 이력(Access History)은 사용자 쿼리가 데이터를 읽을 때와 SQL 문이 데이터 쓰기 작업(INSERT, UPDATE, DELETE 및 COPY 명령의 변형 포함)을 소스 데이터 객체에서 대상 데이터 객체로 수행할 때를 말해요. 사용자 접근 이력은 ACCOUNT_USAGE와 ORGANIZATION_USAGE 스키마의 ACCESS_HISTORY 뷰를 쿼리해 찾을 수 있어요. 이 뷰의 레코드는 규정 준수 감사를 용이하게 하고 인기 있고 자주 접근되는 테이블과 열에 대한 인사이트를 제공해요. 사용자(즉 쿼리 운영자), 쿼리, 테이블 또는 뷰, 열, 데이터 사이에 직접 연결이 있기 때문이에요.
ACCESS_HISTORY 뷰의 각 행은 SQL 문당 단일 레코드를 포함해요. 레코드는 다음 종류의 정보를 포함해요.
- 쿼리가 직접·간접적으로 접근한 소스 열(source columns) — 쿼리 데이터가 오는 기본 테이블과 같은
- 사용자가 쿼리 결과에서 보는 투영 열(projected columns) — 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열에 기록돼요.
참고(Note): Account Usage QUERY_HISTORY 뷰의 레코드가 항상 ACCESS_HISTORY 뷰에 기록되는 것은 아니에요. SQL 문의 구조가 Snowflake가 ACCESS_HISTORY 뷰에 항목을 기록할지 여부를 결정해요.
ACCESS_HISTORY 뷰가 지원하는 읽기·쓰기 작업에 대한 자세한 내용은 뷰 사용 참고 사항을 참고해요.
읽기·쓰기 작업 추적
ACCOUNT_USAGE와 ORGANIZATION_USAGE 두 스키마의 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 열, 테이블, 스테이지를 포함하는 데이터 쓰기 정보를 지정해요.
direct_objects_accessed, base_objects_accessed, objects_modified 열에서 Snowflake가 반환하는 정보는 Snowflake의 쿼리와 데이터베이스 객체가 생성된 방식에 따라 결정돼요.
마찬가지로 쿼리가 행 접근 정책으로 보호된 객체나 마스킹 정책으로 보호된 열을 참조하면 Snowflake는 policies_referenced 열에 정책 정보를 기록해요.
object_modified_by_ddl 열은 데이터베이스, 스키마, 테이블, 뷰, 열에 대한 DDL 작업을 기록해요. 이러한 작업에는 테이블이나 뷰에 행 접근 정책을, 열에 마스킹 정책을 지정하는 문과 객체나 열에 대한 태그 업데이트(예: 태그 설정, 태그 값 변경)도 포함돼요.
parent_query_id와 root_query_id 열은 다음에 해당하는 쿼리 ID를 기록해요.
- 다른 객체에 대해 읽기 또는 쓰기 작업을 수행하는 쿼리
- 중첩 저장 프로시저 호출을 포함해 저장 프로시저를 호출하는 객체에 대해 읽기 또는 쓰기 작업을 수행하는 쿼리. 자세한 내용은 조상 쿼리(ancestor queries)(이 주제에서)을 참고해요.
열 세부정보는 ACCESS_HISTORY 뷰의 열(Columns) 섹션을 참고해요.
읽기(Read)
읽기 쿼리와 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은 쿼리에 포함되지 않았고 view_2의 데이터 소스 역할을 하는 기본 객체가 아니므로 direct_objects_accessed 및 base_objects_accessed 열에 포함되지 않는다는 점을 주목해요.
쓰기(Write)
쓰기 작업과 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을 반환
지원되는 작업
ACCESS_HISTORY 뷰가 지원하는 읽기·쓰기 작업의 완전한 설명은 ACCESS_HISTORY 뷰의 사용 참고 사항 섹션을 참고해요.
단일 요청의 여러 문장
Snowflake는 여러 문장을 단일 요청으로 동시에 실행하는 것을 지원해요. 요청을 접근 이력에서 추적하는 방법은 Snowsight에서 실행되었는지 프로그래매틱하게 실행되었는지에 따라 달라져요.
- Snowsight로 여러 문장을 실행하면 쿼리를 하나씩 실행하고 마지막으로 실행된 쿼리의
query_id를 반환해요. 실행된 모든 문장과 그 반환 값은 ACCESS_HISTORY 뷰에서 찾을 수 있어요. - Snowflake Python 커넥터나 Snowflake SQL API 같은 기능은 여러 SQL 문을 단일 요청으로 결합하고 모든 문장에 대해 단일
query_id를 반환해요. 이 숫자는 실제로 모든 개별 문장의 상위 쿼리 ID(parent query id)예요. 요청을 구성한 각 문장의query_id를 반환하려면parent_query_id를 사용해 ACCESS_HISTORY 뷰를 쿼리해야 해요. 예를 들어 요청이query_id = 6789를 반환했다면 다음을 실행해 개별 문장의 쿼리 ID를 반환할 수 있어요.
SELECT query_id, parent_query_id, direct_objects_accessed
FROM snowflake.account_usage.access_history
WHERE parent_query_id = 6789;
이점
Snowflake의 접근 이력은 읽기·쓰기 작업과 관련해 다음 이점을 제공해요.
데이터 발견(Data discovery): 사용하지 않는 데이터를 발견해 데이터를 보관(archive)할지 삭제할지 결정해요.
민감 데이터 이동 추적(Track how sensitive data moves): 외부 클라우드 스토리지 위치(예: Amazon S3 버킷)에서 대상 Snowflake 테이블로, 그리고 그 반대로의 데이터 이동을 추적해요. Snowflake 테이블에서 다른 Snowflake 테이블로의 내부 데이터 이동도 추적해요.
민감 데이터의 이동을 추적한 후 데이터를 보호하기 위한 마스킹 및 행 접근 정책을 적용하고, 스테이지와 테이블에 대한 접근을 더 규제하기 위해 접근 제어 설정을 업데이트하고, 민감 데이터가 있는 스테이지, 테이블, 열이 규정 준수 요구사항을 위해 추적될 수 있도록 태그를 설정해요.
데이터 검증(Data validation): 데이터가 원래 소스로 추적될 수 있으므로 보고서, 대시보드, 차트·그래프 같은 데이터 시각화 제품의 정확성과 무결성이 검증돼요. 데이터 관리자(데이터 스튜어드)는 특정 테이블이나 뷰를 삭제하거나 변경하기 전에 사용자에게 알릴 수도 있어요.
규정 준수 감사(Compliance auditing): GDPR과 CCPA 같은 규정 준수를 위해 테이블이나 스테이지에서 쓰기 작업을 수행한 Snowflake 사용자와 작업이 발생한 시점을 식별해요.
전반적인 데이터 거버넌스 강화(Enhance overall data governance): ACCESS_HISTORY 뷰는 어떤 데이터가 접근됐는지, 언제 접근됐는지, 접근된 데이터가 소스 데이터 객체에서 대상 데이터 객체로 어떻게 이동했는지에 대한 통합된 그림을 제공해요.
Horizon Iceberg REST Catalog 작업의 접근 이력
ACCESS_HISTORY 뷰는 외부 쿼리 엔진(예: Spark, Trino, DuckDB, PyIceberg)이 Horizon Iceberg REST Catalog(IRC) API를 통해 Snowflake 관리 Apache Iceberg™ 테이블에 대해 수행하는 성공적인 작업도 기록해요. 이러한 레코드는 ACCESS_HISTORY가 Snowflake 내부에서 실행된 SQL 문에 대해 생성하는 항목을 보완하므로, 외부 엔진에서 시작된 읽기, DML, DDL을 Snowflake 측 활동과 함께 감사할 수 있어요.
IRC 출처 레코드는 event_source = 'horizon_irc'로 필터링해 식별할 수 있으며, additional_properties 열을 검사해 Iceberg 작업 유형(예: LoadTable, CreateTable, UpdateTable)을 확인할 수 있어요.
자세한 내용은 ACCESS_HISTORY의 Horizon Iceberg REST Catalog 작업을 참고해요.
열 계보(Column lineage)
열 계보(즉 열에 대한 접근 이력)는 Account Usage ACCESS_HISTORY 뷰를 확장해 쓰기 작업에서 데이터가 소스 열에서 대상 열로 어떻게 흐르는지 지정해요. Snowflake는 계보 체인의 객체가 삭제되지 않는 한 소스 열에서 그 소스 열의 데이터를 참조하는 모든 후속 테이블 객체(예: INSERT, MERGE, CTAS)를 통해 데이터를 추적해요. Snowflake는 ACCESS_HISTORY 뷰의 objects_modified 열을 강화해 열 계보를 접근 가능하게 해요.
열 계보는 다음 이점을 제공해요.
파생 객체 보호(Protect Derived Objects): 데이터 스튜어드는 파생 객체(예: CTAS)를 만든 후 추가 작업을 하지 않고도 민감 소스 열을 쉽게 태그 지정할 수 있어요. 그런 다음 데이터 스튜어드는 민감 열을 포함하는 테이블을 행 접근 정책으로 보호하거나, 민감 열 자체를 마스킹 정책 또는 태그 기반 마스킹 정책으로 보호할 수 있어요.
민감 열 복사 빈도(Sensitive Column Copy Frequency): 데이터 개인정보 보호 책임자는 민감 데이터를 포함하는 열의 객체 수(예: 테이블 1개, 뷰 2개)를 빠르게 확인할 수 있어요. 민감 데이터가 있는 열이 테이블 객체에 몇 번 나타나는지 알면 데이터 개인정보 보호 책임자는 규정 준수 표준(예: 유럽 연합의 GDPR 표준 충족)을 어떻게 충족하는지 증명할 수 있어요.
근본 원인 분석(Root Cause Analysis): 열 계보는 데이터를 소스로 추적하는 메커니즘을 제공해, 열악한 데이터 품질로 인한 실패 지점을 정확히 찾는 데 도움이 되고 문제 해결 과정 중 분석할 열 수를 줄일 수 있어요.
열 계보에 대한 추가 세부정보는 열 계보(이 주제에서)을 참고해요.
마스킹·행 접근 정책 참조
POLICIES_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)에 대한 복잡한 조인의 필요성이 사라져요.
계정 수준 대 조직 수준 접근 이력
관리자는 계정의 ACCOUNT_USAGE 스키마에서 ACCESS_HISTORY 뷰를 쿼리해 계정 수준에서 접근 이력을 모니터링해요. ACCOUNT_USAGE.ACCESS_HISTORY 뷰와 관련된 추가 비용은 없어요.
ORGANIZATION_USAGE 스키마의 ACCESS_HISTORY 뷰는 조직의 모든 계정의 접근 이력을 단일 뷰로 모아 조직 수준 접근 이력을 제공해요. 이 ORGANIZATION_USAGE.ACCESS_HISTORY 뷰는 조직 계정(organization account)에서만 찾을 수 있어요.
ORGANIZATION_USAGE 스키마의 조직 수준 접근 이력은 ACCOUNT_USAGE 스키마의 접근 이력과 다음 방식으로 달라요.
추가 열: 조직 계정의 ORGANIZATION_USAGE.ACCESS_HISTORY 뷰는 조직 리스팅(organizational listings)과 관련된 인사이트를 제공하는 추가 열을 포함해요. 이 열은 조직 리스팅에 연결된 데이터 제품 중 소비자의 쿼리에 의해 접근된 것과, 그러한 데이터 제품이 마스킹 정책 같은 정책으로 보호되는지 여부를 결정하는 데 사용할 수 있어요. 자세한 내용은 조직 리스팅 거버넌스를 참고해요.
추가 비용: 조직 계정의 ORGANIZATION_USAGE.ACCESS_HISTORY 뷰는 다음 비용이 발생하는 프리미엄 뷰예요.
- ACCESS_HISTORY 뷰를 채우는 서버리스 태스크와 관련된 컴퓨팅 비용
- ACCESS_HISTORY 뷰에 데이터를 저장하는 것과 관련된 스토리지 비용
이 비용에 대한 자세한 내용은 프리미엄 뷰와 관련된 비용을 참고해요.
지원되는 객체
SQL 문이 특정 유형의 객체를 포함할 때 ACCESS_HISTORY 뷰가 레코드를 포함하는지 결정하려면 다음 표를 사용해요. SQL 문에는 다음이 포함돼요.
- DML(Data Manipulation Language) 문. 예: 테이블에 데이터를 삽입하는 데 사용되는 문
- DQL(Data Query Language) 문. 예: SELECT 문으로 데이터를 투영하는 문
- DDL(Data Definition Language) 문. 예: Snowflake 객체를 생성하거나 변경하는 문
| 객체 | DML | DQL | DDL | 비고 |
|---|---|---|---|---|
| DATABASE | n/a | n/a | ✔ | |
| DYNAMIC TABLE | 부분 | ✔ | ✔ | DML 지원은 ALTER DYNAMIC TABLE ... REFRESH 명령에만 해당 |
| EXTERNAL TABLE | ✔ | ✔ | ✔ | |
| AGENT | n/a | n/a | ✔ | Cortex 에이전트 객체의 DDL(예: CREATE AGENT, ALTER AGENT, DROP AGENT) |
| FUNCTION | n/a | ✔ | ✔ | DQL 지원은 SELECT 문에 나타나는 함수로 제한 |
| ICEBERG TABLE | 부분 | ✔ | ✔ | Snowflake 관리 Apache Iceberg™ 테이블에 대한 전체 지원(DML, DQL, DDL). 외부 관리 Apache Iceberg™ 테이블에 대한 DQL과 DDL 지원만 |
| LISTING | n/a | n/a | ✔ | |
| MATERIALIZED VIEW | n/a | ✔ | ✔ | |
| MCP SERVER | n/a | n/a | ✔ | MCP 서버의 DDL(예: CREATE MCP SERVER, ALTER MCP SERVER, DROP MCP SERVER) |
| POLICY | n/a | ✔ | ✔ | DDL 지원은 정책이 객체에 적용될 때와 SHOW 및 DESCRIBE 명령으로 정책 메타데이터를 쿼리할 때 표시. DQL 지원은 쿼리가 실행될 때 적용 중인 정책을 표시 |
| POSTGRES INSTANCE | n/a | n/a | ✔ | Postgres 인스턴스의 DDL(예: CREATE POSTGRES INSTANCE, ALTER POSTGRES INSTANCE, DROP POSTGRES INSTANCE) |
| PROCEDURE | n/a | ✔ | ✔ | 프로시저는 여러 SQL 문을 가질 수 있으며 각 문은 별도 레코드를 생성 |
| ROLE | n/a | n/a | ✔ | |
| SCHEMA | n/a | n/a | ✔ | |
| SEQUENCE | n/a | ✔ | DML 비지원은 의도적 | |
| SESSION | n/a | n/a | ✔ | |
| SHARE | n/a | n/a | ✔ | |
| STAGE | 부분 | ✔ | DML 지원은 테이블의 소스로 스테이지를 사용하는 것으로 제한. DQL의 경우 스테이지에 대한 쿼리 지원 없음 | |
| STREAM | n/a | 부분 | ✔ | DQL 지원은 테이블의 소스로 스트림을 사용하는 것으로 제한. DDL 지원은 생성 작업으로 제한 |
| TABLE | ✔ | ✔ | ✔ | |
| TAG | n/a | n/a | ✔ | |
| VIEW | n/a | ✔ | ✔ |
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/"
}
민감 스테이지 데이터 이동 추적
일련의 쿼리를 시간 순서대로 실행하면서 민감 스테이지 데이터의 이동을 추적해요.
다음 쿼리를 실행해요. 다섯 문이 스테이지 데이터에 접근한다는 점을 주목해요. 따라서 스테이지 접근에 대해 ACCESS_HISTORY 뷰를 쿼리하면 결과 집합에 다섯 행이 포함되어야 해요.
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 열의 데이터는 다음 표에 나와 있어요.
[
{
"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"
}
]
쿼리 예시에 대해 다음을 주목해요.
- 재귀 공통 테이블 식을 사용해요.
- 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;
쿼리는 스테이지 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 | Table | ["CONTENT"] |
| TEST_DB.TEST_SCHEMA.S1–>TEST_DB.TEST_SCHEMA.T1–>TEST_DB.TEST_SCHEMA.S2 | TEST_DB.TEST_SCHEMA.S2 | 118 | Stage | [] |
| TEST_DB.TEST_SCHEMA.S1–>TEST_DB.TEST_SCHEMA.T1–>TEST_DB.TEST_SCHEMA.T2 | TEST_DB.TEST_SCHEMA.T2 | 66568 | Table | ["NAME","ID"] |
| TEST_DB.TEST_SCHEMA.S1–>TEST_DB.TEST_SCHEMA.T1–>TEST_DB.TEST_SCHEMA.T4 | TEST_DB.TEST_SCHEMA.T4 | 66572 | Table | ["ID","NAME"] |
| TEST_DB.TEST_SCHEMA.S1–>TEST_DB.TEST_SCHEMA.T3 | TEST_DB.TEST_SCHEMA.T3 | 66570 | Table | ["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 호출
두 숫자의 곱을 계산하는 다음 SQL UDF를 고려하고 mydb.udfs라는 스키마에 저장되어 있다고 가정해요.
CREATE FUNCTION MYDB.UDFS.GET_PRODUCT(num1 number, num2 number)
RETURNS number
AS
$$
NUM1 * NUM2
$$
;
get_product를 직접 호출하면 UDF 세부정보가 direct_objects_accessed 열에 기록돼요.
[
{
"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 함수를 다음에 기록해요.
- SQL 문에 함수가 명시적으로 이름이 지정되므로
direct_objects_accessed열, 그리고 - 함수가 열에 삽입되는 값의 소스이므로
objects_modified열의directSources배열
마찬가지로 테이블 t1은 같은 열에 기록돼요.
[
{
"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":[]
}
]
}
]
공유 UDF
공유 UDF는 직접 또는 간접으로 참조될 수 있어요.
- 직접 참조는 UDF를 명시적으로 호출(이 주제에서)하는 것과 동일하지만 UDF가
base_objects_accessed와direct_objects_accessed열 둘 다에 기록되는 결과를 낳아요. - 간접 참조의 예시는 뷰를 만들기 위해 UDF를 호출하는 것이에요.
create view v as
select get_product(c1, c2) as vc from t;
base_objects_accessed 열은 UDF와 테이블을 기록해요.
direct_objects_accessed 열은 뷰를 기록해요.
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"
}
}
}
}
참고(Note): 태그를 만들 때 허용 값을 지정하지 않으면
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
}
}
}
}
}
테이블 교체(Swap)
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" : {}
}
참고(Note): 열 값은 대표적이며 태그와 행 접근 정책의 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 열에 표시된 것처럼myproc_child라는 첫 번째 프로시저를 호출하는 쿼리에 해당해요.myproc_child를 호출하는 쿼리가 직접 호출한myproc_parent를 호출하는 쿼리에 의해 시작되었으므로parent_query_id와root_query_id열은 같은 쿼리 ID를 반환해요. - 세 번째 행은
direct_objects_accessed열에 표시된 것처럼myproc_child프로시저에서mytable이라는 테이블에 접근한 쿼리에 해당해요.parent_query_id열은mytable에 접근한 쿼리의 쿼리 ID를 반환하며, 이는myproc_child호출에 해당해요. 그 저장 프로시저는root_query_id열에 표시된myproc_parent를 호출하는 쿼리에 의해 시작됐어요.
시퀀스(Sequence)
시퀀스를 만드는 다음 SQL 문을 고려해요.
CREATE SEQUENCE SEQ
START = 2
INCREMENT = 7
COMMENT = 'Comment on sequence';
이 시퀀스를 만들면 접근 이력에 다음 항목이 생겨요.
{
"objectDomain": "Sequence",
"objectId": 1,
"objectName": "TEST_DB.TEST_SCHEMA.SEQ",
"operationType": "CREATE",
"properties": {
"start": {
"value": "2"
},
"increment": {
"value": "7"
},
"comment": {
"value": "Comment on Sequence"
}
}
}
조인(Join)
쿼리의 조인은 접근 이력에서 direct_accessed_objects 열의 joinObject로 나타나요. 접근 이력은 쿼리에서 명시적으로 언급된 조인만 추적하므로 joinObject는 다른 열에는 나타나지 않아요.
예를 들어 테이블 t1을 테이블 t2와 조인하는 다음 쿼리를 고려해요.
CREATE OR REPLACE VIEW v1 (vc1, vc2) AS
SELECT
t1.c1 AS vc1,
t2.c2 AS vc2
FROM t1 LEFT OUTER JOIN t2
ON t1.c2 = t2.c1;
이 쿼리를 실행하면 direct_accessed_objects 열의 t1 객체에 대해 다음이 나타나요.
{
"columns": [
{
"columnId": 0,
"columnName": "C1"
},
{
"columnId": 0,
"columnName": "C2"
}
],
"joinObjects": [
{
"joinType": "LEFT_OUTER_JOIN",
"node": {
"objectDomain": "Table",
"objectId": 0,
"objectName": "DB1.SCH.T2"
}
}
],
"objectDomain": "Table",
"objectId": 0,
"objectName": "DB1.SCH.T1"
}
참고(Note): 이 예시에서 접근 이력은
t2객체에 대한joinObject를 포함하지 않아요. 테이블t1의joinObject가 제공하는 정보와 중복되기 때문이에요.
더 알아보기 (Learn more)
- ACCESS_HISTORY 뷰(Account Usage) — 접근 이력 뷰
- 열 계보 — 열 계보 개념
- 마스킹 정책 — 민감 열 보호