I am utilizing partition schemes in order to logically isolate data based on a TenantId
column. TenantId
is the partition column and clustering index across all tables that require tenant isolation. I would like to create unique, non-clustered indexes on the [Tenant]
table that also holds TenantAlias
and TenantName
. I cannot create the unique indexes on the partition scheme unless they are a part of the partitioning column as a composite primary key. This fact leads me to worry about performance implications (especially in larger contexts), but this may be a misguided thought.
Even with the potential performance implications, given that the combination of TenantId
, TenantAlias
, and TenantName
will always be a unique composition, I'm falling towards including the non-partitioning columns onto the clustered index. However, I would like to know if it's possible otherwise, if there are detriments to not including it in the composite key, or if it's better to avoid placing them on the partition scheme at all.
For optimal context, let's set up the security schema, file group, partition function, and partition scheme to a database called [IsolationExample]
.
IF (SCHEMA_ID('Auth') IS NULL)
BEGIN
EXEC ('CREATE SCHEMA [Auth] AUTHORIZATION [dbo]')
END;
GO
ALTER DATABASE [IsolationExample]
ADD FILEGROUP [Auth]
GO
ALTER DATABASE [IsolationExample]
ADD FILE (
NAME = N'IsolationExample_Auth', FILENAME = N'E:\MSSQL\Data\IsolationExample_Auth.ndf', SIZE = 512MB , MAXSIZE = UNLIMITED, FILEGROWTH = 512MB
)
TO FILEGROUP [Auth]
GO
IF EXISTS (SELECT 1 FROM sys.partition_schemes WHERE [name] = 'PS_Tenant_Isolation') DROP PARTITION SCHEME [PS_Tenant_Isolation];
IF EXISTS (SELECT 1 FROM sys.partition_functions WHERE [name] = 'PF_Tenant_Isolation') DROP PARTITION FUNCTION [PF_Tenant_Isolation];
IF NOT EXISTS (SELECT 1 FROM sys.partition_functions WHERE [name] = 'PF_Tenant_Isolation')
BEGIN
CREATE PARTITION FUNCTION [PF_Tenant_Isolation] ([int])
-- RANGE RIGHT values will be SPLIT based on external logic to add the correct values (1,2,3 ...) that corresponds with [Tenant].[TenantId].
AS RANGE RIGHT FOR VALUES (0);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.partition_schemes WHERE [name] = 'PS_Tenant_Isolation')
BEGIN
CREATE PARTITION SCHEME [PS_Tenant_Isolation]
AS PARTITION [PF_Tenant_Isolation]
ALL TO ([Auth]);
END;
GO
Option 1
The first attempted DDL operation does not work:
DROP TABLE IF EXISTS [Auth].[Tenant];
IF OBJECT_ID('[Auth].[Tenant]', 'U') IS NULL
BEGIN
CREATE TABLE [Auth].[Tenant] (
[TenantId] [int] IDENTITY(1,1) NOT NULL
,[TenantAlias] [varchar](3) NOT NULL
,[TenantName] [varchar](256) NOT NULL
,CONSTRAINT [PK_Tenant_TenantId] PRIMARY KEY CLUSTERED ([TenantId] ASC)
,CONSTRAINT [UQ_Tenant_TenantAlias] UNIQUE NONCLUSTERED ([TenantAlias] ASC)
,CONSTRAINT [UQ_Tenant_TenantName] UNIQUE NONCLUSTERED ([TenantName] ASC)
) ON [PS_Tenant_Isolation]([TenantId]);
END;
GO
Column 'TenantId' is partitioning column of the index 'UQ_Tenant_TenantName'. Partition columns for a unique index must be a subset of the index key.
Option 2
If I place the DDL operations outside of the scope of the CREATE TABLE
operation, it also tosses the same error.
DROP TABLE IF EXISTS [Auth].[Tenant];
IF OBJECT_ID('[Auth].[Tenant]', 'U') IS NULL
BEGIN
CREATE TABLE [Auth].[Tenant] (
[TenantId] [int] IDENTITY(1,1) NOT NULL
,[TenantAlias] [varchar](3) NOT NULL
,[TenantName] [varchar](256) NOT NULL
,CONSTRAINT [PK_Tenant_TenantId] PRIMARY KEY CLUSTERED ([TenantId] ASC)
) ON [PS_Tenant_Isolation]([TenantId]);
CREATE UNIQUE NONCLUSTERED INDEX [UQ_Tenant_TenantAlias] ON [Auth].[Tenant]([TenantAlias] ASC) ON [PS_Tenant_Isolation]([TenantId]);
CREATE UNIQUE NONCLUSTERED INDEX [UQ_Tenant_TenantName] ON [Auth].[Tenant]([TenantName] ASC) ON [PS_Tenant_Isolation]([TenantId]);
END;
GO
Column 'TenantId' is partitioning column of the index 'UQ_Tenant_TenantName'. Partition columns for a unique index must be a subset of the index key.
*Option 3
If I do not place the unique indexes on the partition scheme, the DDL operation succeeds, but as one would expect it places the indexes on the specified file group directly. In my mind, this defeats the entire purpose of the partition scheme as we cannot leverage the performance benefits of the partitioning column. But I may be wrong on this and would like to be corrected if so.
DROP TABLE IF EXISTS [Auth].[Tenant];
IF OBJECT_ID('[Auth].[Tenant]', 'U') IS NULL
BEGIN
CREATE TABLE [Auth].[Tenant] (
[TenantId] [int] IDENTITY(1,1) NOT NULL
,[TenantAlias] [varchar](3) NOT NULL
,[TenantName] [varchar](256) NOT NULL
,CONSTRAINT [PK_Tenant_TenantId] PRIMARY KEY CLUSTERED ([TenantId] ASC)
) ON [PS_Tenant_Isolation]([TenantId]);
CREATE UNIQUE NONCLUSTERED INDEX [UQ_Tenant_TenantAlias] ON [Auth].[Tenant]([TenantAlias] ASC) ON [Auth];
CREATE UNIQUE NONCLUSTERED INDEX [UQ_Tenant_TenantName] ON [Auth].[Tenant]([TenantName] ASC) ON [Auth];
END;
GO
Option 4 - Composite Key
Lastly, if I include them as a composite key, the DDL operation works and places them on the partition scheme. This is the "desired" end behavior where the indexes are on the partition scheme, but I want to know if it is appropriate to include them as a composite key on the clustered index at all.
DROP TABLE IF EXISTS [Auth].[Tenant];
IF OBJECT_ID('[Auth].[Tenant]', 'U') IS NULL
BEGIN
CREATE TABLE [Auth].[Tenant] (
[TenantId] [int] IDENTITY(1,1) NOT NULL
,[TenantAlias] [varchar](3) NOT NULL
,[TenantName] [varchar](256) NOT NULL
,CONSTRAINT [PK_Tenant_TenantId] PRIMARY KEY CLUSTERED ([TenantId] ASC, [TenantAlias] ASC, [TenantName] ASC)
) ON [PS_Tenant_Isolation]([TenantId]);
END;
GO
Am I better served with option 3 or option 4, or perhaps an option 5 that I am unaware of?
I would also like to think outside of the context of purely this [Tenant]
table and carry the same general understanding onto other tables that require additional unique keys as well. For example, a [Document]
table with potentially billions of rows, where in addition to TenantId
there is a bigint identity column of DocumentId
as well as a unique DocumentBatchName
.