Prestandaöverväganden för ABAC-principer

Principer för radfilter och kolumnmask introducerar logik som körs vid frågetillfället, så prestanda beror på hur du utformar dina principer. Det finns ingen enda rätt metod för varje arbetsbelastning. Den bästa metoden beror på din datavolym, frågemönster, hur användarna interagerar med skyddade tabeller och önskat maskerings- eller filtreringsbeteende. Följande avsnitt beskriver de vanligaste prestandaövervägandena. Använd dem som en checklista när du utformar dina principer och testa med representativa frågor innan du distribuerar till produktion.

Prestandaöversikt

Att tänka på Description
Minska UDF-komplexiteten Komplex UDF-logik kan hämma frågeprestanda. enkla funktioner presterar bättre.
Metod för att rikta in sig på aktörer Bestäm om du vill implementera huvudbaserad logik i principens TO/EXCEPT satser eller inuti UDF med hjälp av identitetsfunktioner.
Använda deterministiska, felsäkra uttryck Icke-deterministiska funktioner och uttryck som kan utlösa fel minskar optimerarens möjlighet att cachelagra resultat och ordna om åtgärder.
Undvik Python-UDF:er Använd SQL UDF:er i stället för Python UDF:er när det är möjligt.
Håll uppslagstabeller små UDF:er som refererar till externa tabeller presterar bäst när dessa tabeller är tillräckligt små för att sändas.
Förstå predikat-pushdown på skyddade tabeller Frågeställningar mot skyddade tabeller kanske inte drar nytta av partitionsbeskärning eller dynamisk klustring om predikat har sideffekter.
Återanvänd kolumnmasker där det är möjligt Varje distinkt mask i en tabell lägger till omkostnader. Återanvändning av samma funktion mellan kolumner kan minska den.
Undvik regexmaskering på stora textfält Regex-baserad maskering på serialiserade dokument tvingar motorn att skanna och skriva om hela nyttolasten för varje rad.

Minska UDF-komplexiteten

UDF i en ABAC-policy körs för varje rad (radfilter) eller varje matchande kolumnvärde (kolumnmasker) under frågeutförandet. Komplexiteten i UDF påverkar direkt frågeprestanda.

Do:

  • Håll UDF:er enkla. Prioritera grundläggande CASE instruktioner och enkla booleska uttryck.
  • Referera endast till måltabellkolumner i UDF:er så mycket som möjligt. Detta möjliggör predikat-pushdown.
  • Om din UDF måste referera till externa tabeller bör du hålla alla externa referenser så små att de kan sändas. Kontrollera att refererade tabeller är optimerade och partitionerade för att matcha principens åtkomstmönster. Du kan till exempel partitionera en policyuppslagstabell baserat på användarnamn.
  • Undvik kapsling på flera nivåer och onödiga funktionsanrop. Använd inbyggda SQL-funktioner så mycket som möjligt.

Undvik:

  • Externa API-anrop eller sökningar till andra databaser i UDF:er. Nätverksanrop kan medföra ytterligare svarstider och tidsgränser.
  • Komplexa underfrågor eller kopplingar mot stora tabeller. Dessa förhindrar hash-kombinationer vid sändning och tvingar istället användning av nästlade looper-kopplingar.
  • Tung regeluttryck på stora textfält. Se Regex om stora textfält.
  • Metadatasökningar per rad, till exempel genom att fråga information_schema.

Metod för att rikta in sig på huvudanvändare

När du skriver en ABAC-princip bestämmer du var du ska implementera huvudbaserad logik: i principens TO/EXCEPT satser eller i UDF med hjälp av identitetsfunktioner som current_user() och .is_account_group_member()

I allmänhet använder du polisyns TO/EXCEPT satser för att definiera vilka aktörer en policy gäller för. Detta gör principdefinitionen enklare och UDF fokuserar på datatransformering, filtrering eller maskering. EXCEPT Satsen eliminerar principen helt för undantagna användare, vilket innebär att ingen UDF-execution för dessa användare.

