ABAC 정책에 대한 성능 고려 사항

행 필터 및 열 마스크 정책은 쿼리 시 실행되는 논리를 도입하므로 성능은 정책을 디자인하는 방법에 따라 달라집니다. 모든 워크로드에 대한 단일 올바른 접근 방식은 없습니다. 가장 좋은 방법은 데이터 볼륨, 쿼리 패턴, 사용자가 보호된 테이블과 상호 작용하는 방법 및 원하는 마스킹 또는 필터링 동작에 따라 달라집니다. 다음 섹션에서는 가장 일반적인 성능 고려 사항을 다룹니다. 정책을 디자인할 때 검사 목록으로 사용하고 프로덕션에 배포하기 전에 대표적인 쿼리로 테스트 합니다.

성능 개요

고려 사항 Description
UDF 복잡성 감소 복잡한 UDF 논리는 쿼리 성능을 억제할 수 있습니다. 단순 함수의 성능이 향상됩니다.
주체들을 겨냥한 접근 방법 정책의 TO/EXCEPT 절이나 ID 함수(신원 함수)를 사용하는 UDF 내에서 보안 주체 기반 논리를 구현할지를 결정합니다.
결정적이고 오류로부터 안전한 식 사용 오류를 발생시킬 수 있는 비결정적 함수와 식은 결과를 캐싱하고 작업을 다시 정렬할 수 있는 최적화 도구의 능력을 줄입니다.
Python UDF 사용을 피하십시오 가능하면 Python UDF 대신 SQL UDF를 사용하세요.
조회 테이블을 작게 유지 외부 테이블을 참조하는 UDF는 해당 테이블이 브로드캐스트할 수 있을 만큼 작을 때 가장 잘 수행됩니다.
보호된 테이블의 조건식 푸시다운 이해 조건자에 부작용이 있는 경우, 보호된 테이블에 대한 쿼리는 파티션 프루닝 또는 리퀴드 클러스터링의 혜택을 받을 수 없습니다.
열 마스크를 가능한 경우 재사용하십시오 테이블의 각 고유 마스크는 오버헤드를 추가합니다. 열에서 동일한 함수를 다시 사용하면 줄일 수 있습니다.
큰 텍스트 필드에서 regex 마스킹 방지 직렬화된 문서에 대한 Regex 기반 마스킹은 엔진이 모든 행에 대해 전체 페이로드를 검사하고 다시 작성하도록 합니다.

UDF 복잡성 감소

ABAC 정책의 UDF는 쿼리 실행 중에 모든 행(행 필터) 또는 일치하는 모든 열 값(열 마스크)에 대해 실행됩니다. UDF의 복잡성은 쿼리 성능에 직접적인 영향을 줍니다.

Do:

  • UDF를 단순하게 유지합니다. 기본 CASE 문과 단순 불리언 식을 사용하는 것을 선호하십시오.
  • UDF의 대상 테이블 열만 가능한 한 많이 참조합니다. 이렇게 하면 조건자 푸시다운이 가능합니다.
  • UDF가 외부 테이블을 참조해야 하는 경우 외부 참조를 브로드캐스트할 수 있을 만큼 작게 유지합니다. 참조된 테이블이 정책의 액세스 패턴과 일치하도록 최적화되고 분할되었는지 확인합니다. 예를 들어 사용자 이름으로 정책 조회 테이블을 분할합니다.
  • 다단계 중첩 및 불필요한 함수 호출을 방지합니다. 가능한 한 기본 제공 SQL 함수를 사용합니다.

피하십시오:

  • 외부 API는 UDF의 다른 데이터베이스를 호출하거나 조회합니다. 네트워크 호출은 추가 대기 시간 및 시간 제한을 도입할 수 있습니다.
  • 큰 테이블에 대한 복잡한 하위 쿼리나 조인. 브로드캐스트 해시 조인을 방지하고 중첩된 루프 조인을 강제 적용합니다.
  • 큰 텍스트 필드에서 무거운 정규식을 사용합니다. 큰 텍스트 필드의 Regex를 참조하세요.
  • 행별 메타데이터 조회(예: 쿼리 information_schema)

보안 주체를 대상으로 하는 방법

ABAC 정책을 작성할 때, 보안 주체 기반 논리를 구현할 위치를 결정합니다. 정책의 TO/EXCEPT 절에서 구현하거나, current_user()is_account_group_member()와 같은 ID 함수를 사용하여 UDF 내에서 구현할 수 있습니다.

