Penamaan boolean di query builder bukan sekadar soal gaya menulis kode. Saat query mulai lambat, data tumbuh, dan logika SQL dipenuhi kondisi opsional, nama boolean yang jelas membantu tim memahami mengapa sebuah filter, hint, pagination strategy, atau count query dijalankan.

Masalah utamanya bukan hanya keterbacaan. Boolean yang namanya kabur seperti flag, cursor, count, atau active sering membuat developer salah membaca maksud cabang logika. Akibatnya, optimasi SQL bisa meleset: indeks yang seharusnya dipakai tidak terpakai, COUNT(*) dijalankan di jalur yang salah, atau offset pagination tetap aktif ketika data sudah terlalu besar. Dengan nama seperti isActive, hasIndexHint, shouldUseCursor, dan needsCountQuery, keputusan query menjadi eksplisit dan lebih mudah diaudit.

Mengapa penamaan boolean penting di lapisan query

Di backend, query jarang ditulis sebagai SQL statis. Umumnya query dibentuk lewat query builder, scope ORM, repository, atau service yang menambahkan kondisi sedikit demi sedikit. Di titik inilah penamaan boolean memengaruhi tiga hal penting:

  • Keterbacaan logika SQL: reviewer bisa cepat melihat kapan filter aktif, kapan strategi pagination berubah, dan kapan query tambahan dijalankan.
  • Akurasi optimasi: saat membaca EXPLAIN atau log query lambat, developer dapat menghubungkan gejala di database dengan cabang logika di kode.
  • Pencegahan optimasi yang salah: boolean yang jelas mengurangi asumsi keliru ketika volume data bertambah dan perilaku query berubah.

Nama boolean yang baik biasanya berbentuk pertanyaan implisit yang bisa dijawab ya/tidak. Karena itu prefiks seperti is, has, should, needs, atau can lebih tepat daripada nama benda atau kata kerja yang ambigu.

Masalah yang sering terjadi saat nama boolean kabur

1. Filter aktif tetapi maksudnya tidak jelas

Variabel seperti active bisa berarti banyak hal: nilai filter dari request, status entitas, atau penanda bahwa kondisi WHERE perlu ditambahkan. Saat dibaca di query builder, maknanya tidak selalu jelas.

// Before: ambigu, sulit ditebak maksudnya
active = request.active

if (active) {
  query.where('users.status', 'active')
}

Apakah active berarti request meminta hanya user aktif, atau hanya menampung string active? Nama yang lebih eksplisit mengurangi kebingungan:

// After: maksudnya jelas
isActive = request.status === 'active'

if (isActive) {
  query.where('users.status', 'active')
}

Dengan bentuk ini, reviewer langsung paham bahwa boolean tersebut menentukan apakah filter status aktif diterapkan.

2. Fallback pagination tidak terbaca

Pada dataset kecil, offset pagination sering cukup. Namun saat tabel membesar, cursor pagination biasanya lebih stabil untuk halaman dalam. Jika boolean hanya diberi nama cursor atau paginationMode tanpa cabang yang eksplisit, mudah terjadi salah pakai.

// Before
cursor = request.cursor

if (cursor) {
  query.where('id', '>', cursor)
} else {
  query.offset(page * limit)
}

Masalahnya, cursor di sini bukan boolean murni. Ia menyimpan nilai cursor sekaligus dipakai sebagai penentu strategi. Lebih aman dipisah:

// After
cursorValue = request.cursor
shouldUseCursor = cursorValue != null

if (shouldUseCursor) {
  query.where('id', '>', cursorValue)
  query.orderBy('id', 'asc')
} else {
  query.offset(page * limit)
}

Pemisahan ini penting ketika Anda menganalisis performa. Saat query lambat muncul pada jalur offset, tim bisa segera memeriksa kenapa shouldUseCursor bernilai false.

3. Count query mahal dijalankan tanpa sadar