Om den villkorliga logiken är för komplex för principens huvudsatser är identitetsfunktioner i UDF ett möjligt alternativ. Dessa funktioner löses en gång under frågeanalysen, inte per rad. Flera anrop till identitetsfunktioner som is_account_group_member() med olika gruppargument resulterar i ett enda UC API-anrop, så prestandapåverkan är vanligtvis minimal.

Följande UDF är effektivt eftersom det bara förlitar sig på identitetsfunktioner som löses en gång under frågeanalysen:

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;

Däremot är följande UDF långsammare eftersom det kodar behörigheter i en sekundär tabell, vilket kräver ytterligare en tabellsökning:

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;

Använda deterministiska, felsäkra uttryck

Använd deterministiska uttryck som inte kan utlösa fel i princip-UDF:er och i frågor mot skyddade tabeller.

Icke-deterministiska funktioner (funktioner som returnerar olika resultat för samma indata, till exempel rand() eller now()) förhindrar att optimeraren cachelagrar resultat eller tillämpar konstant vikning. Både SQL och Python UDF:er stöder nyckelordet DETERMINISTIC i instruktionen CREATE FUNCTION. För SQL UDF:er härleder optimeraren determinism från funktionstexten automatiskt, men du kan också ange den explicit. För Python UDF:er kan optimeraren inte inspektera funktionstexten, så det är viktigt att uttryckligen markera en Python UDF som deterministisk för att aktivera cachelagring av resultat för anrop med identiska argument.

Vissa uttryck utlöser fel om indata inte är giltiga, till exempel ANSI-division på en noll nämnare. När SQL-kompilatorn identifierar den här möjligheten kan den inte skicka åtgärder som filter nedåt i frågeplanen. Detta kan utlösa fel som visar information om värden innan filtrering eller maskering börjar gälla. Använd felsäkra alternativ som try_divide i stället för /, try_cast i stället för CASToch try_to_number i stället för to_number. Dessa returnerar NULL fel i stället för att kasta, vilket gör att optimeraren kan ordna om och vika uttryck fritt.

Avoid Python UDFs

Undvik Python-användardefinierade funktioner i ABAC-policyer när det är möjligt. Python UDF:er måste vara inkapslade i en SQL UDF för att användas i policys. De är också vanligtvis långsammare än SQL-UDF:er eftersom optimeraren inte kan infoga eller optimera dem, och funktionen Python körs för varje rad i måltabellen.

Om en Python UDF är oundviklig kan du läsa Deterministiska, felsäkra uttryck för hur du markerar det som DETERMINISTIC för att aktivera cachelagring av resultat.

Håll uppslagstabeller små

Ett vanligt mönster är att kontrollera åtkomsträttigheter mot en liten uppslagstabell (till exempel en tabell som mappar användare till tillåtna prioritetsnivåer). Om uppslagstabellen är betydligt mindre än måltabellen konverterar optimeraren underfrågan till en sändningshashkoppling. Uppslagstabellen kopieras till varje köre och lagras i minnet som en hashmap, vilket möjliggör snabb filtrering under tabellgenomsökningen. För ett kodexempel, se Uppslagstabeller i ABAC-policy-UDF:er.

  • Om uppslagstabellen är stor återgår optimeraren till en shuffle-koppling, vilket är långsammare.
  • Om uppslagspredikatet är komplext (inte en enkel likhetskontroll) kan också sändningskopplingen bli olämplig.
  • Även med broadcast hash-koppling medför varje rad fortfarande kostnaden för en hashtabellsökning under körningen.

Förstå predikat-pushdown på skyddade tabeller

Predikat pushdown är en prestandaoptimering där den överför dina filtervillkor till lagringsskiktet. På så sätt kan motorn hoppa över hela partitioner av data som inte matchar din fråga, vilket avsevärt minskar I/O och påskyndar körningen.

För tabeller som skyddas av radfilter och kolumnmasker är den här optimeringen mer komplex. Det här är den vanligaste källan till prestandaproblem med skyddade tabeller och den svåraste att åtgärda, eftersom principförfattare inte kan styra vilka frågor som användare kör mot skyddade tabeller.

Hur barriären SecureView påverkar predikatnedtryckning

