Sangeetha-Grantha

Metadata Value
Status Active
Version 1.1.0
Last Updated 2026-09-10
Author Sangeetha Grantha Team
Document Type Design reference

Query Optimization Evaluation: Krithi UPDATE Operations


[!NOTE] Design/reference material: this page may include proposals or earlier implementation assumptions. Use current feature map for implemented behavior and current operating steps.


Current Query Pattern Analysis

Observed Query Sequence (from logs)

When updating only temple_id for a Krithi:

  1. UPDATE query - Updates ALL fields (even unchanged ones):
    UPDATE krithis SET 
      title='...', title_normalized='...', incipit='...', 
      incipit_normalized='...', composer_id='...', 
      musical_form='KRITHI'::musical_form_enum, 
      primary_language='sa'::language_code_enum, 
      tala_id='...', temple_id='...',  -- Only this changed!
      is_ragamalika=FALSE, workflow_state='draft'::workflow_state_enum,
      sahitya_summary='...', notes='...', updated_at='...'
    WHERE krithis.id = '...'
    
  2. SELECT krithi_ragas - Always executed if ragaIds parameter is provided:
    SELECT krithi_ragas.krithi_id, krithi_ragas.raga_id, 
           krithi_ragas.order_index, krithi_ragas."section", 
           krithi_ragas.notes 
    FROM krithi_ragas 
    WHERE krithi_ragas.krithi_id = '...'
    
  3. SELECT krithis - To return updated DTO:
    SELECT krithis.id, krithis.title, krithis.incipit, ...
    FROM krithis 
    WHERE krithis.id = '...'
    
  4. SELECT krithi_tags - If tags are being updated:
    SELECT krithi_tags.krithi_id, krithi_tags.tag_id, ...
    FROM krithi_tags 
    WHERE krithi_tags.krithi_id = '...'
    
  5. INSERT audit_log - Audit trail entry

Issues Identified

1. Inefficient UPDATE: All Fields Included

Problem: The UPDATE statement includes ALL non-null fields from the request, even if they haven’t changed.

Impact:

Root Cause: Exposed’s update method includes all fields set in the lambda, regardless of actual changes.

2. Redundant SELECT After UPDATE

Problem: After updating, a separate SELECT is executed to fetch the updated row.

Impact:

Better Approach: Use PostgreSQL’s RETURNING clause to get updated row in same query.

Problem:

Impact:

4. No Change Detection

Problem: No comparison with existing values before updating.

Impact:

Optimization Recommendations

Priority 1: Use PostgreSQL RETURNING Clause

Current:

val updated = KrithisTable.update(...) { ... }
// ... raga updates ...
KrithisTable.selectAll().where { ... }.map { it.toKrithiDto() }.singleOrNull()

Optimized:

val updatedRow = KrithisTable.update({ ... }) { ... }
    .returning()
    .singleOrNull()
    ?.let { it.toKrithiDto() }

Benefits:

Note: Confirmed Exposed v1 supports RETURNING.

Priority 2: Selective Field Updates (Change Detection)

Current:

KrithisTable.update({ KrithisTable.id eq id.toJavaUuid() }) {
    title?.let { value -> it[KrithisTable.title] = value }
    templeId?.let { value -> it[KrithisTable.templeId] = value }
    // ... all fields included if non-null
}

Optimized:

// Fetch existing row first (or cache from previous query)
val existing = KrithisTable.selectAll()
    .where { KrithisTable.id eq id.toJavaUuid() }
    .singleOrNull() ?: return@dbQuery null

// Only update changed fields
KrithisTable.update({ KrithisTable.id eq id.toJavaUuid() }) {
    if (title != null && title != existing[KrithisTable.title]) {
        it[KrithisTable.title] = title
    }
    if (templeId != null && templeId != existing[KrithisTable.templeId]) {
        it[KrithisTable.templeId] = templeId
    }
    // ... only include changed fields
    it[KrithisTable.updatedAt] = now
}

Trade-off: Adds one SELECT upfront, but:

Alternative: If frontend sends only changed fields, this becomes easier.

Current:

ragaIds?.let { ragas ->
    // Always queries existing ragas
    val existingRagas = KrithiRagasTable.selectAll()...
}

Optimized:

// Only query if ragas are actually being updated
if (ragaIds != null) {
    val existingRagas = KrithiRagasTable.selectAll()...
    // ... update logic
}

Note: Current code already does this with ?.let, but ensure it’s not being called unnecessarily.

Priority 4: Batch DELETE Operations

Current:

toDelete.forEach { (ragaId, orderIndex) ->
    KrithiRagasTable.deleteWhere {
        (KrithiRagasTable.krithiId eq javaKrithiId) and
        (KrithiRagasTable.ragaId eq ragaId) and
        (KrithiRagasTable.orderIndex eq orderIndex)
    }
}

Optimized:

if (toDelete.isNotEmpty()) {
    // Single DELETE with IN clause for composite key
    // Note: PostgreSQL supports composite IN, but Exposed may need workaround
    KrithiRagasTable.deleteWhere {
        (KrithiRagasTable.krithiId eq javaKrithiId) and
        // Use OR conditions for composite key deletes
        // Or use raw SQL if Exposed doesn't support efficiently
    }
}

Challenge: Exposed may not support efficient composite key batch deletes. May need raw SQL:

DELETE FROM krithi_ragas 
WHERE krithi_id = ? 
  AND (raga_id, order_index) IN ((?, ?), (?, ?), ...)

Priority 5: Combine Queries with JOINs (If Needed)

If the frontend needs ragas/tags in the response, consider:

Current: Separate queries Optimized: Single query with LEFT JOINs (if Exposed supports efficiently)

Trade-off: More complex query, but fewer round-trips. Only beneficial if data is always needed.

Implementation Strategy

Phase 1: Quick Wins (Low Risk) ✅ COMPLETED

  1. ✅ Use RETURNING clause - Implemented across all repositories
  2. ✅ Add conditional checks to skip unnecessary related data queries - Implemented
  3. ✅ Batch DELETE operations where possible - Implemented in collection updates

Phase 2: Change Detection (Medium Risk) ✅ COMPLETED

  1. ✅ Smart diffing algorithm implemented for collection updates
  2. ✅ Only changed fields are updated in collections (sections, tags, ragas)
  3. ✅ Metadata preservation for unchanged records

Phase 3: Advanced Optimizations (Higher Risk) ⚠️ DEFERRED

  1. Raw SQL for complex batch operations - Not needed, Exposed handles efficiently
  2. Caching strategies for frequently accessed data - Future enhancement
  3. Consider GraphQL-style field selection if frontend supports it - Future enhancement

Expected Impact ✅ ACHIEVED

Query Count Reduction ✅ ACHIEVED

Query Size Reduction ✅ ACHIEVED

Performance Improvement ✅ ACHIEVED

Additional Benefits ✅ ACHIEVED

Code Example: Optimized Update Method ✅ IMPLEMENTED

suspend fun update(
    id: Uuid,
    // ... parameters ...
): KrithiDto? = DatabaseFactory.dbQuery {
    val now = OffsetDateTime.now(ZoneOffset.UTC)
    val javaId = id.toJavaUuid()

    // Use Exposed 1.0.0 updateReturning to update and fetch the row in one round-trip
    val updatedKrithi = KrithisTable
        .updateReturning(
            where = { KrithisTable.id eq javaId }
        ) {
            title?.let { value -> it[KrithisTable.title] = value }
            titleNormalized?.let { value -> it[KrithisTable.titleNormalized] = value }
            // ... other fields
            it[KrithisTable.updatedAt] = now
        }
        .singleOrNull()
        ?.toKrithiDto()

    if (updatedKrithi == null) {
        return@dbQuery null
    }

    // Handle ragas with smart diffing (only if provided)
    ragaIds?.let { ragas ->
        // Smart diffing algorithm for raga updates
        // ... existing raga update logic with delta updates ...
    }

    updatedKrithi
}

Key Improvements:

Testing Considerations

  1. Verify change detection - Ensure unchanged fields aren’t updated
  2. Test with NULL values - Ensure NULL handling works correctly
  3. Concurrent updates - Ensure no race conditions
  4. Audit logging - Verify audit logs still capture updates correctly
  5. Performance benchmarks - Measure before/after query counts and execution times

Notes

Implementation Status

Status: ✅ COMPLETED (2025-01-27)

Repositories Optimized:

Related Documentation:


Section index · Documentation home · Feature status