COUNT(*) bisa mahal tergantung join, filter, dan ukuran data. Pada beberapa endpoint, total hasil memang dibutuhkan; pada yang lain, yang dibutuhkan hanya satu halaman data. Jika nama boolean terlalu umum seperti count, maksudnya mudah salah ditafsirkan.

// Before
count = includeTotal

if (count) {
  total = query.clone().count()
}

Nama count bisa dibaca sebagai angka atau aksi. Lebih baik gunakan nama yang menyatakan keputusan:

// After
needsCountQuery = includeTotal === true

if (needsCountQuery) {
  total = baseQuery.clone().count()
}

needsCountQuery menegaskan bahwa yang diputuskan adalah perlu atau tidaknya query tambahan, bukan nilai totalnya.

Contoh before/after di query builder yang lebih realistis

Berikut contoh logika query yang umum di backend: ada filter status, opsi index hint, strategi pagination, dan kemungkinan menjalankan COUNT(*).

Before: jalan, tetapi sulit dibaca dan sulit dioptimasi

q = db.table('orders')

active = filters.status === 'active'
hint = config.indexHint
cursor = params.cursor
count = params.includeTotal

if (active) {
  q.where('orders.status', 'active')
}

if (hint) {
  q.useIndex(hint)
}

if (cursor) {
  q.where('orders.id', '>', cursor)
  q.orderBy('orders.id', 'asc')
} else {
  q.orderBy('orders.created_at', 'desc')
  q.offset(params.offset)
}

rows = q.limit(params.limit).get()

total = null
if (count) {
  total = q.clone().count()
}

Ada beberapa masalah:

  • hint tidak memberi tahu apakah hint benar-benar tersedia atau hanya string config mentah.
  • cursor dipakai sebagai nilai sekaligus boolean keputusan.
  • count ambigu: angka, flag, atau aksi.
  • q dipakai ulang untuk data dan count, berisiko menghitung query yang sudah terkena limit/order tertentu jika implementasinya tidak hati-hati.

After: niat optimasi lebih eksplisit

baseQuery = db.table('orders')

isActive = filters.status === 'active'
indexHintValue = config.indexHint
hasIndexHint = indexHintValue != null && indexHintValue !== ''
cursorValue = params.cursor
shouldUseCursor = cursorValue != null
needsCountQuery = params.includeTotal === true

if (isActive) {
  baseQuery.where('orders.status', 'active')
}

if (hasIndexHint) {
  baseQuery.useIndex(indexHintValue)
}

dataQuery = baseQuery.clone()

if (shouldUseCursor) {
  dataQuery.where('orders.id', '>', cursorValue)
  dataQuery.orderBy('orders.id', 'asc')
} else {
  dataQuery.orderBy('orders.created_at', 'desc')
  dataQuery.offset(params.offset)
}

rows = dataQuery.limit(params.limit).get()

total = null
if (needsCountQuery) {
  countQuery = baseQuery.clone()
  total = countQuery.count()
}

Keuntungan bentuk kedua:

  • Reviewer bisa melihat keputusan query tanpa menebak arti variabel.
  • Saat hasil EXPLAIN buruk, tim dapat memeriksa jalur spesifik: apakah hasIndexHint aktif, apakah shouldUseCursor benar, apakah needsCountQuery semestinya dimatikan.
  • Base query dipisahkan dari data query dan count query, sehingga perubahan optimasi lebih aman.

Dampaknya pada analisis query lambat dan debugging EXPLAIN

Saat menerima laporan query lambat, biasanya Anda memeriksa beberapa hal: SQL final, parameter bind, rencana eksekusi, jumlah baris yang dipindai, indeks yang dipakai, dan jalur kode yang memproduksi query tersebut. Nama boolean yang baik mempercepat langkah terakhir.

Menghubungkan log aplikasi dengan SQL final

Jika log aplikasi mencatat keputusan penting seperti:

{
  "isActive": true,
  "hasIndexHint": false,
  "shouldUseCursor": false,
  "needsCountQuery": true
}

maka developer langsung tahu konteks query sebelum membuka database. Ini sangat membantu ketika SQL final tampak kompleks karena banyak kondisi dinamis.

