SQL Server Dynamic Data Masking: The Complete DBA Guide
Dynamic Data Masking (DDM) is a SQL Server security feature that hides sensitive column values from unauthorized users at query time, without changing the data stored in the database. A user without the UNMASK permission sees masked output when they SELECT a masked column. A user with UNMASK permission sees the real value. The data on disk is never altered.
DDM is not encryption. It is not a substitute for proper access control. It is a complementary layer that reduces accidental exposure of sensitive data to users who have SELECT permission on a table but should not see values like phone numbers, email addresses, credit card digits, or date of birth. Understanding exactly what DDM does and does not protect is as important as knowing how to configure it.
Contents
How DDM Works
BeginnerMasking rules are defined at the column level in a table. When a user without UNMASK permission executes a SELECT that includes a masked column, SQL Server applies the masking function to the column values in the result set before returning the data to the client. The underlying stored data is never modified. No new tables are created. No triggers fire. The masking occurs purely at the query output layer.
This means:
- Applications that write to masked columns work normally — INSERT and UPDATE operations are not affected by DDM.
- Masked data cannot be used in WHERE clauses to filter results based on the real value, but the column is still filterable using the masked representation.
- Users with CONTROL or db_owner permissions on the database always see unmasked data regardless of whether UNMASK has been explicitly granted.
- DDM applies to SELECT results. It does not affect BACKUP, index definitions, or encryption operations.
The Five Mask Functions
BeginnerSQL Server provides five masking functions. Each applies a different transformation to the column value in the query result. The function is chosen at the time the mask is defined on a column and can be changed later without dropping and recreating the column.
| Function | Applies To | What the Non-Privileged User Sees | Version |
|---|---|---|---|
default() |
All data types | Strings: XXXX. Numeric types: 0. Date/time types: 1900-01-01 00:00:00.0000000. Binary types: single byte ASCII value 0. |
SQL Server 2016+ |
email() |
String columns containing email addresses | First character of the email address followed by XXX@XXXX.com — always ends in .com regardless of the real domain. |
SQL Server 2016+ |
random(start, end) |
Numeric columns only | A random numeric value within the specified range. The value changes with each query execution. | SQL Server 2016+ |
partial(prefix, padding, suffix) |
String columns | Exposes a specified number of characters at the start (prefix) and end (suffix) with a custom padding string in the middle. Example: partial(1,"XXXXXXX",0) turns 555.123.1234 into 5XXXXXXX. |
SQL Server 2016+ |
datetime("unit") |
datetime, datetime2, date, time, datetimeoffset, smalldatetime columns | Masks a specific portion of the date/time value. Unit values: Y (year), M (month), D (day), h (hour), m (minute), s (seconds). Only one portion is masked per function call. |
SQL Server 2022+ |
Permissions Model
Intermediate| Permission | What It Allows |
|---|---|
SELECT on the table | Required to query the table. Users with SELECT but without UNMASK see masked values. |
UNMASK | Grants the ability to see unmasked data from masked columns. Can be granted at database, schema, table, or column level from SQL Server 2022. |
ALTER ANY MASK | Required to add, change, or remove masks on columns. Appropriate for security officers managing the masking policy. |
ALTER on the table | Required in addition to ALTER ANY MASK to modify masks on a specific table. |
CONTROL on the database | Includes both ALTER ANY MASK and UNMASK. sysadmin and db_owner have this by default and always see unmasked data. |
Creating a table with masked columns does not require any special permission beyond the standard CREATE TABLE and ALTER on schema permissions. The mask definition is part of the column definition syntax.
Applying Masks with T-SQL
IntermediateDefine a mask at table creation time
-- Create a table with masks defined inline on columns
CREATE TABLE dbo.CustomerContacts
(
CustomerID INT PRIMARY KEY,
FullName NVARCHAR(100),
Email NVARCHAR(255) MASKED WITH (FUNCTION = 'email()'),
Phone VARCHAR(20) MASKED WITH (FUNCTION = 'partial(2,"XXX-XXX-",4)'),
CreditScore INT MASKED WITH (FUNCTION = 'random(300, 850)'),
DateOfBirth DATE MASKED WITH (FUNCTION = 'datetime("Y")'), -- SQL Server 2022+
SSN CHAR(11) MASKED WITH (FUNCTION = 'default()')
);
GO
Add a mask to an existing column
-- Add a mask to a column in an existing table
ALTER TABLE dbo.CustomerContacts
ALTER COLUMN Email
ADD MASKED WITH (FUNCTION = 'email()');
GO
-- Add a partial mask to a phone number column
-- Shows first 3 digits and last 4 digits, masks the middle
ALTER TABLE dbo.CustomerContacts
ALTER COLUMN Phone
ADD MASKED WITH (FUNCTION = 'partial(3,"XXX",4)');
GO
-- Add a default mask to a credit card number column
ALTER TABLE dbo.PaymentData
ALTER COLUMN CardNumber
ADD MASKED WITH (FUNCTION = 'default()');
GO
Test masking behavior by impersonating a non-privileged user
-- Create a test user without UNMASK permission
CREATE USER TestViewer WITHOUT LOGIN;
GRANT SELECT ON dbo.CustomerContacts TO TestViewer;
GO
-- Insert test data
INSERT INTO dbo.CustomerContacts
(CustomerID, FullName, Email, Phone, CreditScore, DateOfBirth, SSN)
VALUES
(1, 'Alice Johnson', 'alice.johnson@example.com', '555-867-5309', 720, '1985-03-15', '123-45-6789');
GO
-- View as privileged user (sees real data)
SELECT CustomerID, FullName, Email, Phone, CreditScore, DateOfBirth, SSN
FROM dbo.CustomerContacts;
GO
-- View as non-privileged user (sees masked data)
EXECUTE AS USER = 'TestViewer';
SELECT CustomerID, FullName, Email, Phone, CreditScore, DateOfBirth, SSN
FROM dbo.CustomerContacts;
REVERT;
GO
-- Expected masked output for TestViewer:
-- Email: aXXX@XXXX.com
-- Phone: 555XXXXX5309 (depending on partial parameters)
-- CreditScore: random number between 300 and 850
-- DateOfBirth: date with year masked per datetime("Y") function
-- SSN: XXXX (default mask for char type)
-- FullName: Alice Johnson (no mask defined)
Granular UNMASK Permissions (SQL Server 2022)
IntermediatePrior to SQL Server 2022, UNMASK was an all-or-nothing database-level permission. Granting UNMASK allowed a user to see unmasked data in every masked column across the entire database. From SQL Server 2022, UNMASK can be granted at four levels of granularity.
| Scope | T-SQL Syntax | Effect |
|---|---|---|
| Database level | GRANT UNMASK ON DATABASE::DatabaseName TO UserName | User sees unmasked data in all masked columns across all tables in the database |
| Schema level | GRANT UNMASK ON SCHEMA::SchemaName TO UserName | User sees unmasked data in all masked columns within the specified schema only |
| Table level | GRANT UNMASK ON OBJECT::SchemaName.TableName TO UserName | User sees unmasked data in all masked columns within the specified table only |
| Column level | GRANT UNMASK ON OBJECT::SchemaName.TableName(ColumnName) TO UserName | User sees unmasked data in that specific column only; all other masked columns remain masked |
-- Grant UNMASK at column level only (SQL Server 2022+)
-- The support analyst can see the real email but not SSN or credit score
GRANT UNMASK ON OBJECT::dbo.CustomerContacts(Email) TO SupportAnalyst;
GO
-- Grant UNMASK at table level
-- The fraud team sees all unmasked columns in the PaymentData table
GRANT UNMASK ON OBJECT::dbo.PaymentData TO FraudTeamRole;
GO
-- Grant UNMASK at schema level
-- Security officers can see unmasked data across the entire Security schema
GRANT UNMASK ON SCHEMA::Security TO SecurityOfficerRole;
GO
-- Revoke granular UNMASK
REVOKE UNMASK ON OBJECT::dbo.CustomerContacts(Email) FROM SupportAnalyst;
GO
Querying Masked Columns with sys.masked_columns
IntermediateThe sys.masked_columns catalog view shows all columns in the database that have a masking function applied. It extends sys.columns with two additional columns: is_masked and masking_function.
-- List all masked columns in the current database
SELECT
SCHEMA_NAME(t.schema_id) AS schema_name,
t.name AS table_name,
c.name AS column_name,
c.masking_function,
tp.name AS data_type,
c.max_length,
c.is_nullable
FROM sys.masked_columns c
JOIN sys.tables t ON c.object_id = t.object_id
JOIN sys.types tp ON c.user_type_id = tp.user_type_id
WHERE c.is_masked = 1
ORDER BY schema_name, table_name, column_name;
GO
-- Audit: users with UNMASK permission and the scope of their permission
SELECT
dp.name AS principal_name,
dp.type_desc AS principal_type,
perm.permission_name,
perm.state_desc,
perm.class_desc,
OBJECT_NAME(perm.major_id) AS object_name
FROM sys.database_permissions perm
JOIN sys.database_principals dp ON perm.grantee_principal_id = dp.principal_id
WHERE perm.permission_name = 'UNMASK'
ORDER BY dp.name;
GO
Modifying and Removing Masks
Beginner-- Change the mask function on an existing masked column
ALTER TABLE dbo.CustomerContacts
ALTER COLUMN Email
ADD MASKED WITH (FUNCTION = 'partial(1,"XXX@XXX",4)');
GO
-- Note: ADD MASKED replaces an existing mask on the column
-- Remove a mask from a column
ALTER TABLE dbo.CustomerContacts
ALTER COLUMN SSN
DROP MASKED;
GO
What Happens When Masked Data Is Copied
IntermediateThis is one of the most important behaviors to understand in production environments. When a user without UNMASK permission copies masked data to another table or exports it, the destination receives the masked values — not the real values.
-- A user without UNMASK running SELECT INTO
-- copies MASKED values into the new table, not real values
SELECT * INTO dbo.CustomerContacts_Backup
FROM dbo.CustomerContacts;
-- If run by a non-privileged user: all masked columns in the backup
-- contain the masked representation, not the original data.
-- The mask definition is NOT carried over to the new table.
The same behavior applies to:
- SQL Server Import and Export operations run by a non-privileged user: the exported file contains masked values
INSERT INTO ... SELECTfrom a masked table: destination rows contain masked values- Linked server queries to a masked table from a remote server: the remote server receives already-masked values and cannot unmask them
Limitations: What DDM Cannot Mask
IntermediateThe following column types and configurations cannot have a DDM mask applied. Attempting to add a mask to these columns returns an error.
| Not Supported | Reason / Notes |
|---|---|
| Always Encrypted columns | DDM and Always Encrypted cannot be combined on the same column. Always Encrypted encrypts at rest and in transit; DDM operates at query output layer only. |
| FILESTREAM columns | Not supported. |
| COLUMN_SET or sparse columns that are part of a column set | Not supported. |
| Computed columns | A mask cannot be defined on a computed column. However, if a computed column references a masked column, the computed column returns masked data automatically. |
| Full-text index key columns | A masked column cannot be used as the key column for a full-text index. |
| PolyBase external table columns | DDM does not apply to columns in external tables. |
| Indexed views referencing the base table | DDM is not supported when the underlying base table is referenced in an indexed view. |
Additionally, the deprecated T-SQL statements READTEXT, UPDATETEXT, and WRITETEXT do not function correctly on columns configured with DDM for users without UNMASK permission.
DDM vs Always Encrypted vs Row-Level Security
AdvancedThese three features are complementary, not alternatives. A well-designed security architecture uses all three together for different purposes.
| Feature | What It Protects | Data Changed on Disk | DBA Can Bypass | Application Changes Required |
|---|---|---|---|---|
| Dynamic Data Masking | Hides column values in query results from non-privileged users. Protects against accidental exposure. | No — data unchanged on disk | Yes — db_owner and sysadmin always see real data | No — masking rules applied transparently |
| Always Encrypted | Encrypts column values on disk and in transit. The database engine never sees plaintext — decryption happens at the client application. | Yes — data stored encrypted | No — even DBAs cannot read plaintext without the client encryption key | Yes — application must handle encryption/decryption using Always Encrypted client drivers |
| Row-Level Security | Restricts which rows a user can see based on a security predicate function. Users querying a table automatically receive only rows they are authorized to see. | No — data unchanged on disk | No (by default) — but RLS can be bypassed by db_owner unless explicitly blocked | No — filtering applied transparently by the database engine |
The technical information in this article was verified against Microsoft documentation at the time of publication. SQL Server features, cloud service capabilities, licensing terms, and configuration requirements can change between versions and cumulative updates. Always validate implementation details against current Microsoft Learn documentation before deploying to production. References in this article link directly to the authoritative Microsoft sources.
References
- Microsoft Docs: Dynamic Data Masking (SQL Server)
- Microsoft Docs: Dynamic Data Masking (Azure SQL Database)
- Microsoft Docs: sys.masked_columns (Transact-SQL)
- Microsoft Docs: Always Encrypted (Database Engine)
- Microsoft Docs: Row-Level Security
- SQLYARD: SQL Server Data Classification: Discovery, Labeling, and Audit Integration
- SQLYARD: SQL Server Service Account Permissions
- SQLYARD: SQL Server 2022: Azure Integration, Query Intelligence, Security, and Data Virtualization
Discover more from SQLYARD
Subscribe to get the latest posts sent to your email.


