I am looking to understand a typical approach to indexes in a database parent-child design. This is a basic financial calculator for calculating complex commissions used monthly and driven by stored procedures.
Currently, the base number of transactions driving this is about 400,000 rows. Every month this increases approximately 3-5%. Each transaction may have 1-5 (average of 3) commission records stored, meaning that each month a million records or more are inserted into the below tables.
Often, commission is run and deleted multiple times depending on analysts adjustments.
Environment: SQL Server 2016 standard, 32Gb memory, 8 cpu cores
Design:
transaction_mstr - Table that stores each commission run date, period, and user details (who ran it, what date and time, etc.)
transaction_map - Table that stores every account being commission in the period
transaction_hdr - Table that stores every transaction associated with the account
transaction_dtl - Table that stores commission details per transaction
Keys
transaction_mstr: id
transaction_map: id, mstr_id
transaction_hdr: id, mstr_id, map_id
transaction_dtl: id, mstr_id, map_id, hdr_id
Simple Query to illustrate the relationships:
SELECT *
FROM transaction_mstr a
JOIN transaction_map b ON a.id = b.mstr_id
JOIN transaction_hdr c ON b.id = c.map_id
JOIN transaction_dtl d ON c.id = d.hdr_id;
Current keys
The ID column in each table is a primary key (unique auto increment value).
A single foreign key is created on each table relating referencing the parent primary key.
The Ask In it's current state, inserting and deleting commission from the hdr and dtl tables is very slow. I have asked the infrastructure team to ensure the environment is resourced appropriately, but I'd like to understand if a better key design would improve performance.
In addition, I have narrowed down the wait time to the actual insert and delete. The calculations run rather quickly (3-5 mins), but the inserts into the various tables take upwards of 30 minutes. The deletes take approximately 20 mins per commission period.
Create table / index scripts
CREATE TABLE [dbo].[transaction_dtl](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[mstr_id] [smallint] NULL,
[map_id] [bigint] NULL,
[hdr_id] [bigint] NULL,
[trx_id] [bigint] NULL,
[trx_parent_id] [int] NULL,
[agent_id] [smallint] NULL,
[rate_id] [smallint] NULL,
[frequency_id] [tinyint] NULL,
[period_days] [tinyint] NULL,
[commission] [numeric](38, 6) NULL,
[row_create_ts] [datetime] NULL,
CONSTRAINT [PK_transaction_dtl] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[transaction_hdr](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[mstr_id] [smallint] NULL,
[map_id] [bigint] NULL,
[trx_id] [bigint] NULL,
[trx_parent_id] [int] NULL,
[trx_other_id] [int] NULL,
[level] [tinyint] NULL,
[days_held] [smallint] NULL,
[units_bal] [numeric](38, 6) NULL,
[reversal] [int] NULL,
[sortkey] [varchar](300) NULL,
[row_create_ts] [datetime] NULL,
CONSTRAINT [PK_transaction_hdr] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[transaction_map](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[mstr_id] [smallint] NULL,
[fund_id] [tinyint] NULL,
[account_id] [int] NULL,
[wholesaler_id] [int] NULL,
[unbalanced] [bit] NULL,
[nobalance] [bit] NULL,
[row_create_ts] [datetime] NULL,
CONSTRAINT [PK_transaction_map] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[transaction_mstr](
[id] [smallint] IDENTITY(1,1) NOT NULL,
[caller_sid] [varbinary](85) NOT NULL,
[caller_name] [sysname] NOT NULL,
[status] [tinyint] NOT NULL,
[start_time] [datetimeoffset](7) NULL,
[end_time] [datetimeoffset](7) NULL,
[commission_type] [tinyint] NULL,
[commission_date] [date] NULL,
[run_date] [date] NULL,
[frequency_id] [tinyint] NULL,
[closed] [tinyint] NOT NULL,
[row_create_ts] [datetime] NOT NULL,
[row_modified_ts] [datetime] NULL,
CONSTRAINT [PK_transaction_mstr] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[transaction_dtl] ADD CONSTRAINT [DF_commission_dtl_row_create_ts] DEFAULT (getdate()) FOR [row_create_ts]
GO
ALTER TABLE [dbo].[transaction_hdr] ADD CONSTRAINT [DF_transaction_hdr_reversal] DEFAULT ((0)) FOR [reversal]
GO
ALTER TABLE [dbo].[transaction_hdr] ADD CONSTRAINT [DF_commission_hdr_row_create_ts] DEFAULT (getdate()) FOR [row_create_ts]
GO
ALTER TABLE [dbo].[transaction_map] ADD CONSTRAINT [DF_transaction_map_balanced] DEFAULT ((0)) FOR [unbalanced]
GO
ALTER TABLE [dbo].[transaction_map] ADD CONSTRAINT [DF_transaction_map_nobalance] DEFAULT ((0)) FOR [nobalance]
GO
ALTER TABLE [dbo].[transaction_map] ADD CONSTRAINT [DF_commission_map_row_create_ts] DEFAULT (getdate()) FOR [row_create_ts]
GO
ALTER TABLE [dbo].[transaction_mstr] ADD CONSTRAINT [DF_transaction_master_closed] DEFAULT ((0)) FOR [closed]
GO
ALTER TABLE [dbo].[transaction_mstr] ADD CONSTRAINT [DF_transaction_master_create_row_ts] DEFAULT (getdate()) FOR [row_create_ts]
GO
ALTER TABLE [dbo].[transaction_dtl] WITH CHECK ADD CONSTRAINT [FK_transaction_dtl_hdr] FOREIGN KEY([hdr_id])
REFERENCES [dbo].[transaction_hdr] ([id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[transaction_dtl] CHECK CONSTRAINT [FK_transaction_dtl_hdr]
GO
ALTER TABLE [dbo].[transaction_hdr] WITH CHECK ADD CONSTRAINT [FK_transaction_hdr_map] FOREIGN KEY([map_id])
REFERENCES [dbo].[transaction_map] ([id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[transaction_hdr] CHECK CONSTRAINT [FK_transaction_hdr_map]
GO
ALTER TABLE [dbo].[transaction_map] WITH CHECK ADD CONSTRAINT [FK_transaction_map_mstr] FOREIGN KEY([mstr_id])
REFERENCES [dbo].[transaction_mstr] ([id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[transaction_map] CHECK CONSTRAINT [FK_transaction_map_mstr]
GO
Cascading Deletes
I have been on the fence about using cascading deletes. I can easily enough script this, but thought it might be useful for referential integrity.
Updated Index Code (slowed performance significantly)
CREATE TABLE [dbo].[transaction_dtl](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[mstr_id] [int] NOT NULL,
[map_id] [bigint] NOT NULL,
[hdr_id] [bigint] NOT NULL,
[trx_id] [bigint] NULL,
[trx_parent_id] [int] NULL,
[agent_id] [smallint] NULL,
[rate_id] [smallint] NULL,
[frequency_id] [tinyint] NULL,
[period_days] [tinyint] NULL,
[commission] [numeric](38, 6) NULL,
[row_create_ts] [datetime] NULL,
CONSTRAINT [PK_transaction_dtl] PRIMARY KEY CLUSTERED
(
[mstr_id] ASC,
[map_id] ASC,
[hdr_id] ASC,
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[transaction_hdr](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[mstr_id] [int] NOT NULL,
[map_id] [bigint] NOT NULL,
[trx_id] [bigint] NULL,
[trx_parent_id] [int] NULL,
[trx_other_id] [int] NULL,
[level] [tinyint] NULL,
[days_held] [smallint] NULL,
[units_bal] [numeric](38, 6) NULL,
[reversal] [int] NULL,
[sortkey] [varchar](300) NULL,
[row_create_ts] [datetime] NULL,
CONSTRAINT [PK_transaction_hdr] PRIMARY KEY CLUSTERED
(
[mstr_id] ASC,
[map_id] ASC,
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[transaction_map](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[mstr_id] [int] NOT NULL,
[fund_id] [tinyint] NULL,
[account_id] [int] NULL,
[wholesaler_id] [int] NULL,
[unbalanced] [bit] NULL,
[nobalance] [bit] NULL,
[row_create_ts] [datetime] NULL,
CONSTRAINT [PK_transaction_map] PRIMARY KEY CLUSTERED
(
[mstr_id] ASC,
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[transaction_mstr](
[id] [int] IDENTITY(1,1) NOT NULL,
[caller_sid] [varbinary](85) NOT NULL,
[caller_name] [sysname] NOT NULL,
[status] [tinyint] NOT NULL,
[start_time] [datetimeoffset](7) NULL,
[end_time] [datetimeoffset](7) NULL,
[commission_type] [tinyint] NULL,
[commission_date] [date] NULL,
[run_date] [date] NULL,
[frequency_id] [tinyint] NULL,
[closed] [tinyint] NOT NULL,
[row_create_ts] [datetime] NOT NULL,
[row_modified_ts] [datetime] NULL,
CONSTRAINT [PK_transaction_mstr] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[transaction_dtl] ADD CONSTRAINT [DF_commission_dtl_row_create_ts] DEFAULT (getdate()) FOR [row_create_ts]
GO
ALTER TABLE [dbo].[transaction_hdr] ADD CONSTRAINT [DF_transaction_hdr_reversal] DEFAULT ((0)) FOR [reversal]
GO
ALTER TABLE [dbo].[transaction_hdr] ADD CONSTRAINT [DF_commission_hdr_row_create_ts] DEFAULT (getdate()) FOR [row_create_ts]
GO
ALTER TABLE [dbo].[transaction_map] ADD CONSTRAINT [DF_transaction_map_balanced] DEFAULT ((0)) FOR [unbalanced]
GO
ALTER TABLE [dbo].[transaction_map] ADD CONSTRAINT [DF_transaction_map_nobalance] DEFAULT ((0)) FOR [nobalance]
GO
ALTER TABLE [dbo].[transaction_map] ADD CONSTRAINT [DF_commission_map_row_create_ts] DEFAULT (getdate()) FOR [row_create_ts]
GO
ALTER TABLE [dbo].[transaction_mstr] ADD CONSTRAINT [DF_transaction_master_closed] DEFAULT ((0)) FOR [closed]
GO
ALTER TABLE [dbo].[transaction_mstr] ADD CONSTRAINT [DF_transaction_master_create_row_ts] DEFAULT (getdate()) FOR [row_create_ts]
GO
ALTER TABLE [dbo].[transaction_dtl] WITH CHECK ADD CONSTRAINT [FK_transaction_dtl_transaction_hdr] FOREIGN KEY([mstr_id], [map_id], [hdr_id])
REFERENCES [dbo].[transaction_hdr] ([mstr_id], [map_id], [id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[transaction_dtl] CHECK CONSTRAINT [FK_transaction_dtl_transaction_hdr]
GO
ALTER TABLE [dbo].[transaction_hdr] WITH CHECK ADD CONSTRAINT [FK_transaction_hdr_transaction_map] FOREIGN KEY([mstr_id], [map_id])
REFERENCES [dbo].[transaction_map] ([mstr_id], [id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[transaction_hdr] CHECK CONSTRAINT [FK_transaction_hdr_transaction_map]
GO
ALTER TABLE [dbo].[transaction_map] WITH CHECK ADD CONSTRAINT [FK_transaction_map_transaction_mstr] FOREIGN KEY([mstr_id])
REFERENCES [dbo].[transaction_mstr] ([id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[transaction_map] CHECK CONSTRAINT [FK_transaction_map_transaction_mstr]
GO
CREATE TABLE
statements and allCREATE INDEX
statements. Also. Describe how you're doing theINSERT
s. A system half that powerful should be able to add 1M rows to each table in <5m.INSERT
andDELETE
statements you're using, as those will determine what the proper indexing should be (and may also provide a clue why your queries are running so slow). Even your calculations taking minutes on a table that size seems under-performant.