Mencegah salah baca hasil EXPLAIN

Misalnya, hasil EXPLAIN menunjukkan pemindaian baris besar pada endpoint daftar order. Tanpa nama boolean yang jelas, tim bisa buru-buru menambah indeks baru padahal akar masalahnya adalah jalur offset pagination masih aktif untuk halaman dalam. Jika kode menampilkan shouldUseCursor = false, investigasi menjadi lebih terarah.

Catatan: EXPLAIN memberi gambaran rencana eksekusi, tetapi nilainya jauh lebih berguna bila Anda tahu kondisi logika yang menghasilkan query tersebut. Penamaan boolean membantu menjembatani kode aplikasi dan perilaku database.

Meninjau kapan index hint dipakai

Index hint bisa berguna di beberapa kasus, tetapi juga bisa menjadi beban jika dipasang permanen. Variabel hasIndexHint lebih baik daripada hint karena membedakan dua hal:

  • apakah sistem punya nilai hint, dan
  • apakah query akan benar-benar mengeksekusinya.

Jika ingin lebih eksplisit lagi, Anda bahkan bisa memisahkan menjadi indexHintValue dan shouldApplyIndexHint. Ini berguna ketika ada feature flag atau hanya query tertentu yang boleh memakai hint.

Penerapan di scope ORM, feature flag query, dan repository

Scope ORM

Pada ORM, scope sering menjadi tempat menumpuknya kondisi. Penamaan boolean yang eksplisit membuat scope tetap mudah ditinjau.

function scopeVisibleToReport(query, filters) {
  isActive = filters.status === 'active'
  needsCountQuery = filters.includeTotal === true

  if (isActive) {
    query.where('status', 'active')
  }

  return { query, needsCountQuery }
}

Bahkan bila implementasi ORM Anda berbeda, prinsipnya sama: nama boolean harus menjelaskan keputusan, bukan hanya menyimpan nilai mentah.

Feature flag untuk strategi query

Ketika melakukan migrasi dari offset ke cursor pagination, atau dari query biasa ke hint tertentu, tim sering memakai feature flag. Hindari nama seperti newQuery atau flagA. Pilih nama yang menjelaskan efeknya.

shouldUseCursor = featureFlags.cursorPagination && params.cursor != null
shouldApplyIndexHint = featureFlags.indexHintForOrders && hasIndexHint

Nama seperti ini membuat review kode lebih aman. Reviewer tidak perlu menebak apa konsekuensi teknis dari feature flag tersebut.

Repository atau service layer

Di service layer, boolean sering menjadi kontrak keputusan antarfungsi. Nama yang buruk akan menyebarkan ambiguitas ke banyak tempat. Nama yang baik membatasi salah tafsir sejak awal.

result = orderRepository.findOrders({
  isActive,
  shouldUseCursor,
  needsCountQuery,
  shouldApplyIndexHint
})

Saat dibaca, objek parameter ini sudah cukup menjelaskan perilaku query tanpa perlu membuka implementasi internal terlebih dahulu.

Kapan memakai is, has, should, dan needs

Empat prefiks ini tidak selalu bisa dipertukarkan. Masing-masing punya nuansa yang berguna.

is*

Gunakan untuk menyatakan status atau kondisi yang benar/salah.

  • isActive
  • isArchived
  • isSoftDeleted

Cocok untuk status data, state request, atau mode yang sedang aktif.

has*

Gunakan untuk menyatakan keberadaan sesuatu.

  • hasIndexHint
  • hasCursorValue
  • hasSearchKeyword

Biasanya dipakai saat variabel asli menyimpan nilai lain, lalu dibuat boolean turunannya agar keputusan lebih eksplisit.

should*

Gunakan untuk keputusan perilaku.

  • shouldUseCursor
  • shouldApplyIndexHint
  • shouldJoinPayments

Ini sangat cocok untuk query builder karena banyak logika di sana adalah keputusan eksekusi.

needs*

