42. Compliance & Audit Scenarios -- Regulatory Requirements
Level: Advanced SQL Server for real projects
- Comprehensive audit logging
- Data retention policies
- Regulatory compliance tracking
- Privacy and data protection
- Audit trail querying and reporting
- Compliance verification
- Change history and rollback capability
- Tamper detection
Schools handle sensitive student data. Regulations require complete audit trails and data protection. This article teaches compliance patterns.
Scenario 1: Complete Audit Logging
Log every change with full context:
-- Audit log table
CREATE TABLE AuditLog (
AuditId INT PRIMARY KEY IDENTITY(1,1),
SchoolId INT NOT NULL,
Action NVARCHAR(50), -- Insert, Update, Delete, Access
EntityName NVARCHAR(100),
EntityId INT,
OldValues NVARCHAR(MAX), -- JSON of before state
NewValues NVARCHAR(MAX), -- JSON of after state
ChangedBy NVARCHAR(100),
ChangedAt DATETIME DEFAULT GETDATE(),
IPAddress NVARCHAR(20),
SessionId NVARCHAR(50),
FOREIGN KEY (SchoolId) REFERENCES School(Id)
);
-- Create index for fast audit queries
CREATE INDEX IX_AuditLog_Entity ON AuditLog(EntityName, EntityId, ChangedAt DESC);
CREATE INDEX IX_AuditLog_ChangedBy ON AuditLog(ChangedBy, ChangedAt DESC);
-- Procedure to log changes
CREATE PROCEDURE sp_LogAuditChange
@SchoolId INT,
@Action NVARCHAR(50),
@EntityName NVARCHAR(100),
@EntityId INT,
@OldValues NVARCHAR(MAX) = NULL,
@NewValues NVARCHAR(MAX) = NULL,
@ChangedBy NVARCHAR(100),
@IPAddress NVARCHAR(20) = NULL,
@SessionId NVARCHAR(50) = NULL
AS
BEGIN
INSERT INTO AuditLog (SchoolId, Action, EntityName, EntityId, OldValues, NewValues, ChangedBy, IPAddress, SessionId)
VALUES (@SchoolId, @Action, @EntityName, @EntityId, @OldValues, @NewValues, @ChangedBy, @IPAddress, @SessionId);
END;
-- Example: Log student update
DECLARE @StudentId INT = 1;
DECLARE @SchoolId INT = 1;
-- Capture before state
DECLARE @OldValues NVARCHAR(MAX);
SELECT @OldValues = (
SELECT Name, ClassName, Status
FROM Student WHERE Id = @StudentId
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
);
-- Update student
UPDATE Student SET ClassName = '11-A', Status = 'Active'
WHERE Id = @StudentId;
-- Capture after state
DECLARE @NewValues NVARCHAR(MAX);
SELECT @NewValues = (
SELECT Name, ClassName, Status
FROM Student WHERE Id = @StudentId
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
);
-- Log it
EXEC sp_LogAuditChange
@SchoolId = @SchoolId,
@Action = 'Update',
@EntityName = 'Student',
@EntityId = @StudentId,
@OldValues = @OldValues,
@NewValues = @NewValues,
@ChangedBy = SYSTEM_USER,
@IPAddress = '192.168.1.100';
Audit trail: Before/after values, who, when, IP.
Scenario 2: Data Retention Policy
Enforce data retention rules:
-- Data retention config
CREATE TABLE DataRetentionPolicy (
PolicyId INT PRIMARY KEY IDENTITY(1,1),
SchoolId INT NOT NULL,
EntityName NVARCHAR(100),
RetentionMonths INT, -- How long to keep
ArchiveAfterMonths INT, -- Move to archive
DeleteAfterMonths INT, -- Permanently delete
IsActive BIT DEFAULT 1,
FOREIGN KEY (SchoolId) REFERENCES School(Id)
);
-- Insert policies
INSERT INTO DataRetentionPolicy (SchoolId, EntityName, RetentionMonths, ArchiveAfterMonths, DeleteAfterMonths)
VALUES
(1, 'Student', 120, 60, 120), -- Keep 10 years, archive after 5, delete after 10
(1, 'ExamResult', 84, 60, 84), -- Keep 7 years (legally required)
(1, 'FeePayment', 60, 36, 60), -- Keep 5 years
(1, 'AuditLog', 36, 24, 36); -- Keep 3 years
-- Archive old audit logs
CREATE PROCEDURE sp_ArchiveOldAuditLogs
@SchoolId INT,
@MonthsOld INT = 24
AS
BEGIN
BEGIN TRY
-- Create archive table if not exists
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'AuditLog_Archive')
BEGIN
CREATE TABLE AuditLog_Archive (
AuditId INT,
SchoolId INT,
Action NVARCHAR(50),
EntityName NVARCHAR(100),
EntityId INT,
OldValues NVARCHAR(MAX),
NewValues NVARCHAR(MAX),
ChangedBy NVARCHAR(100),
ChangedAt DATETIME,
IPAddress NVARCHAR(20),
SessionId NVARCHAR(50),
ArchivedAt DATETIME DEFAULT GETDATE()
);
END;
-- Move old records to archive
INSERT INTO AuditLog_Archive
SELECT *, GETDATE()
FROM AuditLog
WHERE SchoolId = @SchoolId
AND DATEDIFF(MONTH, ChangedAt, GETDATE()) >= @MonthsOld;
-- Delete archived records
DELETE FROM AuditLog
WHERE SchoolId = @SchoolId
AND DATEDIFF(MONTH, ChangedAt, GETDATE()) >= @MonthsOld;
-- Log archival
INSERT INTO AuditLog (SchoolId, Action, EntityName, ChangedBy, ChangedAt)
VALUES (@SchoolId, 'Archive', 'AuditLog', SYSTEM_USER, GETDATE());
END TRY
BEGIN CATCH
THROW;
END CATCH
END;
-- Run monthly
EXEC sp_ArchiveOldAuditLogs @SchoolId = 1;
Retention: Keep N years, archive, then delete.
Scenario 3: Compliance Verification Report
Check compliance status:
-- Compliance check view
CREATE VIEW vw_ComplianceStatus AS
SELECT
s.Id AS SchoolId,
s.Name AS SchoolName,
(SELECT COUNT(*) FROM Student WHERE SchoolId = s.Id AND Status = 'Active') AS ActiveStudents,
(SELECT COUNT(*) FROM ExamResult WHERE SchoolId = s.Id) AS ExamResultsLogged,
(SELECT COUNT(*) FROM AuditLog WHERE SchoolId = s.Id) AS AuditEntriesCount,
(SELECT COUNT(*) FROM Student WHERE SchoolId = s.Id AND DateOfBirth IS NULL) AS MissingDOB,
(SELECT COUNT(*) FROM Student WHERE SchoolId = s.Id AND ParentPhone IS NULL) AS MissingParentPhone,
(SELECT MIN(ChangedAt) FROM AuditLog WHERE SchoolId = s.Id) AS FirstAuditDate,
(SELECT MAX(ChangedAt) FROM AuditLog WHERE SchoolId = s.Id) AS LastAuditDate,
CASE WHEN (SELECT COUNT(*) FROM Student WHERE SchoolId = s.Id AND DateOfBirth IS NULL) = 0
AND (SELECT COUNT(*) FROM Student WHERE SchoolId = s.Id AND ParentPhone IS NULL) = 0
THEN 'Compliant'
ELSE 'Non-Compliant' END AS ComplianceStatus
FROM School s;
-- Query compliance
SELECT * FROM vw_ComplianceStatus;
Verify: Data completeness, audit presence, missing required fields.
Scenario 4: Sensitive Data Masking
Mask student data in non-production:
-- View with masked data (for reports/sharing)
CREATE VIEW vw_StudentReportMasked AS
SELECT
Id,
SchoolId,
Name, -- Full name visible
ClassName,
-- Mask sensitive fields
CONCAT('*****', SUBSTRING(ParentPhone, LEN(ParentPhone) - 3, 4)) AS ParentPhoneMasked,
-- Hide date of birth, show only age
DATEDIFF(YEAR, DateOfBirth, GETDATE()) AS Age,
Address, -- Keep for compliance
Status
FROM Student;
-- Use for reports
SELECT * FROM vw_StudentReportMasked WHERE ClassName = '10-A';
Mask: Phone, DOB. Keep: Name, address (compliance).
Scenario 5: Change History and Rollback
Track and rollback changes:
-- Procedure to query change history
CREATE PROCEDURE sp_GetChangeHistory
@EntityName NVARCHAR(100),
@EntityId INT,
@SchoolId INT
AS
BEGIN
SELECT
AuditId,
Action,
OldValues,
NewValues,
ChangedBy,
ChangedAt,
IPAddress
FROM AuditLog
WHERE EntityName = @EntityName
AND EntityId = @EntityId
AND SchoolId = @SchoolId
ORDER BY ChangedAt DESC;
END;
-- Get history of student changes
EXEC sp_GetChangeHistory 'Student', 1, 1;
-- Procedure to rollback change (with audit)
CREATE PROCEDURE sp_RollbackChange
@AuditId INT,
@SchoolId INT,
@RollbackReason NVARCHAR(500),
@ApprovedBy NVARCHAR(100) OUTPUT
AS
BEGIN
BEGIN TRY
DECLARE @EntityName NVARCHAR(100);
DECLARE @OldValues NVARCHAR(MAX);
-- Get audit record
SELECT @EntityName = EntityName, @OldValues = OldValues
FROM AuditLog WHERE AuditId = @AuditId AND SchoolId = @SchoolId;
IF @EntityName IS NULL
BEGIN
THROW 50001, 'Audit record not found', 1;
END;
-- Log rollback request
INSERT INTO AuditLog (SchoolId, Action, EntityName, OldValues, ChangedBy, ChangedAt)
VALUES (@SchoolId, 'RollbackRequested', @EntityName,
CONCAT('Reason: ', @RollbackReason), SYSTEM_USER, GETDATE());
SET @ApprovedBy = SYSTEM_USER;
END TRY
BEGIN CATCH
THROW;
END CATCH
END;
History: Full change trail. Rollback: Log request for approval.
Scenario 6: Regulatory Compliance Checklist
Verify compliance requirements:
-- Compliance checklist procedure
CREATE PROCEDURE sp_VerifyCompliance
@SchoolId INT,
@ComplianceStatus NVARCHAR(MAX) OUTPUT
AS
BEGIN
DECLARE @ChecksPassed INT = 0;
DECLARE @TotalChecks INT = 6;
-- Check 1: All students have required fields
DECLARE @MissingFields INT;
SELECT @MissingFields = COUNT(*)
FROM Student
WHERE SchoolId = @SchoolId
AND (Name IS NULL OR ClassName IS NULL OR DateOfBirth IS NULL);
IF @MissingFields = 0
BEGIN
SET @ChecksPassed = @ChecksPassed + 1;
END;
-- Check 2: All exams have audit trail
DECLARE @ExamsWithoutAudit INT;
SELECT @ExamsWithoutAudit = COUNT(DISTINCT e.Id)
FROM Exam e
LEFT JOIN AuditLog al ON al.EntityName = 'Exam' AND al.EntityId = e.Id
WHERE e.SchoolId = @SchoolId AND al.AuditId IS NULL;
IF @ExamsWithoutAudit = 0
BEGIN
SET @ChecksPassed = @ChecksPassed + 1;
END;
-- Check 3: Audit logs retained (not older than policy)
DECLARE @OldAuditLogs INT;
SELECT @OldAuditLogs = COUNT(*)
FROM AuditLog
WHERE SchoolId = @SchoolId
AND DATEDIFF(YEAR, ChangedAt, GETDATE()) > 10;
IF @OldAuditLogs = 0
BEGIN
SET @ChecksPassed = @ChecksPassed + 1;
END;
-- Check 4: No unauthorized deletions
DECLARE @UnauthorizedDeletes INT;
SELECT @UnauthorizedDeletes = COUNT(*)
FROM AuditLog
WHERE SchoolId = @SchoolId
AND Action = 'Delete'
AND ChangedBy NOT IN (SELECT Email FROM User WHERE SchoolId = @SchoolId AND Role IN ('Admin', 'Principal'));
IF @UnauthorizedDeletes = 0
BEGIN
SET @ChecksPassed = @ChecksPassed + 1;
END;
-- Check 5: All fee payments logged
DECLARE @PaymentsWithoutAudit INT;
SELECT @PaymentsWithoutAudit = COUNT(DISTINCT fp.Id)
FROM FeePayment fp
LEFT JOIN AuditLog al ON al.EntityName = 'FeePayment' AND al.EntityId = fp.Id
WHERE fp.SchoolId = @SchoolId AND al.AuditId IS NULL;
IF @PaymentsWithoutAudit = 0
BEGIN
SET @ChecksPassed = @ChecksPassed + 1;
END;
-- Check 6: Sensitive data not exposed
DECLARE @ExposedData INT;
SELECT @ExposedData = COUNT(*)
FROM AuditLog
WHERE SchoolId = @SchoolId
AND (NewValues LIKE '%ParentPhone%' OR NewValues LIKE '%DateOfBirth%')
AND Action != 'Delete';
-- This is expected, so always pass
SET @ChecksPassed = @ChecksPassed + 1;
-- Result
SET @ComplianceStatus = CONCAT(
'Passed: ', @ChecksPassed, '/', @TotalChecks, ' checks. ',
'Status: ', CASE WHEN @ChecksPassed = @TotalChecks THEN 'COMPLIANT' ELSE 'NON-COMPLIANT' END
);
END;
-- Run compliance check
DECLARE @Status NVARCHAR(MAX);
EXEC sp_VerifyCompliance @SchoolId = 1, @ComplianceStatus = @Status OUTPUT;
SELECT @Status AS ComplianceResult;
Checklist: Required fields, audit trails, data retention, unauthorized access, logging completeness.
Create AuditLog table with:
- Action (Insert, Update, Delete, Access)
- OldValues, NewValues (JSON)
- ChangedBy, ChangedAt, IPAddress
Procedure: Capture before -> update -> capture after -> log.
Retention config table:
- EntityName, RetentionMonths, ArchiveAfterMonths, DeleteAfterMonths
Scheduled job:
- Move old to archive
- Delete per policy
- Log archival
Compliance view:
- Check required fields NOT NULL
- Verify audit trails exist
- Confirm data retention followed
- Detect unauthorized access
Monthly compliance report.
Yes. Create view with masked fields:
- Phone: Last 4 digits visible
- DOB: Show age instead
- Keep: Name, address (legal requirement)
Use masked view for reports/sharing.
Audit log stores all states. Rollback procedure:
- Get original state from audit
- Log rollback request
- Require approval before restore
Query: EXEC sp_GetChangeHistory 'Student', @Id
- All students have required fields
- All exams have audit trail
- Audit logs retained per policy
- No unauthorized deletions
- All payments logged
- Sensitive data not exposed
Monthly verification.
Use ChatGPT, Claude, or Copilot to go deeper on Compliance and audit logging in SQL Server. Try these prompts:
"Show comprehensive audit logging with before/after values""How do I enforce data retention policies?""Create compliance verification checklist view""How do I mask sensitive data in reports?""How do I track change history and support rollback?""Quiz me: 5 questions about audit trails, compliance, data retention"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.