I have a big performance problem with my SQL Server stored procedure. It uses a WHILE loop to handle payment calculations and it’s super slow.
The procedure processes payment data by looping through each batch_num in a temp table called payment_batch_temp. For each batch, it does premium calculations, updates the policy_transaction_main table, and inserts records into payment_processing_log.
The main issue: Processing 300K policies takes 15 hours because of the row-by-row approach and lots of subqueries in the loop.
What the loop currently does:
- Goes through each
batch_numinpayment_batch_temp - Calculates premium amounts and applies business rules to update
policy_transaction_main - Inserts results into
payment_processing_log - Updates
processed_flagin bothpayment_batch_tempandpolicy_transaction_main - Sometimes creates new rows in
policy_transaction_mainfor leftover premium amounts
My main question: How can I convert this whole thing to work with sets instead of loops while keeping the exact same logic?
Requirements:
- Final results must match exactly what the loop produces
- Main tables:
payment_batch_temp,policy_transaction_main,payment_processing_log,payment_source_temp,policy_data_temp - The
payment_batch_temptable groups payment records bybatch_num - Each
batch_numrepresents a group of records processed together
I’m looking for CTE examples, bulk update patterns, or any proven methods to eliminate this loop bottleneck.
truncate table payment_batch_temp
insert into payment_batch_temp WITH(tablock)(contract_id,premium_amount,transaction_id,process_date,created_date,batch_num,system_id,agent_id,original_transaction_id,processed_status,policy_updated_flag,manual_entry_flag,premium_type,coverage_item,coverage_location,wind_coverage,endorsement_flag,contract_year,reinsurance_date,custom_field5)
select contract_id,premium_amount,transaction_id,process_date,created_date,row_number() over (partition by contract_id,system_id order by effective_date asc, manual_entry_flag desc,coverage_item,premium_amount desc,transaction_id) as batch_num,system_id,agent_id,original_transaction_id,0,policy_updated_flag,manual_entry_flag,premium_type,coverage_item,coverage_location,wind_coverage,endorsement_flag,contract_year,reinsurance_date,custom_field5
from (
select distinct p.contract_id, p.premium_amount, p.transaction_id, p.effective_date as process_date,p.created_date,p.system_id
,case when p.agent_id is not null and p.agent_id <> 0 then p.agent_id else m.agent_id end as agent_id
,p.original_transaction_id
,p.effective_date
,p.manual_entry_flag
,p.policy_updated_flag
,p.transaction_attribute_3 as premium_type,p.coverage_item,p.coverage_location,p.wind_coverage,p.endorsement_flag,p.contract_year,p.reinsurance_date,m.custom_field5
from payment_source_temp p with (nolock)
JOIN policy_transaction_main m with (nolock)
ON m.contract_id = p.contract_id
and m.system_id = p.system_id
and (isnull(m.premium_type,'STANDARD')) = isnull(p.transaction_attribute_3,'STANDARD')
where (
(m.transaction_type_code <> 4 and
(cast(p.effective_date as date) <= cast(m.process_date as date))
)
OR
(isnull(p.manual_entry_flag,0) = 0 and m.transaction_type_code = 4 and (cast(p.effective_date as date) < cast(m.process_date as date)))
OR
(isnull(p.manual_entry_flag,0) = 1 and m.transaction_type_code = 4 and (cast(p.effective_date as date) <= cast(m.process_date as date)))
)
and m.record_type = 2
and m.batch_id = @current_batch_id
and isnull(p.processed_indicator,0) = 0
and sign(p.premium_amount) = 1
)a
SELECT @max_batch = max(batch_num), @min_batch = min(batch_num) from payment_batch_temp with (nolock)
WHILE (@min_batch <= @max_batch)
BEGIN
UPDATE target SET
target.commission_premium = CASE WHEN calc.has_payment = 1 THEN
isnull(target.commission_premium,0) +
CASE WHEN row_rank = 1 then calc.calculated_commission +
CASE WHEN calc.payment_amount > calc.total_amount then 0
else calc.balance_adjustment end
else calc.calculated_commission END
ELSE isnull(target.commission_premium,0) END
,target.outstanding_premium = CASE WHEN calc.has_payment = 1 THEN CASE WHEN calc.row_rank = 1 THEN calc.calculated_outstanding - calc.balance_adjustment ELSE calc.calculated_outstanding END ELSE target.outstanding_premium END
,target.processed_status = CASE WHEN (CASE WHEN calc.row_rank = 1 THEN calc.calculated_outstanding - calc.balance_adjustment ELSE calc.calculated_outstanding END) = 0 AND calc.total_amount <> 0 AND calc.has_payment = 1 THEN '1' ELSE CASE WHEN calc.total_amount <> 0 AND calc.has_payment = 1 THEN '2' ELSE 'N' END END
,target.collected_premium = CASE WHEN calc.has_payment = 1 THEN target.term_premium - calc.calculated_outstanding ELSE target.collected_premium END
,target.has_payment_flag = CASE WHEN calc.has_payment = 0 THEN 0 WHEN calc.total_amount <> 0 THEN 1 ELSE 0 END
,target.payment_process_date = calc.process_date
,target.payment_created_date = calc.payment_created_date_new
,process_step = 'STEP: 3', notes = 'Applied payment processing for batch - '+ cast(@current_batch_id as varchar(100))
output calc.transaction_id
into payment_processing_log(transaction_id)
from policy_transaction_main target
JOIN (
Select CASE WHEN balance_adjustment < 0.00 then row_number() over (partition by contract_id,system_id order by calculated_outstanding asc, calculated_commission asc)
else row_number() over (partition by contract_id,system_id order by calculated_outstanding desc, calculated_commission asc) end as row_rank,*
from (
select pb.premium_amount as payment_amount
,CASE WHEN pm.total_premium = 0
THEN CAST(CASE WHEN isnull(pm.outstanding_premium,0) <> 0 THEN pm.outstanding_premium ELSE pm.premium_change END AS numeric(18,3))
ELSE CASE WHEN cast(((CAST(CASE WHEN isnull(pm.outstanding_premium,0) <> 0 THEN pm.outstanding_premium ELSE pm.premium_change END AS numeric(18,3)) / pm.total_premium) * pb.premium_amount) as numeric(18,2)) > CAST(CASE WHEN isnull(pm.outstanding_premium,0) <> 0 THEN pm.outstanding_premium ELSE pm.premium_change END AS numeric(18,3)) AND CAST(CASE WHEN isnull(pm.outstanding_premium,0) <> 0 THEN pm.outstanding_premium ELSE pm.premium_change END AS numeric(18,3)) > 0
THEN CAST(CASE WHEN isnull(pm.outstanding_premium,0) <> 0 THEN pm.outstanding_premium ELSE pm.premium_change END AS numeric(18,3))
ELSE cast(((CAST(CASE WHEN isnull(pm.outstanding_premium,0) <> 0 THEN pm.outstanding_premium ELSE pm.premium_change END AS numeric(18,3)) / pm.total_premium) * pb.premium_amount) as numeric(18,2))
END
END as calculated_commission
,CASE WHEN pm.total_premium = 0
THEN CAST(CASE WHEN isnull(pm.outstanding_premium,0) <> 0 THEN pm.outstanding_premium ELSE pm.premium_change END AS numeric(18,3))
ELSE CAST(CASE WHEN isnull(pm.outstanding_premium,0) <> 0 THEN pm.outstanding_premium ELSE pm.premium_change END AS numeric(18,3)) - cast(((CAST(CASE WHEN isnull(pm.outstanding_premium,0) <> 0 THEN pm.outstanding_premium ELSE pm.premium_change END AS numeric(18,3)) / pm.total_premium) * pb.premium_amount) as numeric(18,2))
END as calculated_outstanding
,pm.total_premium as total_amount
,isnull(pb.has_payment,0) as has_payment
,balance_adjustment = pb.premium_amount - case when pm.total_premium = 0 then 0
else sum(cast(((isnull(pm.outstanding_premium,0) / pm.total_premium) * pb.premium_amount) as numeric(18,2))) over (partition by pm.contract_id,pm.system_id) end
,pm.*
,pb.process_date
,pb.transaction_id
,pb.original_transaction_id
,pb.created_date as payment_created_date_new
FROM (
select contract_id,premium_amount,1 as has_payment,process_date,transaction_id,created_date,system_id,original_transaction_id,premium_type
from payment_batch_temp with (nolock)
where batch_num = @min_batch
and processed_status = 0
)pb
JOIN (select SUM(CAST(CASE WHEN isnull(mt.outstanding_premium,0) <> 0 THEN mt.outstanding_premium ELSE mt.premium_change END AS numeric(18,3))) over(partition by mt.contract_id,mt.system_id) as total_premium, mt.*
from (
select *, dense_rank() over(partition by a.contract_id,a.system_id order by a.effective_date,a.expiration_date asc,contract_year,transaction_ref_id) as date_rank
from policy_transaction_main a with (nolock)
where batch_id = @current_batch_id and processed_status IN ('N','2')
)mt
join payment_batch_temp pi with (nolock) on mt.contract_id = pi.contract_id and mt.system_id = pi.system_id
and (isnull(mt.premium_type,'STANDARD')) = isnull(pi.premium_type,'STANDARD')
where mt.batch_id = @current_batch_id
and CAST(CASE WHEN isnull(mt.outstanding_premium,0) <> 0 THEN mt.outstanding_premium ELSE mt.premium_change END AS numeric(18,3)) <> 0
and mt.record_type in(1,4)
AND mt.processed_status IN ('N','2')
and pi.batch_num = @min_batch
and (isnull(pi.agent_id,0) = 0 or isnull(pi.agent_id,0) = mt.agent_id)
and date_rank = 1
) pm
ON pb.contract_id = pm.contract_id
and pb.system_id = pm.system_id
and (isnull(pb.premium_type,'STANDARD')) = isnull(pm.premium_type,'STANDARD')
WHERE pm.batch_id = @current_batch_id
and pm.total_premium <> 0
and pm.record_type in(1,4)
AND pm.processed_status IN ('N','2')
)calculation_data
)calc
ON target.record_id = calc.record_id
update p set processed_status = 1
from payment_batch_temp p
join payment_processing_log pl with (nolock)
on pl.transaction_id = p.transaction_id
SET @min_batch += 1
END