SQL Server Dynamic Data Masking: The Complete DBA Guide

SQL Server Dynamic Data Masking: The Complete DBA Guide | SQLYARD

SQL Server Dynamic Data Masking: The Complete DBA Guide


SQL Server 2016+
SQL Server 2022
Azure SQL

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.

DDM is not a security boundary against determined attacks. A user with ad-hoc query permissions and sufficient privileges can construct queries that infer masked values through brute-force or inference techniques. DDM protects against accidental exposure, not against a motivated adversary with direct database access. For truly sensitive data such as payment card numbers or health records, Always Encrypted provides a stronger control. DDM and Always Encrypted cannot be applied to the same column simultaneously.
1

How DDM Works

Beginner

Masking 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.
DDM was introduced in SQL Server 2016. DDM is available in SQL Server 2016 (13.x) and later, Azure SQL Database, Azure SQL Managed Instance, Azure Synapse Analytics, and SQL database in Microsoft Fabric. The datetime mask function was added in SQL Server 2022. Granular column-level UNMASK permission was also added in SQL Server 2022.
2

The Five Mask Functions

Beginner

SQL 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.

FunctionApplies ToWhat the Non-Privileged User SeesVersion
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+
SHUFFLE and REVERSE are not SQL Server masking functions. These functions do not exist in SQL Server Dynamic Data Masking. The five supported functions are default, email, random, partial, and datetime. Any documentation or article claiming otherwise is inaccurate.
3

Permissions Model

Intermediate
PermissionWhat It Allows
SELECT on the tableRequired to query the table. Users with SELECT but without UNMASK see masked values.
UNMASKGrants 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 MASKRequired to add, change, or remove masks on columns. Appropriate for security officers managing the masking policy.
ALTER on the tableRequired in addition to ALTER ANY MASK to modify masks on a specific table.
CONTROL on the databaseIncludes both ALTER ANY MASK and UNMASK. sysadmin and db_owner have this by default and always see unmasked data.
sysadmin and db_owner always see unmasked data. Members of the sysadmin fixed server role and the db_owner database role have CONTROL permission and always see real column values regardless of mask definitions. DDM cannot hide data from database administrators. This is by design and cannot be overridden.

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.

4

Applying Masks with T-SQL

Intermediate

Define 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)
5

Granular UNMASK Permissions (SQL Server 2022)

Intermediate

Prior 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.

ScopeT-SQL SyntaxEffect
Database levelGRANT UNMASK ON DATABASE::DatabaseName TO UserNameUser sees unmasked data in all masked columns across all tables in the database
Schema levelGRANT UNMASK ON SCHEMA::SchemaName TO UserNameUser sees unmasked data in all masked columns within the specified schema only
Table levelGRANT UNMASK ON OBJECT::SchemaName.TableName TO UserNameUser sees unmasked data in all masked columns within the specified table only
Column levelGRANT UNMASK ON OBJECT::SchemaName.TableName(ColumnName) TO UserNameUser 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
Column-level UNMASK is the correct production pattern. In most real environments, different roles need access to different sensitive columns. A support team might need to see email addresses to verify identity but should never see SSNs or credit card numbers. Column-level UNMASK (SQL Server 2022) makes this precise control possible without oversharing.
6

Querying Masked Columns with sys.masked_columns

Intermediate

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

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
Adding or changing a mask is a schema change. Because DDM mask definition is stored as column metadata, adding or changing a mask requires ALTER permission on the table and is treated as a schema change. This means it cannot be performed on a column that has a dependency (such as an index) without first dropping the dependency, making the change, and recreating the dependency.
8

What Happens When Masked Data Is Copied

Intermediate

This 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 ... SELECT from 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
Cross-database queries involving masked columns do not produce correct results for comparisons or joins. When a masked column is involved in a join or comparison operation across two databases on different SQL Server instances, the remote values are already in masked form. Joining or comparing masked values to real values produces incorrect results. This is a confirmed limitation documented by Microsoft.
9

Limitations: What DDM Cannot Mask

Intermediate

The following column types and configurations cannot have a DDM mask applied. Attempting to add a mask to these columns returns an error.

Not SupportedReason / Notes
Always Encrypted columnsDDM 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 columnsNot supported.
COLUMN_SET or sparse columns that are part of a column setNot supported.
Computed columnsA 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 columnsA masked column cannot be used as the key column for a full-text index.
PolyBase external table columnsDDM does not apply to columns in external tables.
Indexed views referencing the base tableDDM 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.

10

DDM vs Always Encrypted vs Row-Level Security

Advanced

These three features are complementary, not alternatives. A well-designed security architecture uses all three together for different purposes.

FeatureWhat It ProtectsData Changed on DiskDBA Can BypassApplication 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
Use all three together for defense in depth. Row-Level Security controls which rows a user can see. DDM controls which column values a user can read clearly within those rows. Always Encrypted protects the most sensitive columns (SSN, payment card data) even from database administrators. These three features stack — applying them together provides layered protection at the row level, column visibility level, and encryption level simultaneously.

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.


Discover more from SQLYARD

Subscribe to get the latest posts sent to your email.

Leave a Reply

Discover more from SQLYARD

Subscribe now to keep reading and get access to the full archive.

Continue reading