Belajar Ruby on Rails - Advanced Active Record
Episode 21 of 27

Belajar Ruby on Rails - Advanced Active Record

Membedah Active Record tingkat lanjut: query optimization dengan select dan group, agregasi, raw SQL yang aman, composite primary keys Rails 8.1, serta praktik reporting query dengan indexing

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Setelah di episode 12 kalian mengoptimasi query dasar, episode 21 membawa Active Record ke tingkat berikutnya: query yang kompleks dan hemat. Di sinilah kalian berpindah dari "mengambil data" menjadi "menyuruh database bekerja" — agregasi, grouping, dan reporting langsung di level database.

Mengapa episode ini penting? Karena aplikasi yang bertumbuh tidak bisa lagi memuat semua baris ke Ruby untuk dihitung. Menghitung 10 ribu komentar di Ruby memakan memori; menghitungnya di database butuh milidetik. Menguasai agregasi AR berarti kalian bisa membangun dashboard, laporan, dan analytics tanpa gem tambahan — dan tahu kapan harus turun ke raw SQL.

select: Mengambil Hanya Kolom yang Dibutuhkan

select membatasi kolom yang diambil — hemat transfer dan memori, dan memungkinkan agregasi:

Rubyselect untuk subset kolom
Post.select(:id, :title)
# SELECT "posts"."id", "posts"."title" FROM "posts"

Kunci yang sering luput: model yang di-select sebagian tidak bisa mengakses kolom lainpost.body pada hasil di atas akan error. Pakai select untuk data ringan (list, dropdown) dan hindari mengakses kolom yang tidak di-select.

Pluck: Array Data Cepat

Untuk mengambil satu kolom sebagai array:

Rubypluck vs map
Post.pluck(:title)            # satu query, array string
Post.all.map(&:title)         # memuat objek penuh dulu — lebih lambat & boros

pluck menghindari instansiasi model — jauh lebih efisien untuk data sederhana.

group & count: Agregasi di Database

Menghitung komentar per post:

Rubycount per group
Comment.group(:post_id).count
# SELECT post_id, COUNT(*) FROM comments GROUP BY post_id
# => { 1 => 3, 2 => 7, ... }

Mengelompokkan dengan kolom dari relasi lain:

RubyGrouping dengan join
Post.joins(:user).group("users.name").count
# SELECT users.name, COUNT(*) FROM posts JOIN users ... GROUP BY users.name

Hasilnya hash nama user => jumlah post. Ini pattern inti reporting: hitung di database, bukan di Ruby.

Agregasi Lain: sum, average, minimum, maximum

RubyAgregasi numerik
Order.sum(:total)
Order.average(:total)
Order.maximum(:total)
Order.minimum(:total)

Semua dieksekusi sebagai satu query SQL — tidak ada data yang dimuat ke memori.

Reporting Query: Praktik Lengkap

Mari bangun laporan "post per bulan per status":

RubyReporting: post per bulan
Post
  .group("strftime('%Y-%m', created_at)")
  .count
# => { "2026-06" => 12, "2026-07" => 19, ... }

Untuk distribusi status:

RubyReporting: count per status
Post.group(:status).count
# => { "draft" => 8, "published" => 34, "archived" => 2 }

Menggabungkan beberapa metrik dalam satu query:

RubyMetrik multi-agregasi
Post.joins(:user)
    .group("users.id")
    .select("users.id, COUNT(posts.id) AS post_count, AVG(posts.views) AS avg_views")
    .order("post_count DESC")

Indexing: Kunci Kecepatan Agregasi

Semua query di atas menjadi lambat tanpa index pada kolom yang di-group/filter. Index membuat database mencari baris tanpa memindai seluruh tabel:

RubyMigration tambah index
class AddIndexesForReporting < ActiveRecord::Migration[8.1]
  def change
    add_index :posts, :status
    add_index :posts, :created_at
  end
end

Aturan praktis indexing:

  • Index kolom yang sering dipakai di where, order, group, join.
  • Composite index untuk filter berpasangan: add_index :comments, [:post_id, :created_at].
  • Index mempercepat baca tapi memperlambat tulis — jangan index semua kolom.
  • Verifikasi dengan EXPLAIN: Post.where(status: :published).explain menampilkan plan query dan penggunaan index.
Output explain
EXPLAIN for: SELECT "posts".* FROM "posts" WHERE "posts"."status" = ?
QUERY PLAN
  Bitmap Heap Scan on posts  (cost=... rows=...)
    Recheck Cond: (status = 'published'::text)

Raw SQL yang Aman

Kadang AR tidak cukup ekspresif. Dua cara turun ke SQL tanpa membuka celah:

Rubyfind_by_sql dengan placeholder
Post.find_by_sql([<<~SQL, "published"])
  SELECT p.id, p.title, COUNT(c.id) AS comments_count
  FROM posts p
  LEFT JOIN comments c ON c.post_id = p.id
  WHERE p.status = ?
  GROUP BY p.id
SQL
Rubysanitize untuk fragmen dinamis
Post.where("posts.title ILIKE ?", "%#{Post.sanitize_sql_like(params[:q])}%")

Aturan emas tetap berlaku: semua input user harus lewat ? placeholder — termasuk di find_by_sql. sanitize_sql_like digunakan saat user input masuk ke pola LIKE (untuk meng-escape %/_).

Composite Primary Keys (Rails 8.1)

Rails 8.1 menambahkan dukungan penuh composite primary keys — tabel dengan primary key multi-kolom, umum di database legacy atau tabel join:

RubyComposite primary key
class JoinRecord < ApplicationRecord
  self.primary_key = %i[user_id group_id]
end

Dengan dukungan ini, AR bisa memakai tabel yang PRIMARY KEY (user_id, group_id) tanpa kolom id sintetik — berguna saat mengintegrasikan schema warisan atau tabel yang didesain tanpa single PK.

Common Pitfalls

  • map(&:attribute) alih-alih pluck — boros memori; gunakan pluck.
  • Mengakses kolom yang tidak di-selectNoMethodError membingungkan; pastikan semua kolom yang dipakai di-select.
  • Agregasi di Ruby untuk data besarcomments.all.count vs comments.count; selalu count di database.
  • group tanpa index — query lambat; index kolom group/filter.
  • Interpolasi raw SQL — celah SQLi; selalu ? placeholder.
  • Index berlebihan — menulis jadi lambat; pilih kolom yang memang sering difilter.

Penutup

Episode 21 membekali kalian Active Record tingkat lanjut: select/pluck untuk data ringan, group/count/sum untuk agregasi di database, reporting query dengan index yang tepat, raw SQL yang aman dengan placeholder, dan composite primary keys Rails 8.1 untuk schema legacy.

Inti yang harus dibawa pulang:

  • select membatasi kolom; pluck menghindari instansiasi model.
  • group(:x).count menghitung di database — bukan di Ruby.
  • Index kolom yang di-filter/group; verifikasi dengan explain.
  • Raw SQL aman: selalu ? placeholder + sanitize_sql_like.
  • Composite PK (Rails 8.1): self.primary_key = %i[...] untuk tabel legacy.

Di episode 22 selanjutnya kita akan membedah background jobs scale & Solid Stack — Solid Queue, Solid Cache, Solid Cable sebagai stack database-driven tanpa Redis, dan produksi dengan Solid Stack. Sampai jumpa di episode 22!

Belajar Ruby on Rails - Advanced Active Record | Belajar Ruby on Rails