Gunakan untuk kebutuhan tambahan atau langkah lanjutan.

  • needsCountQuery
  • needsDistinct
  • needsRebuildQuery

Biasanya dipakai saat ada biaya tambahan, seperti query ekstra, transformasi tambahan, atau jalur yang lebih berat.

Kesalahan umum yang perlu dihindari

  • Mencampur nilai mentah dan boolean keputusan. Contoh: cursor dipakai sebagai nilai cursor sekaligus penentu strategi. Pisahkan jadi cursorValue dan shouldUseCursor.
  • Nama terlalu generik. Contoh: flag, mode, count, hint. Nama seperti ini murah saat ditulis, mahal saat di-debug.
  • Negasi ganda. Contoh: isNotDisabled atau shouldNotSkipCount. Ini memperlambat pembacaan dan rawan bug kondisi terbalik.
  • Terlalu dekat ke implementasi, bukan niat. Contoh: useIdClause kurang jelas dibanding shouldUseCursor jika tujuan sebenarnya adalah strategi pagination.
  • Menggunakan boolean untuk terlalu banyak makna. Jika satu flag mengendalikan filter, sort, dan count sekaligus, pecah menjadi beberapa boolean yang spesifik.

Checklist penamaan boolean untuk tim

Gunakan checklist berikut saat menulis atau me-review query builder:

  1. Apakah nama boolean menjawab pertanyaan ya/tidak?
    Jika tidak, kemungkinan namanya masih berupa nilai mentah atau kata benda ambigu.
  2. Apakah prefiksnya sesuai dengan maksud?
    is untuk status, has untuk keberadaan, should untuk keputusan perilaku, needs untuk kebutuhan langkah tambahan.
  3. Apakah nilai mentah dipisahkan dari boolean turunannya?
    Contoh: cursorValue terpisah dari shouldUseCursor.
  4. Apakah nama boolean menjelaskan dampak ke SQL?
    Contoh: shouldJoinInvoices lebih informatif daripada withInvoices jika yang dimaksud adalah keputusan menambah join.
  5. Apakah nama ini membantu membaca log dan EXPLAIN?
    Jika nama tidak membantu menjelaskan kenapa SQL tertentu terbentuk, pertimbangkan perbaikan.
  6. Apakah ada negasi ganda atau istilah yang bisa disalahartikan?
    Utamakan bentuk positif yang ringkas.
  7. Apakah biaya query tambahan terlihat dari namanya?
    Nama seperti needsCountQuery memberi sinyal bahwa ada beban ekstra.

Praktik implementasi yang layak dipertahankan

  • Log keputusan query penting bersama request id dan waktu eksekusi, terutama pada endpoint yang sering lambat.
  • Pisahkan base query dari query turunan untuk data, count, atau export agar perubahan satu jalur tidak bocor ke jalur lain.
  • Review nama boolean sebagai bagian dari review performa, bukan hanya review gaya kode.
  • Gunakan helper kecil bila perlu untuk menghitung keputusan seperti shouldUseCursor atau needsCountQuery, asalkan tidak menyembunyikan logika penting.
  • Uji jalur query yang berbeda: dengan dan tanpa cursor, dengan dan tanpa count, dengan dan tanpa join opsional. Nama boolean yang jelas memudahkan penulisan test case.

Penutup

Penamaan boolean di query builder agar SQL lebih mudah dioptimasi adalah kebiasaan kecil dengan dampak besar. Ia membantu tim memahami keputusan query, membaca review kode lebih cepat, menghubungkan log aplikasi dengan hasil EXPLAIN, dan mencegah optimasi yang salah ketika data tumbuh.

Jika Anda ingin memulai dari perubahan sederhana, audit dulu boolean yang mengendalikan filter, pagination, join opsional, index hint, dan COUNT(*). Ubah nama yang ambigu menjadi bentuk yang eksplisit seperti isActive, hasIndexHint, shouldUseCursor, dan needsCountQuery. Saat bottleneck SQL muncul di produksi, nama-nama ini akan menghemat waktu investigasi lebih dari yang terlihat saat pertama kali ditulis.