일반적으로 정책의 TO/EXCEPT 절을 사용하여 정책이 적용되는 주체들을 정의합니다. 이렇게 하면 정책 정의가 더 단순해지고 UDF가 데이터 변환, 필터링 또는 마스킹에 중점을 줍니다. 이 절은 EXCEPT 제외 사용자에 대한 정책을 완전히 제거하므로 해당 사용자에 대한 UDF 실행이 없습니다.

조건부 논리가 정책의 주 절에 대해 너무 복잡할 경우 UDF 내의 ID 함수가 가능한 대안입니다. 이러한 함수는 쿼리 분석 중에 행마다가 아닌 한 번만 실행됩니다. 여러 그룹 인수와 같은 is_account_group_member() ID 함수에 대한 여러 호출은 단일 UC API 호출을 발생하므로 성능에 미치는 영향은 일반적으로 최소화됩니다.

다음 UDF는 쿼리 분석 중에 한 번 확인되는 ID 함수에만 의존하므로 효율적입니다.

CREATE OR REPLACE FUNCTION rowfilter()
RETURNS BOOLEAN
RETURN
  CASE
    WHEN is_account_group_member('auditors') OR is_account_group_member('external-auditors') THEN true
    WHEN is_account_group_member('low-privileged') THEN false
    WHEN session_user() = 'admin@organization.com' THEN true
    ELSE false
  END;

반면, 다음 UDF는 추가 테이블 조회가 필요한 보조 테이블의 권한을 인코딩하므로 속도가 느립니다.

CREATE OR REPLACE FUNCTION rowfilter()
RETURNS BOOLEAN
RETURN
  CASE WHEN EXISTS(SELECT 1 FROM access_lease WHERE user = session_user()) THEN true
  ELSE false END;

결정적이고 오류로부터 안전한 식 사용

정책 UDF 및 보호된 테이블에 대한 쿼리에서 오류를 발생시키지 않는 결정적 표현을 사용합니다.

비결정적 함수(예: rand() 또는 now() 같은 입력에 대해 다른 결과를 반환하는 함수)는 최적화 컴파일러가 결과를 캐싱하거나 상수 폴딩을 적용하지 못하게 합니다. SQL 및 Python UDF는 모두 DETERMINISTIC 구문에 CREATE FUNCTION 키워드를 지원합니다. SQL UDF의 경우 최적화 프로그램은 함수 본문에서 결정성을 자동으로 파생하지만 명시적으로 설정할 수도 있습니다. Python UDF의 경우 최적화 프로그램은 함수 본문을 검사할 수 없으므로 Python UDF를 결정적으로 명시적으로 표시하는 것은 동일한 인수를 사용하는 호출에 결과 캐싱을 사용하도록 설정하는 것이 중요합니다.

입력이 유효하지 않은 경우, 분모가 0인 ANSI 나누기와 같은 일부 연산에서 오류가 발생합니다. SQL 컴파일러가 이러한 가능성을 감지하면 쿼리 계획에서 필터와 같은 작업을 푸시할 수 없습니다. 이렇게 하면 필터링 또는 마스킹이 적용되기 전에 값에 대한 정보를 표시하는 오류가 트리거됩니다. 대신 오류로부터 안전한 대체(예: try_divide/대신 , try_castCAST 대신 )try_to_numberto_number를 사용합니다. 실패 시 예외를 던지는 대신 NULL을 반환함으로써 최적화 프로그램이 식을 자유롭게 재배열하고 결합할 수 있게 합니다.

Python UDFs를 피하십시오

가능하면 ABAC 정책에서 Python UDF 사용을 피하십시오. 정책에 사용하려면 Python UDF를 SQL UDF로 래핑하여야 합니다. 또한 최적화 프로그램이 인라인 또는 최적화할 수 없고 Python 함수가 대상 테이블의 모든 행에 대해 실행되므로 일반적으로 SQL UDF보다 느립니다.

Python UDF를 피할 수 없는 경우 결정적이고 오류로부터 안전한 식DETERMINISTIC 표시하여 결과 캐싱을 사용하도록 설정하는 방법을 참조하세요.

조회 테이블을 작게 유지

