View a markdown version of this page

Comparison controls - AWS Clean Rooms

Comparison controls

In SQL, literal comparison matches a column from your dataset against a literal value typed directly into the query. Column comparison matches values from two different columns against each other, either within the same table or across joined tables.

Default behavior

Comparison controls are an allowlist. Once you set allowedLiteralComparisonColumns, only the columns you list can be compared to a literal value, and every column you do not list is blocked. The same applies to allowedColumnComparisonColumns for column-to-column comparisons. Adding a column to one allowlist does not add it to the other.

If you do not configure comparisonControls, AWS Clean Rooms applies no comparison restrictions — a query can compare any column to a literal value or to another column.

If you do not configure comparisonControls but you do set a minimum aggregation threshold, comparisons remain otherwise unrestricted. However, AWS Clean Rooms never allows a literal comparison on a column listed in identityColumns. That restriction comes from the threshold itself, so it applies whether or not you configure comparison controls. For more information, see Minimum aggregation thresholds.

If you configure comparisonControls together with disallowed output columns, the two controls are independent and both apply. Comparison controls govern which columns a query can compare; disallowed output columns govern which columns can appear in the query result. A column can be allowed in a comparison and still be barred from the result. For more information, see Disallowed output columns.

Literal comparison

A literal comparison evaluates a column against a single hardcoded constant (a string, number, or date). The right side of the operator never changes during the query execution. Syntax examples include:

  • WHERE status = 'Active'

  • WHERE price > 49.99

When you use Minimum aggregation thresholds, AWS Clean Rooms does not allow literal comparison on the identityColumns value. This prevents the query runner from submitting a query filtered down to an individual or specific set of users. Avoid allowing literal comparison on low cardinality columns that can single out small groups or individual data subjects.

{ "comparisonControls": { "allowedLiteralComparisonColumns": [ "status", "price" ] } }
Choosing columns for literal comparison

Allow literal comparison only on columns that don't identify individuals or small groups. Avoid allowing it on low-cardinality columns (for example, age_band, coarse region code). Even though these columns are not the configured identityColumns value, comparing them to literals can narrow results to a small, identifiable population. High-cardinality, non-identifying dimensions such as campaign_id or product_sku are safer choices.

Example: Allowing literal comparison on a campaign column

A publisher configures a custom analysis rule with a minimum aggregation threshold so that every output row represents at least 100 distinct users (user_id). An advertiser runs queries against this table but needs to scope their analysis to a specific advertising campaign — for example, to measure reach for one campaign at a time.

Because user_id is the identity column, it can't be compared to a literal, preventing the advertiser from filtering results down to a single user. But campaign_id and event_date are high-cardinality, non-identifying dimensions, so the publisher adds them to allowedLiteralComparisonColumns, which lets the advertiser filter by campaign and scope the analysis to a date range:

{ "aggregationThresholds": [ { "identityColumns": ["user_id"], "minimumIdentityCount": 100 } ], "comparisonControls": { "allowedLiteralComparisonColumns": ["campaign_id", "event_date"] } }

Given the preceding configuration, this query is allowed:

-- Allowed: campaign_id and event_date are both in allowedLiteralComparisonColumns SELECT campaign_id, COUNT(DISTINCT user_id) AS reach FROM impressions WHERE campaign_id = 'CMP-1024' AND event_date >= '2026-01-01' GROUP BY campaign_id;

Given the same configuration, this query is blocked:

-- Blocked: user_id is the identity column and can never be compared to a literal SELECT campaign_id, COUNT(DISTINCT user_id) AS reach FROM impressions WHERE user_id = 'U-88231' GROUP BY campaign_id;

The first query still returns only rows backed by at least 100 distinct users, while the literal comparisons on campaign_id and event_date filter which rows are considered. The second query is rejected because it attempts to single out an individual data subject.

Column comparison

A column comparison evaluates the value of one column against the value of another column dynamically for every single row. Syntax examples include:

  • WHERE retail_price < wholesale_price

  • WHERE users.id = orders.user_id

When using Minimum aggregation thresholds, a data provider can allow column comparison on the identityColumns value for use cases that require joining across tables, such as an audience overlap report.

{ "comparisonControls": { "allowedColumnComparisonColumns": [ "user_id" ] } }

Example: Allowing column comparison on the identity column for an overlap report

A publisher and an advertiser want to measure their audience overlap — how many users appear in both of their datasets — without either party learning who any individual user is. This requires joining the two tables on user_id, which is a column-to-column comparison. The publisher allows the advertiser to run an audience overlap analysis scoped to specific campaign IDs.

Because user_id is the identity column, the publisher has already blocked literal comparisons on it (so no one can filter to a specific person). To enable the join across tables, the publisher adds user_id to allowedColumnComparisonColumns. To enable campaign filtering, the publisher adds campaign_id to allowedLiteralComparisonColumns.

{ "aggregationThresholds": [ { "identityColumns": ["user_id"], "minimumIdentityCount": 100 } ], "comparisonControls": { "allowedLiteralComparisonColumns": ["campaign_id"], "allowedColumnComparisonColumns": ["user_id"] } }

Given the preceding configuration, this query is allowed:

-- Allowed: user_id is compared against another column (column-to-column join) SELECT COUNT(DISTINCT p.user_id) AS overlapping_users FROM publisher_audience p JOIN advertiser_audience a ON p.user_id = a.user_id;

Given the same configuration, this query is blocked:

-- Blocked: email is not in allowedColumnComparisonColumns SELECT COUNT(DISTINCT p.user_id) AS overlapping_users FROM publisher_audience p JOIN advertiser_audience a ON p.email = a.email;

The join succeeds because a column-to-column comparison evaluates dynamically per row and doesn't let the query runner target a known value. The result enforces the minimum aggregation threshold, ensuring the overlap count is only returned if it represents at least 100 distinct users. The second query is blocked because email is not in the allowedColumnComparisonColumns allowlist and cannot be used in a comparison — only user_id can.

Comparison controls and expressions

Comparison controls follow indirect literal comparisons, not only a direct WHERE column = 'literal' predicate. If you allow ANY_EXPRESSION inside aggregate functions through allowedAggregateExpressionType, comparison controls still block a literal comparison on a column that is not in allowedLiteralComparisonColumns. This applies even when the literal is nested inside an expression.

The following example shows a query that remains blocked because zip_code is not in the allowlist, even though the literal is inside a CASE expression rather than written as a direct predicate:

-- Blocked: zip_code is not in allowedLiteralComparisonColumns, -- even though the literal comparison is nested inside a CASE expression SELECT SUM(CASE WHEN zip_code = '00001' THEN salary ELSE 0 END) AS total FROM employees;

This is why the two controls are complementary: allowing expressions inside aggregates widens what a query can compute, while comparison controls still constrain which columns a query can single out by value. For more information, see Allowing nested expressions in aggregate functions.