Både ABAC och radfilter på tabellnivå och kolumnmasker använder en SecureView barriär för att förhindra att predikat med biverkningar skickas över principgränsen. Detta skyddar mot sidokanaldataläckage, men det kan också blockera partitionsrensning och optimering av flytande klustring, vilket kan tvinga fram fullständiga tabellgenomsökningar. Detta gäller även när principens UDF matchas till en konstant true (vilket innebär att inga rader faktiskt filtreras). Förekomsten av en politik på en tabell introducerar barriären SecureView .

Filter som påverkas av barriären

I allmänhet kan optimeraren endast skicka sidoeffektfria predikat genom barriären SecureView .

  • Nedtryckt (snabbt): Enkla likhetsjämförelser (WHERE col = 'value') och grundläggande intervalljämförelser (WHERE col > 100). Dessa är fria från biverkningar och riskerar inte att läcka data.
  • Blockerad (långsammare): Predikat som anropar funktioner (WHERE date_format(col, 'yyyy-MM-dd') = '1995-07-29') eller introducerar implicita typgjutningar. Dessa hålls ovanför barriären SecureView , vilket innebär att motorn måste skanna tabellen innan filtret tillämpas.

I följande exempel visas skillnaden. Överväg en tabell med en partitionsnyckel på o_orderdate och en fråga som filtrerar med :date_format

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

Utan en princip visas predikatet date_format i PartitionFiltersPhotonScan noden, vilket innebär att partitionsrensning är aktiv:

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

Med en policy (även en som alltid returnerar true) blockerar barriären predikatet SecureView. Den flyttas till en position ovanför genomsökningen i stället för att stanna kvar på PartitionFilters, vilket resulterar i en fullständig tabellskanning:

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

En enklare predikat som WHERE o_orderdate = '1995-07-29' har inga biverkningar och kan fortfarande skjutas ner även med barriären SecureView på plats:

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

Använd enkla likhetspredikat i skyddade tabeller när det är möjligt. För undantagna användare, använd klausulen EXCEPT i policyn för att eliminera barriären SecureView helt, vilket återställer full predicate pushdown.

Återanvänd kolumnmasker där det är möjligt

Om du tillämpar många distinkta kolumnmasker på en enda tabell blir kostnaden per kolumn mer sammansatt. Maskera endast kolumner som innehåller verkligt känsliga data.

När flera kolumner kräver samma transformering (till exempel redigera till NULL eller ersätta med en fast sträng) återanvänder du samma maskeringsfunktion i stället för att skapa en separat funktion per kolumn.

Azure Databricks identifierar principer som refererar till samma UDF med samma argument som samma effektiva mask, så återanvändning av funktioner undviker onödiga omkostnader.

Undvik regexmaskering på stora textfält

Det är dyrt att använda regexp_replace i en kolumnmask för att redigera element i ett serialiserat dokument (XML eller JSON som lagras som en STRING-kolumn). regexp_replace går hela strängen för varje rad. Optimeraren behandlar STRING-kolumnen som ett täckande värde och kan inte rensa oanvända delar av dokumentet. Motorn läser och skriver om hela nyttolasten även om frågan bara behöver några få fält.

-- 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;

Materialisera i stället de känsliga fälten i inskrivna kolumner i en separat tabell och tillämpa sedan kolumnmasker på dessa skalärkolumner. Maskfunktionen fungerar sedan på ett enda litet värde per rad i stället för hela det serialiserade dokumentet.

-- 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;

Om du kan lagra data som en struct-kolumn i stället för XML använder du variantmönstret för flexibel maskering för att redigera enskilda fält i structen. Se Kolumner i struct maskeras med VARIANT.

Testa UDF-prestanda

Testa i stor skala

Testa UDF-prestanda på minst 1 miljon rader innan du distribuerar till produktion. Förutom syntetiska skalningstester kör du frågor som representerar den faktiska arbetsbelastning som du förväntar dig i den skyddade tabellen. Gör inkrementella ändringar i dina principfunktioner och mät effekten av varje ändring i stället för att bara testa den slutliga versionen.

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;

Ersätt your_mask_function med den UDF som du testar. Jämför resultat med och utan den princip som tillämpas för att isolera principens omkostnader.