일반적인 패턴은 작은 조회 테이블(예: 사용자를 허용된 우선 순위 수준에 매핑하는 테이블)에 대한 액세스 권한을 확인하는 것입니다. 조회 테이블이 대상 테이블보다 훨씬 작은 경우 최적화 프로그램은 하위 쿼리를 브로드캐스트 해시 조인으로 변환합니다. 조회 테이블은 각 실행기에 복사되고 메모리에 해시맵으로 저장되므로 테이블 검색 중에 빠르게 필터링할 수 있습니다. 코드 예제는 ABAC 정책 UDF의 조회 테이블을 참조하세요.

  • 조회 테이블이 크면 최적화 프로그램이 순서 섞기 조인으로 돌아가서 속도가 느립니다.
  • 조회 조건자가 복잡한 경우(단순 같음 검사가 아님) 브로드캐스트 조인도 부적격 상태가 될 수 있습니다.
  • 브로드캐스트 해시 조인을 사용하는 경우에도 각 행에는 실행 중에 해시 테이블 조회 비용이 발생합니다.

보호된 테이블의 프레디케이트 푸시다운 이해

성능 최적화 기법 중 하나인 조건자 푸시다운은 엔진이 필터 조건을 스토리지 계층으로 푸시하는 것입니다. 이렇게 하면 엔진이 쿼리와 일치하지 않는 데이터의 전체 파티션을 건너뛰어 I/O를 크게 줄이고 실행 속도를 높일 수 있습니다.

행 필터 및 열 마스크로 보호되는 테이블의 경우 이 최적화가 더 복잡합니다. 이는 보호된 테이블의 성능 문제의 가장 일반적인 원인이며 정책 작성자는 사용자가 보호된 테이블에 대해 실행하는 쿼리를 제어할 수 없으므로 해결하기가 가장 어렵습니다.

장벽이 SecureView 프레디케이트 푸시다운에 미치는 영향

ABAC 및 테이블 수준 행 필터와 열 마스크는 모두 정책 경계 너머로 부작용이 있는 조건이 전달되지 않도록 장벽을 SecureView 사용합니다. 이렇게 하면 사이드 채널 데이터 누출을 방지할 수 있지만 파티션 정리 및 액체 클러스터링 최적화를 차단하여 전체 테이블 검사를 강제할 수 있습니다. 이는 정책 UDF가 상수 true 로 확인되는 경우에도 적용됩니다(실제로 필터링된 행이 없음). 테이블에 정책이 적용되면 SecureView 장벽이 생깁니다.

장벽의 영향을 받는 필터

일반적으로 최적화 프로그램은 SecureView 장벽을 통과하여 부작용이 없는 조건자만 밀어넣을 수 있습니다.

  • 푸시다운(빠른): 단순 같음 비교(WHERE col = 'value') 및 기본 범위 비교(WHERE col > 100). 이들은 부작용이 없으며 데이터 유출 위험을 감수하지 않습니다.
  • 차단됨(느린 속도): 함수를WHERE date_format(col, 'yyyy-MM-dd') = '1995-07-29' 호출하거나 암시적 형식 캐스트를 도입하는 조건자입니다. SecureView 장벽 위에 유지됩니다. 따라서 엔진은 필터를 적용하기 전에 테이블을 스캔해야 합니다.

다음 예제에서는 차이점을 보여 있습니다. o_orderdate이(가) 파티션 키인 테이블과 date_format을(를) 사용하여 필터링하는 쿼리를 고려합니다.

EXPLAIN SELECT * FROM orders
WHERE date_format(o_orderdate, 'yyyy-MM-dd') = '1995-07-29'

정책이 없으면 조건자가 date_format 노드 내부에 PartitionFilters 나타납니다PhotonScan. 즉, 파티션 정리가 활성화되어 있습니다.

+- PhotonScan parquet orders[...]
   PartitionFilters: [isnotnull(o_orderdate),
   (date_format(cast(o_orderdate as timestamp), yyyy-MM-dd, ...))]

정책(항상 true을 반환하는 경우를 포함)을 사용하면, SecureView 장벽이 프레디케이트를 차단합니다. PhotonFilterPartitionFilters에 머무르지 않고 검색 위로 이동하여 전체 테이블 검색을 초래합니다.

+- PhotonFilter (date_format(cast(o_orderdate as timestamp),
   yyyy-MM-dd, ...) = 1995-07-29)
    +- PhotonSecureView orders
        +- PhotonScan parquet orders[...]
           PartitionFilters: [isnotnull(o_orderdate)]

간단한 조건자인 WHERE o_orderdate = '1995-07-29'는 부작용이 없어서, SecureView 장벽이 있어도 여전히 아래로 밀어 넣을 수 있습니다.

