このページでは、ABAC 行フィルターおよび列マスク ポリシーを実装するための一般的なパターンについて説明します。
- 全体的な概念については、 属性ベースのアクセス制御 (ABAC) のコア概念を参照してください。
- ポリシー構文については、「 行フィルターポリシーと列マスク ポリシーの作成と管理」を参照してください。
- GRANTポリシーについては、ABACGRANTポリシーをご覧ください。
- ご利用の環境で RBAC を使用している場合は、ユーザーがロールを引き受けたときに ID 関数がどのように動作するか、および RBAC と ABAC を組み合わせるパターンについては、 ABAC での RBAC の使用に関するページを参照してください。
キャスト互換マスク関数
Azure Databricksは、マスク関数の出力をターゲット列のデータ型と一致するように自動的にキャストします。 列マスクの自動型キャストを参照してください。
次のパターンは、キャスト互換のマスク関数を設計するのに役立ちます。
キャスト可能な型を返す
列をマスクする場合は、同じデータ型またはそれにキャスト可能な型を返します。 ポリシーの対象となる列のデータ型を確認し、関数のすべての分岐で互換性のある値が返されることを確認します。
-- Succeeds: Masks a DOUBLE column, returns DOUBLE in every branch
CREATE FUNCTION mask_salary(salary DOUBLE, user_role STRING)
RETURNS DOUBLE
RETURN CASE
WHEN user_role IN ('admin', 'hr') THEN salary
WHEN user_role = 'manager' THEN ROUND(salary / 1000) * 1000
ELSE 0.0
END;
-- Fails: 'CONFIDENTIAL' cannot be cast to a DOUBLE column type
CREATE FUNCTION mask_salary_as_text(salary DOUBLE, user_role STRING)
RETURNS STRING
RETURN CASE
WHEN user_role IN ('admin', 'hr') THEN CAST(salary AS STRING)
ELSE 'CONFIDENTIAL'
END;
数値オーバーフローを回避する
mask 関数が受け入れて、ターゲット列よりも広い数値型を返すと、結果は列の型に自動的にキャストバックされます。 返された値がより狭い型の範囲を超えると、キャストがオーバーフローし、実行時にクエリが失敗します。
-- The target column is TINYINT (max 127). The input is upcast to BIGINT
-- for the function. Adding 1000 produces a BIGINT result that overflows
-- when cast back to TINYINT.
CREATE FUNCTION mask_score(score BIGINT)
RETURNS BIGINT
RETURN score + 1000;
複数の列の型に VARIANT を使用する
「 単一の関数で複数カラムタイプをマスキングする」を参照してください。
キャスト互換性のテスト
さまざまなデータ パターンでマスク関数をテストします。
SELECT CAST(mask_salary(salary, 'admin') AS DOUBLE) FROM employees;
SELECT CAST(mask_salary(salary, 'manager') AS DOUBLE) FROM employees;
SELECT CAST(mask_salary(salary, 'viewer') AS DOUBLE) FROM employees;
単一の関数で複数の列タイプをマスキングする方法
VARIANTを受け入れ返す単一のマスキングUDFは、多くのデータタイプの列をマスキングできるため、維持するUDFやポリシーの数が減ります。 Azure Databricks関数を実行する前にカラム値をVARIANTにキャストし、その後ANSI SQLルールに従ってリターン値をカラムの型に戻します。
関数内で schema_of_variant() を使って値を確認し、その型に応じて処理を分岐します。 各分岐で、ターゲット列の型にキャスト可能な値を返します。
複数の数値型をマスクする
最も単純なVARIANTマスクは、Azure Databricks が各列の型にキャストする単一の定数を返します。 この関数を使うポリシーは、 INT、 DOUBLE、 DECIMAL 列を各精度ごとに別々の関数なしでマスキングできます。
CREATE FUNCTION mask_numeric(val VARIANT)
RETURNS VARIANT
DETERMINISTIC
RETURN 0::VARIANT;
タイプごとにマスク値を変化させるには、 schema_of_variant() を分岐し、それぞれに適切な値を返します:
CREATE FUNCTION flexible_mask(data VARIANT)
RETURNS VARIANT
RETURN CASE
WHEN schema_of_variant(data) = 'BIGINT' THEN 0::VARIANT
WHEN schema_of_variant(data) = 'DATE' THEN DATE'1970-01-01'::VARIANT
WHEN schema_of_variant(data) = 'DOUBLE' THEN 0.00::VARIANT
ELSE NULL::VARIANT
END;
整数型はVARIANTでBIGINTに広がるため、INTやTINYINTではなくBIGINTに分岐します。
STRUCT、ARRAY、MAPの各列をマスクする
VARIANTアプローチは複素型列もマスキングし、上記の数値例をネストデータに拡張します。
STRUCT、ARRAY、またはMAP列はVARIANTとして関数に到達し、関数が返すものをAzure Databricks列の宣言型にキャストします。 返される値は入力と同じスキーマでなければならず、そうでなければキャストが失敗しクエリエラーが発生します。
Note
STRUCT カラムはDatabricks Runtime 18.1以降でサポートされています。
ARRAY また、Databricks Runtime 19以降では MAP 列がサポートされています。 このキャスティングはABACのカラムマスクおよび行フィルターポリシー内でのみ機能し、一般的なSQLやテーブルレベルのマスクでは動作しません。
以下の例は特定の STRUCT、 ARRAY、 MAP 型をマスクしています。 マスクするスキーマごとに WHEN ブランチを追加します:
CREATE OR REPLACE FUNCTION generic_mask(val VARIANT)
RETURNS VARIANT
RETURN CASE
-- STRUCT<id: INT, ssn: STRING>: keep id, redact ssn
WHEN schema_of_variant(val) = 'OBJECT<id: BIGINT, ssn: STRING>' THEN
to_variant_object(named_struct('id', val:id, 'ssn', 'xxx-xx-xxxx'))
-- ARRAY<STRING>: return a single redacted element
WHEN schema_of_variant(val) = 'ARRAY<STRING>' THEN
to_variant_object(array('redacted'))
-- MAP<STRING, STRING>: redact every value
WHEN schema_of_variant(val) = 'OBJECT<key1: STRING, key2: STRING>' THEN
to_variant_object(map('key1', 'redacted', 'key2', 'redacted'))
ELSE NULL::VARIANT
END;
各 WHEN 部門は以下のセクションで説明する2つの役割を担っています。
-
バリアントスキーマを特定してください。 各
VARIANTスキーマには独自のマスキングロジックが必要なため、関数は返schema_of_variant(val)戻るスキーマ文字列上で分岐して各スキーマに適切な編集を適用します。 -
マスクされた値を再構築する。 列の型の黒塗り値を作り、
to_variant_object()でラップしてVARIANTを返します。
スキーマが分岐と一致しない値は ELSEに落ちます。
VARIANTスキーマを特定する
カラムの宣言型ではなく、値の VARIANT スキーマと照合してください。 VARIANT にキャストするとデータが正規化されるため、スキーマはカラムの定義と異なる場合があります。 以下の例は一般的なケースを示しています。
| 列の種類 |
schema_of_variant() の結果 |
Notes |
|---|---|---|
ARRAY<STRING> |
ARRAY<STRING> |
|
ARRAY<INT> |
ARRAY<BIGINT> |
整数型は BIGINTに広がります。 |
STRUCT<id: INT, name: STRING> |
OBJECT<id: BIGINT, name: STRING> |
STRUCT
MAP列は両方ともにOBJECTになります。 |
STRUCT<name: STRING, id: INT> |
OBJECT<id: BIGINT, name: STRING> |
フィールドは宣言順ではなく、キーのアルファベット順に並べ替えられます。 |
ARRAY<STRUCT<id: INT, name: STRING>> |
ARRAY<OBJECT<id: BIGINT, name: STRING>> |
|
{a: 'x'} を保持している MAP<STRING, STRING> |
OBJECT<a: STRING> |
行ごとに異なり、各行のキーがスキーマを決定します。 |
{a: 'x', b: 'y'} を保持している MAP<STRING, STRING> |
OBJECT<a: STRING, b: STRING> |
同じ列の別の行が異なるスキーマを生成します。 |
{a: null} を保持している MAP<STRING, STRING> |
OBJECT<a: VOID> |
ヌル値は VOID になります。 |
{a: ...} を保持している MAP<STRING, STRUCT<id: INT, name: STRING>> |
OBJECT<a: OBJECT<id: BIGINT, name: STRING>, ...> |
マスク値の再構築
一致する各スキーマに対して、同じ構造で黒塗り値を作り、それを to_variant_object()でラップします。
-
named_struct()を使ってSTRUCTを再構築し、欲しいフィールドは保持し、残りは置き換えます。 -
array()を使ってARRAYを再構築しましょう。 -
map()を使って、黒塗りされた値を使ったMAPを再構築してください。
Azure Databricksは返されたVARIANTをカラムの宣言された型にキャストするため、再構築された各値はその型にキャスプト可能でなければなりません。
VARIANTマスクのテスト
関数をポリシーに付ける前に、マスクされた出力を確認するためにプレーンクエリでテストできます。 以下の関数は ARRAY<STRUCT<id: BIGINT, value: FLOAT>> 列をマスクし、他のスキーマに対してエラーを発生させます。
CREATE OR REPLACE FUNCTION mask_points(v VARIANT)
RETURNS VARIANT
RETURN CASE
WHEN schema_of_variant(v) = 'ARRAY<OBJECT<id: BIGINT, value: FLOAT>>' THEN
to_variant_object(array(named_struct('id', 1, 'value', 2.1)))
ELSE raise_error('Unexpected VARIANT schema: ' || schema_of_variant(v))
END;
to_variant_object()でカラムを変換し、マスキング関数を適用してvariant_get()でマスクされたVARIANTをカラムの型にキャストします。 これは実行時にポリシーが行う動作を反映しています:
SELECT variant_get(mask_points(to_variant_object(points)), '$', typeof(points)) AS masked
FROM my_catalog.my_schema.my_table;
Limitations
-
CHAR、VARCHAR、GEOMETRY、GEOGRAPHY、またはTIMEを含む複雑な列は、VARIANTでマスキングすることはできません。 -
MAP鍵はSTRINGにしなければならない。MAP<INT, ...>、MAP<DATE, ...>などの列は変換されません。
機密性の高い列がタグ付けされるまでアクセスを禁止する
一般的なガバナンス パターンは、データが分類されたかどうかに基づいてアクセスを制御することです。 これは、分類の状態に応じて異なるレベルの保護を適用する既定の制限付きタグとポリシーを使用して実装できます。
- カタログレベルまたはスキーマ レベルでタグを適用することで、自動化またはタグ継承を通じて、既定ですべての新しいオブジェクトに
classification : unverifiedのようなタグを適用し、カタログまたはスキーマに追加された新しいテーブルが自動的にタグを継承できるようにします。 -
classification : unverifiedタグ付けされたテーブルへのアクセスをブロックする行フィルター ポリシーを作成します。 -
classification : unverifiedタグが存在しなくなったテーブルで機密性の高い列をマスクする列マスク ポリシーを作成します。 - データ スチュワードが分類を完了すると、タグが更新されます。 ブロック ポリシーが一致しなくなり、マスク ポリシーが有効になります。
-- Block access to unverified tables for all non-admin users
CREATE FUNCTION catalog.schema.block_all() RETURNS BOOLEAN
RETURN FALSE;
CREATE POLICY block_unverified
ON CATALOG my_catalog
ROW FILTER catalog.schema.block_all
TO `account users` EXCEPT `data_admins`
FOR TABLES
WHEN has_tag_value('classification', 'unverified');
機密データを分類した後で保護するには、 classification : unverified タグが存在しなくなったときに有効な列マスク ポリシーを定義します。
CREATE FUNCTION catalog.schema.mask_pii(val STRING)
RETURNS STRING
RETURN '***';
CREATE POLICY mask_reviewed_pii
ON CATALOG my_catalog
COLUMN MASK catalog.schema.mask_pii
TO `account users`
EXCEPT `data_admins`
FOR TABLES
WHEN NOT has_tag_value('classification', 'unverified')
MATCH COLUMNS (has_tag_value('pii', 'name') OR has_tag_value('pii', 'address')) AS m
ON COLUMN m;
正規表現なしの部分的な表示
正規表現の代わりに文字列操作を使用して、機密性の高い値の一部を表示します。 正規表現ベースのマスクは、すべての行の値全体をスキャンします。これは、大きなテキスト フィールドではコストがかかります (大 きなテキスト フィールドでの正規表現マスクの回避を参照)。
CREATE FUNCTION mask_ssn(ssn STRING, show_last INT) RETURNS STRING
DETERMINISTIC
RETURN CONCAT('***-**-', RIGHT(ssn, show_last));
一貫性のあるハッシュ (決定論的仮名化)
整合性ハッシュ (決定論的仮名化とも呼ばれます) は、機密データを複数のテーブルで同じハッシュ値に置き換えます。 関数を DETERMINISTIC としてマークすると、関数は常に同じ入力に対して同じ結果を返すので、クエリを最適化するのに役立ちます。
決定的でエラーセーフな式を使用する を参照してください。
次の関数は、文字列値を一貫してハッシュし、 version パラメーターを使用してキーのローテーションをサポートします。 ポリシーのversion句を使用してUSING COLUMNS番号をインクリメントし、以前のバージョンを使用した履歴データを中断せずに新しいハッシュを生成します。 この関数は、ハッシュする前に元の値とバージョン番号を連結するため、同じバージョンの同じ入力で常に同じハッシュが生成されます。
CREATE FUNCTION pseudonymize(val STRING, version INT) RETURNS STRING
DETERMINISTIC
RETURN SHA2(CONCAT(val, CAST(version AS STRING)), 256);
クエリユーザーの属性に基づいてカラムをマスクする
Important
ABACポリシーの識別属性は ベータ版です。 これらを使用するには、アカウント管理者が以下の条件を満たす必要があります:
- アカウントコンソールのプレビューページからABACポリシープレビューでIdentity Attributesを有効にしてください。 アカウント レベルのプレビューの管理を参照してください。
- アカウントのアイデンティティ属性プロビジョニングを設定してください。 「 アイデンティティ属性」を参照してください。
カラムマスクポリシーは、クエリユーザーのアイデンティティ属性を利用して専用グループを必要とせずに機密データをマスクできます。 例えば、department = HR を持つユーザーにはデータのマスクを解除し、それ以外のすべてのユーザーにはマスクすることができます。
これらのパターンは、アイデンティティプロバイダーからユーザーに割り当てられたアイデンティティ属性を必要とし、関数はタグのみの条件とは異なる挙動をしてポリシーの作成方法に影響を与えます。 使用前に、 Identity属性関数 と Identity属性をよく確認してください。
Important
関数は、ユーザーが属性の値を持たない場合や属性キーが存在しない場合に false に解決します。 この false 結果がアクセスを許可するのではなく制限するように条件を書きます。
NOTでマッチを無効にし、属性が一致しない限りマスクが適用されます。 例えば、 WHEN NOT has_identity_attribute_value('department', 'HR') は部門が HRであるユーザー以外のすべてのカラムをマスクし、欠損した値も falseされるため、部門属性のないユーザーもマスクされます。 逆のことは避けてください。属性が一致する場合にのみマスクする条件では、その属性に値を持たないユーザーがマスクされないままになります。
評価の挙動については、 同一属性条件を参照してください。 制限については、 ポリシー条件におけるアイデンティティ属性を参照してください。
固定値にマッチする
部署に所属していないすべての人のためのマスクssnHR:
CREATE FUNCTION hr_catalog.people.mask_ssn(s STRING) RETURNS STRING RETURN '***-**-****';
CREATE OR REPLACE POLICY mask_ssn_non_hr
ON SCHEMA hr_catalog.people
COLUMN MASK hr_catalog.people.mask_ssn
TO `account users`
FOR TABLES
WHEN NOT has_identity_attribute_value('department', 'HR')
MATCH COLUMNS has_tag_value('pii', 'ssn') AS ssn_col
ON COLUMN ssn_col;
この例では、部署が HR のユーザーが実際の価値を見ています。 他の部署のユーザーも、部門属性のないユーザーもマスクを見ることができます。
ガバナンス対象のタグと一致
前の例ではポリシー内の特定の属性値(HR)が指定されているため、複数の部門をカバーする必要があり、それぞれに別々のポリシーを書く必要があります。 すべての部署を単一のポリシーでカバーするには、各テーブルにそのテーブルを所有する部署でタグ付けし、クエリユーザーの department 属性をそのタグと比較します。 この列は、ユーザーの部署がテーブルの dept_tag 値と一致した場合にのみ公開されます。
CREATE FUNCTION prod.sales.mask_ssn(s STRING) RETURNS STRING RETURN '***-**-****';
CREATE OR REPLACE POLICY mask_unless_dept_matches
ON SCHEMA prod.sales
COLUMN MASK prod.sales.mask_ssn
TO `account users`
FOR TABLES
WHEN NOT has_identity_attribute_tag_match('department', 'dept_tag')
MATCH COLUMNS has_tag_value('pii', 'ssn') AS ssn_col
ON COLUMN ssn_col;
属性キーと値はどちらも大文字を区別し、値は正確に比較されます。 Finance と finance は一致しません。
ユーザーを代表して行動する外部エージェントへのアクセスを制限する
Important
ABACポリシーのコンテキスト属性は ベータ版です。 これらを使用するには、アカウント管理者がアカウントコンソールのプレビューページからUC ABACコンテキスト属性プレビューを有効にする必要があります。 アカウント レベルのプレビューの管理を参照してください。
コンテキスト属性は、OAuthアプリケーション(ユーザー対マシン認証(U2M)認証)を通じてユーザーに代わって行われるリクエストのデータアクセスを制限するために使用できます。 エージェントがOAuthで接続されている場合、この設定はユーザーがワークスペースで直接データを読み取れるにもかかわらず、ユーザーを代表して行動する際にデータへのアクセスを防ぐことができます。
Azure Databricks CLI、SDK、またはSQL文実行APIを通じてOAuth認証されたアクセスは、ユーザーが手動でクエリしてもrequest.is_on_behalf_ofを'true'に設定します。 個人アクセストークン(PAT)で認証されたアクセスはそうではありません。 この仕組みではジーニーへのアクセスは奪われません。
これらのパターンはコンテキスト属性関数を使用します。 利用可能な属性や動作については、 コンテキスト属性関数(ベータ版)を参照してください。
エージェントを Azure Databricks に接続する
コンテキスト属性を使用するには、 カスタムOAuthアプリケーションでエージェントを接続します:
- アカウント管理者はアカウントコンソールから UC ABACコンテキスト属性 プレビューを有効にします。 Manage Azure Databricks プレビューを参照してください。
- アカウント管理者はアカウントコンソールでカスタムOAuthアプリケーションを登録し、そのクライアントIDを記録します。
- そのエージェントをそのOAuthアプリケーションを通じてAzure Databricks管理MCPに接続します。 「 カスタムOAuthクライアントの設定」を参照してください。
内蔵の databricks-cli クライアントを使うエージェントはOAuth上で認証しているので、 request.is_on_behalf_of は 'true'を読み取っています。 しかし、両者とも databricks-cli クライアントIDを共有しているため、CLIの手動使用とリクエストを区別することはできません。 特定のアプリケーションを制御するには、カスタムOAuthアプリケーションを登録し、そこを通じてエージェントを接続します。
Warning
保険がカバーしない経路から代理店がデータにアクセスできないようにしてください:
-
request.is_on_behalf_ofに基づいてアクセスを制限する場合は、エージェントがPATで認証できないようにしてください。 PATはrequest.is_on_behalf_ofを'true'に設定しないため、その属性に条件がかかっても制限されません。 -
request.client_idに基づいてアクセスを制限する場合は、担当者があなたの条件でカバーされていないクライアント(例えば一般的なdatabricks-cliクライアント)を通じて接続できないようにしてください。
On-Behalf-Of (代理) 要求に対して列をマスクする
ユーザーに代わって実行されるリクエスト(登録済みの OAuth アプリケーションを介して動作するエージェントなど)では ssn をマスクし、直接のクエリではマスクしないようにします:
CREATE FUNCTION hr_catalog.people.mask_ssn(s STRING) RETURNS STRING RETURN '***-**-****';
CREATE OR REPLACE POLICY mask_ssn_for_agents
ON SCHEMA hr_catalog.people
COLUMN MASK hr_catalog.people.mask_ssn
TO `account users`
FOR TABLES
WHEN has_context_attribute_value('request.is_on_behalf_of', 'true')
MATCH COLUMNS has_tag_value('pii', 'ssn') AS ssn_col
ON COLUMN ssn_col;
この例では、直接クエリは実数値を返し、代理リクエストはマスクされた値を表示します。 CLIとSQLステートメント実行APIを使うと、request.is_on_behalf_of'true'も読み取れるため、このポリシーはこれらのリクエストに対してもカラムをマスクします。 特定のアプリケーションをターゲットにするには、 request.client_id そのアプリケーションのクライアントIDと照合してください。
承認された申請に限定する欄
承認されたアプリケーションからのリクエスト(OAuthクライアントIDで識別されるものを除く)に対して、すべての外部リクエストに対してマスク ssn してください:
CREATE OR REPLACE POLICY mask_ssn_unapproved_apps
ON SCHEMA hr_catalog.people
COLUMN MASK hr_catalog.people.mask_ssn
TO `account users`
FOR TABLES
WHEN NOT has_context_attribute_value('request.client_id', '<your-app-client-id>')
MATCH COLUMNS has_tag_value('pii', 'ssn') AS ssn_col
ON COLUMN ssn_col;
どのアプリケーションがリクエストを行ったかを確認するには、監査ログのidentity_metadata.acting_resourceフィールドを確認してください。
列専用の述語を用いる行フィルタリング
テーブル列のみを参照する単純なブール型ロジックを使用して行をフィルター処理します。 列のみの述語では述語プッシュダウンが有効になります。これにより、エンジンはスキャン中に無関係なデータをスキップできます ( 保護されたテーブルの述語プッシュダウンについてを参照してください)。
CREATE FUNCTION filter_by_region(region STRING, allowed STRING)
RETURNS BOOLEAN
DETERMINISTIC
RETURN array_contains(split(allowed, ','), lower(region));
許可されているリージョンを定数として渡すポリシーで使用します。
CREATE POLICY regional_access
ON CATALOG analytics
ROW FILTER filter_by_region
TO 'emea_team'
FOR TABLES
MATCH COLUMNS has_tag('region') AS rgn
USING COLUMNS (rgn, 'emea,apac');
複数の関連する列の行フィルター処理
テーブルに関連する属性 ( ship_to_country や bill_to_countryなど) を表す複数の列がある場合は、それらを個別のタグ条件と照合し、両方を 1 つの UDF に渡すことができます。 これにより、列ごとに個別のポリシーが作成されるのを回避できます。 行フィルターポリシーは最大6列の表現を含めることができ、列マスクポリシーは MATCH COLUMNS 節に最大3列の表現を含めることができます( ポリシークォータを参照)。
CREATE FUNCTION filter_by_countries(ship_country STRING, bill_country STRING, allowed STRING)
RETURNS BOOLEAN
DETERMINISTIC
RETURN array_contains(split(allowed, ','), lower(ship_country))
OR array_contains(split(allowed, ','), lower(bill_country));
CREATE POLICY regional_orders
ON SCHEMA prod.orders
ROW FILTER filter_by_countries
TO analysts
FOR TABLES
WHEN has_tag_value('sensitivity', 'high')
MATCH COLUMNS
has_tag('ship_country') AS ship,
has_tag('bill_country') AS bill
USING COLUMNS (ship, bill, 'us,ca,mx');
アナリストは、配送先または請求先の国が許可リストに含まれている注文のみを確認します。
ABAC ポリシー UDF の参照テーブル
アクセス 規則がユーザーごとに異なり、ポリシーの TO/EXCEPT 句だけで表現できない場合は、小さな参照テーブルに対するアクセス権を確認できます。 可能な場合は TO/EXCEPT を使用します。これは、プリンシパルをターゲットにするための推奨されるアプローチです (プリンシパルを ターゲットにするためのアプローチを参照)。 オプティマイザーがサブクエリをブロードキャスト ハッシュ結合に変換するように、ルックアップ テーブルを小さくしておきます ( 参照テーブルを小さくするを参照)。
CREATE TABLE access_rules (
principal VARCHAR(255),
priority VARCHAR(64)
);
INSERT INTO access_rules VALUES
('alice@company.com', '1-URGENT'),
('alice@company.com', '2-HIGH'),
('bob@company.com', '1-URGENT');
CREATE FUNCTION priority_allowed(o_priority STRING) RETURNS BOOLEAN
RETURN EXISTS (
SELECT 1 FROM access_rules
WHERE principal = session_user() AND priority = o_priority
);
CREATE POLICY priority_filter
ON CATALOG operations
ROW FILTER priority_allowed
TO `account users`
FOR TABLES
MATCH COLUMNS has_tag('priority') AS pri
USING COLUMNS (pri);