+- PhotonSecureView orders
    +- PhotonScan parquet orders[...]
       PartitionFilters: [isnotnull(o_orderdate),
       (o_orderdate = 1995-07-29)]

가능하면 보호된 테이블에서 단순 같음 조건자를 사용합니다. 예외 사용자의 경우, 정책의 EXCEPT 절을 사용하여 SecureView 장벽을 완전히 제거함으로써 조건자 푸시다운 기능을 복원합니다.

열 마스크를 가능한 경우에 재사용하세요

단일 테이블에 여러 개의 개별 열 마스크를 적용하면 열당 비용이 증가합니다. 정말 중요한 데이터가 포함된 열만 마스킹합니다.

여러 열에 동일한 변환(예 NULL : 수정 또는 고정 문자열로 바꾸기)이 필요한 경우 열당 별도의 함수를 만드는 대신 동일한 마스킹 함수를 다시 사용합니다.

Azure Databricks 동일한 유효 마스크와 동일한 인수를 사용하여 동일한 UDF를 참조하는 정책을 인식하므로 함수를 다시 사용하면 불필요한 오버헤드가 방지됩니다.

큰 텍스트 필드에서 regex 마스킹 방지

열 마스크 내에서 직렬화된 문서(STRING 열로 저장된 XML 또는 JSON) 내의 요소를 수정하는 데는 regexp_replace 비용이 많이 듭니다. regexp_replace 는 모든 행에 대해 문자열을 전체 탐색합니다. 최적화 프로그램은 STRING 열을 불투명 값으로 처리하며 문서의 사용되지 않은 부분을 정리할 수 없습니다. 엔진은 쿼리에 몇 개의 필드만 필요한 경우에도 전체 페이로드를 읽고 다시 작성합니다.

-- Expensive: regex masking on serialized XML
CREATE FUNCTION mask_xml_pii(raw_xml STRING)
RETURNS STRING
RETURN CASE
  WHEN is_account_group_member('sensitive_data_viewers') THEN raw_xml
  ELSE regexp_replace(raw_xml, '<SSN>[^<]*</SSN>', '<SSN>***</SSN>')
END;

대신 중요한 필드를 별도의 테이블의 형식화된 열로 구체화한 다음 해당 스칼라 열에 열 마스크를 적용합니다. 그런 다음 마스크 함수는 전체 직렬화된 문서가 아닌 행당 단일 작은 값으로 작동합니다.

-- Source table stores raw XML as STRING
-- Example XML: <person><SSN>123-45-6789</SSN><name>Alice</name><dob>1990-01-01</dob></person>

-- Recommended: extract fields into a table, then mask scalar values
CREATE TABLE person_data AS
SELECT
  id,
  xpath_string(raw_xml, 'person/SSN') AS ssn,
  xpath_string(raw_xml, 'person/name') AS name,
  xpath_string(raw_xml, 'person/dob') AS date_of_birth,
  raw_xml
FROM raw_records;

-- Simple scalar mask, applied to each extracted column
CREATE FUNCTION redact(val STRING) RETURNS STRING
RETURN CASE
  WHEN is_account_group_member('sensitive_data_viewers') THEN val
  ELSE '***'
END;

데이터를 XML 대신 구조체 열로 저장할 수 있는 경우 VARIANT 유연한 마스킹 패턴을 사용하여 구조체 내의 개별 필드를 수정합니다. VARIANT가 있는 마스크 구조체 열을 참조하세요.

UDF 성능 테스트

대규모 테스트

프로덕션에 배포하기 전에 1백만 개 이상의 행에서 UDF 성능을 테스트합니다. 가상 크기 조정 테스트 외에도 보호된 테이블에서 예상하는 실제 워크로드를 나타내는 쿼리를 실행합니다. 정책 함수를 증분 방식으로 변경하고 최종 버전만 테스트하는 대신 각 변경의 영향을 측정합니다.

WITH test_data AS (
  SELECT
    id,
    your_mask_function(id) AS masked_id,
    current_timestamp() AS ts
  FROM (
    SELECT CONCAT('ID', LPAD(CAST(id AS STRING), 6, '0')) AS id
    FROM range(1000000)
  )
)
SELECT
  COUNT(*) AS rows_processed,
  MAX(ts) - MIN(ts) AS total_duration
FROM test_data;

테스트 중인 UDF로 your_mask_function를 바꿉니다. 정책 오버헤드를 격리하기 위해 적용된 정책과 없는 결과를 비교합니다.