PostgreSQL

PostgreSQL #

PostgreSQL adalah database open-source paling kaya fitur yang tersedia saat ini — dan pilihan utama untuk aplikasi Ruby on Rails modern. Berbeda dari MySQL yang mengutamakan kecepatan, PostgreSQL mengutamakan kepatuhan standar SQL, keandalan ACID yang ketat, dan fitur-fitur canggih yang tidak ada di database lain: tipe data JSONB yang bisa di-query dan di-index, array native, UUID sebagai primary key, full-text search bawaan, LISTEN/NOTIFY untuk event-driven architecture, dan COPY untuk bulk import yang sangat cepat. Gem pg adalah driver PostgreSQL untuk Ruby yang matang, cepat, dan mendukung semua fitur PostgreSQL modern. Artikel ini membahas segalanya dari koneksi dasar hingga fitur-fitur unik PostgreSQL yang membuat ia pilihan terbaik untuk aplikasi Ruby.

Instalasi #

# Ubuntu / Debian — install library PostgreSQL
sudo apt install libpq-dev

# macOS dengan Homebrew
brew install libpq
brew link --force libpq  # agar gem pg bisa menemukannya

# CentOS / RHEL / Fedora
sudo dnf install postgresql-devel

# Install gem
gem install pg
# Gemfile
gem 'pg',     '~> 1.5'    # driver PostgreSQL
gem 'sequel', '~> 5.75'   # query builder opsional

Koneksi dan Connection Pool #

require 'pg'

# Koneksi dasar
conn = PG.connect(
  host:     "localhost",
  port:     5432,
  dbname:   "toko_db",
  user:     "appuser",
  password: ENV["PG_PASSWORD"],
  connect_timeout: 10,
  sslmode:  "require"     # "disable", "allow", "prefer", "require", "verify-full"
)

puts "PostgreSQL #{conn.server_version}"
conn.close

# Koneksi via URL (lebih ringkas)
conn = PG.connect(ENV["DATABASE_URL"])
# DATABASE_URL=postgresql://appuser:password@localhost:5432/toko_db

# Dengan blok — auto-close
PG.connect(dbname: "toko_db", user: "appuser", password: ENV["PG_PASSWORD"]) do |conn|
  hasil = conn.exec("SELECT version()")
  puts hasil.first["version"]
end

# Connection Pool dengan connection_pool gem
require 'connection_pool'

POOL = ConnectionPool.new(size: 10, timeout: 5) do
  PG.connect(
    host:     ENV.fetch("PG_HOST", "localhost"),
    dbname:   ENV.fetch("PG_DATABASE", "toko_db"),
    user:     ENV.fetch("PG_USER", "appuser"),
    password: ENV["PG_PASSWORD"]
  )
end

POOL.with { |conn| conn.exec("SELECT 1") }

Query Dasar #

SELECT dengan exec dan exec_params #

require 'pg'

conn = PG.connect(dbname: "toko_db", user: "appuser", password: ENV["PG_PASSWORD"])

# exec — query tanpa parameter (hati-hati SQL injection!)
hasil = conn.exec("SELECT * FROM produk LIMIT 10")

# Iterasi hasil
hasil.each do |baris|
  puts "#{baris['id']}: #{baris['nama']} - Rp #{baris['harga']}"
end

# Akses seperti Hash
puts hasil.first["nama"]     # => "Laptop"
puts hasil.first["harga"]    # => "15000000"  (String! perlu konversi)
puts hasil.first["harga"].to_i  # => 15000000

# exec_params — parameterized query (CARA YANG BENAR)
# PostgreSQL menggunakan $1, $2, ... sebagai placeholder
hasil = conn.exec_params(
  "SELECT * FROM produk WHERE harga < $1 AND aktif = $2",
  [5_000_000, true]
)

hasil.each { |b| puts "#{b['nama']}: #{b['harga']}" }

# Metadata
puts hasil.ntuples   # jumlah baris
puts hasil.nfields   # jumlah kolom
puts hasil.fields.inspect  # ["id", "nama", "harga", "stok", "aktif"]

# Tipe konversi — pg mengembalikan semua nilai sebagai String
# Gunakan type map untuk konversi otomatis
conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn)

hasil = conn.exec_params("SELECT id, harga, aktif FROM produk WHERE id = $1", [1])
baris = hasil.first
puts baris["id"].class    # => Integer (bukan String!)
puts baris["harga"].class # => BigDecimal
puts baris["aktif"].class # => TrueClass / FalseClass

CRUD Lengkap dengan Parameterized Query #

class ProdukPostgresRepository
  def initialize(conn)
    @conn = conn
    # Aktifkan type mapping untuk konversi otomatis
    @conn.type_map_for_results = PG::BasicTypeMapForResults.new(@conn)
    @conn.type_map_for_queries = PG::BasicTypeMapForQueries.new(@conn)
  end

  # CREATE — gunakan RETURNING untuk dapat baris yang baru diinsert
  def buat(nama:, harga:, stok:, kategori_id:, deskripsi: nil)
    hasil = @conn.exec_params(
      <<~SQL,
        INSERT INTO produk (nama, harga, stok, kategori_id, deskripsi, aktif, created_at)
        VALUES ($1, $2, $3, $4, $5, true, NOW())
        RETURNING *
      SQL
      [nama, harga, stok, kategori_id, deskripsi]
    )
    hasil.first
  end

  # READ
  def temukan(id)
    hasil = @conn.exec_params(
      "SELECT p.*, k.nama AS kategori_nama FROM produk p
       LEFT JOIN kategori k ON p.kategori_id = k.id
       WHERE p.id = $1",
      [id]
    )
    hasil.first
  end

  def cari_semua(aktif: true, limit: 50, offset: 0, urut: "created_at DESC")
    # Sanitasi kolom urut — jangan pakai parameter untuk ORDER BY
    kolom_aman = %w[nama harga stok created_at].include?(urut.split.first) ? urut : "created_at DESC"
    hasil = @conn.exec_params(
      "SELECT * FROM produk WHERE aktif = $1 ORDER BY #{kolom_aman} LIMIT $2 OFFSET $3",
      [aktif, limit, offset]
    )
    hasil.to_a
  end

  def cari_teks(kata_kunci)
    # Full-text search PostgreSQL
    hasil = @conn.exec_params(
      <<~SQL,
        SELECT *, ts_rank(to_tsvector('indonesian', nama || ' ' || COALESCE(deskripsi, '')),
                          plainto_tsquery('indonesian', $1)) AS rank
        FROM produk
        WHERE to_tsvector('indonesian', nama || ' ' || COALESCE(deskripsi, ''))
              @@ plainto_tsquery('indonesian', $1)
          AND aktif = true
        ORDER BY rank DESC
        LIMIT 20
      SQL
      [kata_kunci]
    )
    hasil.to_a
  end

  # UPDATE
  def perbarui(id, **atribut)
    return false if atribut.empty?

    kolom_valid = %i[nama harga stok deskripsi aktif]
    atribut = atribut.slice(*kolom_valid)
    return false if atribut.empty?

    set_clause = atribut.keys.each_with_index.map { |k, i| "#{k} = $#{i + 2}" }.join(", ")
    nilai      = [id] + atribut.values

    # Geser index karena $1 untuk id
    set_clause = atribut.keys.each_with_index.map { |k, i| "#{k} = $#{i + 1}" }.join(", ")
    nilai      = atribut.values + [id]

    hasil = @conn.exec_params(
      "UPDATE produk SET #{set_clause}, updated_at = NOW() WHERE id = $#{nilai.length} RETURNING *",
      nilai
    )
    hasil.first
  end

  # DELETE soft
  def nonaktifkan(id)
    hasil = @conn.exec_params(
      "UPDATE produk SET aktif = false, updated_at = NOW() WHERE id = $1 RETURNING id",
      [id]
    )
    hasil.ntuples > 0
  end
end

Transaksi #

# Transaksi dasar
conn.transaction do |c|
  c.exec_params(
    "UPDATE rekening SET saldo = saldo - $1 WHERE id = $2",
    [500_000, 1]
  )
  c.exec_params(
    "UPDATE rekening SET saldo = saldo + $1 WHERE id = $2",
    [500_000, 2]
  )
  # Jika ada exception → ROLLBACK otomatis
  # Jika sukses → COMMIT otomatis
end

# Savepoint — transaksi bertingkat
conn.transaction do |c|
  c.exec("SAVEPOINT sp_awal")

  begin
    c.exec_params("INSERT INTO audit_log (aksi) VALUES ($1)", ["transaksi_dimulai"])
    c.exec("SAVEPOINT sp_operasi")

    # Operasi berisiko
    c.exec_params("UPDATE produk SET stok = stok - $1 WHERE id = $2", [1, produk_id])

    c.exec("RELEASE SAVEPOINT sp_operasi")
  rescue PG::Error => e
    c.exec("ROLLBACK TO SAVEPOINT sp_operasi")
    raise e
  end
end

# Isolation level — penting untuk concurrency
conn.transaction(isolation: :serializable) do |c|
  # Serializable mencegah phantom read, non-repeatable read, dirty read
  # Lebih lambat tapi paling aman untuk operasi finansial
  saldo = c.exec_params("SELECT saldo FROM rekening WHERE id = $1 FOR UPDATE", [1]).first["saldo"].to_i
  raise "Saldo tidak cukup" if saldo < 500_000
  c.exec_params("UPDATE rekening SET saldo = saldo - $1 WHERE id = $2", [500_000, 1])
end

Tipe Data PostgreSQL yang Unik #

PostgreSQL punya tipe data yang tidak ada di database lain — ini salah satu alasan utama ia dipilih untuk aplikasi modern:

JSONB — JSON yang Bisa Di-index dan Di-query #

# Simpan dokumen JSON
conn.exec_params(
  "INSERT INTO produk_detail (produk_id, spesifikasi) VALUES ($1, $2::jsonb)",
  [1, JSON.generate({ prosesor: "Intel i7", ram: "16GB", storage: "512GB SSD" })]
)

# Query di dalam JSONB
hasil = conn.exec(
  "SELECT nama, spesifikasi->>'prosesor' AS prosesor
   FROM produk_detail pd
   JOIN produk p ON p.id = pd.produk_id
   WHERE spesifikasi->>'ram' = '16GB'"
)

# Operator JSONB
# -> mengembalikan JSON, ->> mengembalikan teks
# @> cek apakah berisi (containment)
# ? cek apakah key ada
hasil = conn.exec(
  "SELECT * FROM produk_detail
   WHERE spesifikasi @> '{\"ram\": \"16GB\"}'::jsonb"
)

# ActiveRecord dengan JSONB
class ProdukDetail < ApplicationRecord
  # Kolom spesifikasi adalah jsonb
  store_accessor :spesifikasi, :prosesor, :ram, :storage

  scope :dengan_ram, ->(ram) {
    where("spesifikasi->>'ram' = ?", ram)
  }
end

detail = ProdukDetail.new
detail.prosesor = "Intel i7"
detail.ram      = "16GB"
detail.save!

ProdukDetail.dengan_ram("16GB")

Array — Kolom Array Native #

# Buat tabel dengan kolom array
conn.exec(
  "CREATE TABLE IF NOT EXISTS artikel (
    id      SERIAL PRIMARY KEY,
    judul   TEXT NOT NULL,
    tag     TEXT[],          -- array teks
    skor    INTEGER[]        -- array integer
  )"
)

# Insert dengan array
conn.exec_params(
  "INSERT INTO artikel (judul, tag) VALUES ($1, $2)",
  ["Ruby Tips", ["ruby", "pemrograman", "tips"]]
)

# Query dengan array operator
# ANY — cek apakah nilai ada dalam array
hasil = conn.exec(
  "SELECT * FROM artikel WHERE 'ruby' = ANY(tag)"
)

# @> — array containment
hasil = conn.exec(
  "SELECT * FROM artikel WHERE tag @> ARRAY['ruby', 'tips']"
)

# ActiveRecord dengan array
class Artikel < ApplicationRecord
  # tag adalah TEXT[] di PostgreSQL
  def self.dengan_tag(tag)
    where("? = ANY(tag)", tag)
  end

  def tambah_tag(tag_baru)
    update(tag: (tag + [tag_baru]).uniq)
  end
end

artikel = Artikel.create!(judul: "Belajar Ruby", tag: ["ruby", "dasar"])
Artikel.dengan_tag("ruby")

UUID — Primary Key yang Lebih Aman #

# Aktifkan extension pgcrypto atau uuid-ossp
conn.exec("CREATE EXTENSION IF NOT EXISTS pgcrypto")

# Buat tabel dengan UUID primary key
conn.exec(
  "CREATE TABLE IF NOT EXISTS sesi (
    id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id    INTEGER NOT NULL,
    token      TEXT NOT NULL,
    expired_at TIMESTAMPTZ NOT NULL
  )"
)

# Insert — id diisi otomatis
hasil = conn.exec_params(
  "INSERT INTO sesi (user_id, token, expired_at)
   VALUES ($1, $2, NOW() + INTERVAL '7 days')
   RETURNING id",
  [1, SecureRandom.hex(32)]
)
puts hasil.first["id"]  # => "550e8400-e29b-41d4-a716-446655440000"

# ActiveRecord dengan UUID
class Sesi < ApplicationRecord
  # config/initializers/generators.rb:
  # config.generators { |g| g.orm :active_record, primary_key_type: :uuid }

  # atau di model:
  self.primary_key = :id   # tipe uuid di migrasi
end

hstore — Key-Value Store #

# Aktifkan extension hstore
conn.exec("CREATE EXTENSION IF NOT EXISTS hstore")

# Buat tabel dengan kolom hstore
conn.exec(
  "CREATE TABLE IF NOT EXISTS konfigurasi (
    id      SERIAL PRIMARY KEY,
    nama    TEXT NOT NULL,
    setting hstore
  )"
)

# Insert hstore
conn.exec_params(
  "INSERT INTO konfigurasi (nama, setting) VALUES ($1, $2::hstore)",
  ["email", "host => smtp.gmail.com, port => 587, tls => true"]
)

# Query hstore
conn.exec("SELECT setting->'host' AS smtp_host FROM konfigurasi WHERE nama = 'email'")

# ActiveRecord dengan hstore
class Konfigurasi < ApplicationRecord
  store_accessor :setting, :host, :port, :tls
end

COPY — Bulk Import/Export Ultra-Cepat #

COPY adalah cara tercepat untuk memasukkan atau mengekspor data dalam jumlah besar:

# COPY FROM — import CSV ke PostgreSQL
conn.copy_data("COPY produk (nama, harga, stok, kategori_id) FROM STDIN WITH CSV") do
  File.foreach("produk.csv") do |baris|
    conn.put_copy_data(baris)
  end
end
puts "Import selesai"

# COPY TO — export data ke CSV
File.open("export_produk.csv", "w") do |f|
  conn.copy_data("COPY (SELECT nama, harga, stok FROM produk WHERE aktif = true) TO STDOUT WITH CSV HEADER") do
    while (data = conn.get_copy_data)
      f.write(data)
    end
  end
end

# Benchmark perbandingan
# INSERT satu per satu: 10.000 baris ≈ 10 detik
# INSERT batch:         10.000 baris ≈ 1 detik
# COPY:                 10.000 baris ≈ 0.1 detik (100x lebih cepat dari INSERT biasa!)

LISTEN/NOTIFY — Real-time Event #

PostgreSQL punya mekanisme pub/sub bawaan yang bisa digunakan untuk notifikasi real-time:

require 'pg'

# PUBLISHER — kirim notifikasi dari satu koneksi
def kirim_notifikasi(conn, channel, payload = nil)
  if payload
    conn.exec_params("NOTIFY #{conn.escape_identifier(channel)}, $1", [payload])
  else
    conn.exec("NOTIFY #{conn.escape_identifier(channel)}")
  end
end

# Database trigger untuk auto-notify saat data berubah
conn.exec(<<~SQL)
  CREATE OR REPLACE FUNCTION notify_produk_berubah()
  RETURNS TRIGGER AS $$
  BEGIN
    PERFORM pg_notify('produk_berubah',
      json_build_object(
        'aksi',     TG_OP,
        'id',       NEW.id,
        'nama',     NEW.nama,
        'updated',  NEW.updated_at
      )::text
    );
    RETURN NEW;
  END;
  $$ LANGUAGE plpgsql;

  DROP TRIGGER IF EXISTS trg_produk_berubah ON produk;
  CREATE TRIGGER trg_produk_berubah
    AFTER INSERT OR UPDATE ON produk
    FOR EACH ROW EXECUTE FUNCTION notify_produk_berubah();
SQL

# SUBSCRIBER — dengarkan notifikasi di thread terpisah
Thread.new do
  listener = PG.connect(dbname: "toko_db", user: "appuser", password: ENV["PG_PASSWORD"])
  listener.exec("LISTEN produk_berubah")
  puts "Mendengarkan perubahan produk..."

  loop do
    listener.wait_for_notify(10) do |channel, pid, payload|
      data = JSON.parse(payload)
      puts "Produk berubah: #{data['aksi']} - #{data['nama']} (ID: #{data['id']})"
      # Invalidasi cache, broadcast ke WebSocket, dst
    end
  end
ensure
  listener.exec("UNLISTEN produk_berubah")
  listener.close
end

# Test — update produk dan lihat notifikasi muncul
sleep 1
conn.exec_params("UPDATE produk SET harga = harga + 1000 WHERE id = $1", [1])
conn.exec("COMMIT")
sleep 2

ActiveRecord dengan PostgreSQL di Rails #

# config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool:     <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV["PG_USER"] || "postgres" %>
  password: <%= ENV["PG_PASSWORD"] %>
  host:     <%= ENV["PG_HOST"] || "localhost" %>
  port:     <%= ENV["PG_PORT"] || 5432 %>
  timeout:  5000
  # Opsi koneksi PostgreSQL
  variables:
    statement_timeout: 10000   # timeout query 10 detik
    lock_timeout:      5000    # timeout lock 5 detik

development:
  <<: *default
  database: toko_development

test:
  <<: *default
  database: toko_test

production:
  <<: *default
  url:  <%= ENV["DATABASE_URL"] %>
# Migrasi dengan fitur PostgreSQL
class BuatTabelProduk < ActiveRecord::Migration[7.1]
  def change
    enable_extension "pgcrypto"   # untuk gen_random_uuid()
    enable_extension "unaccent"   # untuk full-text search tanpa aksen

    create_table :produk, id: :uuid, default: "gen_random_uuid()" do |t|
      t.string      :nama,        null: false, limit: 200
      t.text        :deskripsi
      t.decimal     :harga,       null: false, precision: 15, scale: 2
      t.integer     :stok,        null: false, default: 0
      t.boolean     :aktif,       null: false, default: true
      t.references  :kategori,    null: false, foreign_key: true, type: :uuid
      t.text        :tag,         array: true, default: []   # Array
      t.jsonb       :metadata,    default: {}               # JSONB
      t.tsvector    :search_vector                          # Full-text search

      t.timestamps
    end

    # Standard indexes
    add_index :produk, :aktif
    add_index :produk, :harga
    add_index :produk, [:kategori_id, :aktif]

    # GIN index untuk array dan JSONB — jauh lebih cepat dari B-tree untuk ini
    add_index :produk, :tag,      using: :gin
    add_index :produk, :metadata, using: :gin

    # GiST atau GIN untuk full-text search
    add_index :produk, :search_vector, using: :gin

    # Partial index — hanya index baris yang aktif
    add_index :produk, :harga, where: "aktif = true", name: "idx_produk_harga_aktif"
  end
end
# Model dengan fitur PostgreSQL
class Produk < ApplicationRecord
  belongs_to :kategori

  # Scope untuk array
  scope :dengan_tag, ->(tag) { where("? = ANY(tag)", tag) }
  scope :dengan_semua_tag, ->(tags) { where("tag @> ARRAY[?]::text[]", tags) }

  # Scope untuk JSONB
  scope :dengan_warna, ->(warna) {
    where("metadata @> ?", { warna: warna }.to_json)
  }

  # Full-text search
  scope :cari, ->(kata) {
    where(
      "to_tsvector('indonesian', nama || ' ' || COALESCE(deskripsi, '')) @@ plainto_tsquery('indonesian', ?)",
      kata
    )
    .order(
      Arel.sql("ts_rank(to_tsvector('indonesian', nama || ' ' || COALESCE(deskripsi, '')), plainto_tsquery('indonesian', #{connection.quote(kata)})) DESC")
    )
  }

  before_save :perbarui_search_vector

  private

  def perbarui_search_vector
    self.search_vector = Produk.connection.execute(
      "SELECT to_tsvector('indonesian', #{Produk.connection.quote(nama + ' ' + deskripsi.to_s)})"
    ).first["to_tsvector"]
  end
end

# Penggunaan
Produk.dengan_tag("laptop")
Produk.dengan_semua_tag(["laptop", "gaming"])
Produk.dengan_warna("hitam")
Produk.cari("laptop gaming ringan")

EXPLAIN ANALYZE — Debug Query Lambat #

# Analisis performa query
def explain(conn, query, *params)
  explain_sql = "EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{query}"
  hasil = conn.exec_params(explain_sql, params)
  hasil.each { |b| puts b["QUERY PLAN"] }
end

explain(conn,
  "SELECT * FROM produk WHERE kategori_id = $1 AND aktif = true ORDER BY harga",
  2
)

# Output contoh:
# Sort  (cost=156.23..162.34 rows=2445 width=...) (actual time=2.345..2.567 rows=234)
#   Sort Key: harga
#   Sort Method: quicksort  Memory: 45kB
#   ->  Bitmap Heap Scan on produk  (cost=48.23..124.56 rows=2445 width=...)
#         Recheck Cond: (kategori_id = 2)
#         Filter: aktif
#         Heap Blocks: exact=89
#         ->  Bitmap Index Scan on idx_produk_kategori  (cost=0.00..47.62)
#               Index Cond: (kategori_id = 2)
# Planning Time: 0.456 ms
# Execution Time: 3.123 ms

# Di Rails — gunakan bullet gem untuk N+1, dan pg_query gem untuk parsing EXPLAIN

Sequel dengan PostgreSQL #

require 'sequel'

DB = Sequel.connect(
  adapter:  "postgres",
  host:     "localhost",
  database: "toko_db",
  user:     "appuser",
  password: ENV["PG_PASSWORD"],
  max_connections: 10
)

# Query standar
DB[:produk].all
DB[:produk].where(aktif: true).order(Sequel.desc(:created_at)).limit(10).all

# PostgreSQL-specific dengan Sequel
# JSONB
DB[:produk_detail].where(
  Sequel.pg_json_op(:metadata).get_text("warna") => "hitam"
).all

# Array containment
DB[:produk].where(
  Sequel.pg_array(:tag).contains(["ruby", "programming"])
).all

# Full-text search
DB[:produk].where(
  Sequel.lit("to_tsvector('indonesian', nama) @@ plainto_tsquery('indonesian', ?)", "laptop")
).all

# UPSERT (INSERT ON CONFLICT)
DB[:produk].insert_conflict(
  target: :sku,
  update: { harga: Sequel[:excluded][:harga], updated_at: Sequel::CURRENT_TIMESTAMP }
).insert(sku: "SKU001", nama: "Laptop Pro", harga: 15_000_000)

# Transaksi
DB.transaction(isolation: :serializable) do
  DB[:rekening].where(id: 1).update(saldo: Sequel[:saldo] - 500_000)
  DB[:rekening].where(id: 2).update(saldo: Sequel[:saldo] + 500_000)
end

Tipe Data PostgreSQL ↔ Ruby #

PostgreSQL          Ruby (pg gem + type_map)    Keterangan
────────────────────────────────────────────────────────────────
INTEGER / BIGINT    Integer                     Bilangan bulat
SMALLINT            Integer
NUMERIC / DECIMAL   BigDecimal                  Presisi tinggi
REAL / FLOAT8       Float                       Floating point
BOOLEAN             true / false                Boolean native
TEXT / VARCHAR      String
CHAR(n)             String                      Fixed-length
BYTEA               String (encoding: BINARY)   Data biner
UUID                String                      UUID string
DATE                Date                        Hanya tanggal
TIMESTAMP           Time                        Waktu lokal
TIMESTAMPTZ         Time (dengan offset)        Waktu dengan TZ
INTERVAL            String / PG::Interval       Durasi waktu
JSON                String                      JSON mentah
JSONB               String → parse ke Hash      JSONB terindeks
TEXT[]              Array of String             Array native
INTEGER[]           Array of Integer
HSTORE              Hash                        Key-value store
TSVECTOR            String                      Full-text vector
POINT / LINE        PG::Point, etc.             Tipe geometri
INET / CIDR         String                      Alamat IP / network

Ringkasan #

  • exec_params bukan exec — selalu gunakan exec_params dengan placeholder $1, $2, ... untuk input apapun yang tidak kamu kontrol; ini mencegah SQL injection dan lebih efisien karena PostgreSQL bisa me-cache execution plan.
  • RETURNING * untuk data setelah INSERT/UPDATE — PostgreSQL mendukung RETURNING yang mengembalikan baris yang baru dimodifikasi; jauh lebih bersih dari SELECT terpisah setelah INSERT.
  • Type mapping untuk konversi otomatisconn.type_map_for_results = PG::BasicTypeMapForResults.new(conn) agar integer, boolean, dan float tidak perlu dikonversi manual dari String.
  • JSONB untuk data semi-terstruktur — simpan atribut yang bervariasi per record sebagai JSONB; bisa di-index dengan GIN dan di-query dengan operator ->, ->>, @>.
  • Array native lebih baik dari tabel junction untuk data sederhanatag TEXT[] lebih cepat untuk array kecil yang tidak perlu relasi kompleks.
  • COPY untuk bulk import — 100x lebih cepat dari INSERT satu per satu untuk ribuan baris; gunakan untuk seed data, migrasi, atau import CSV.
  • LISTEN/NOTIFY untuk event real-time — kombinasikan dengan trigger database untuk invalidasi cache atau broadcast ke WebSocket tanpa perlu Redis Pub/Sub untuk kasus sederhana.
  • GIN index untuk JSONB, array, dan full-text search — B-tree tidak efektif untuk tipe data ini; gunakan add_index :tabel, :kolom, using: :gin.
  • Partial index untuk kolom yang sering difilterWHERE aktif = true pada index mengurangi ukuran index secara drastis jika sebagian besar data difilter.
  • EXPLAIN ANALYZE sebelum deploy — analisis execution plan untuk query yang kamu tulis; cari Seq Scan pada tabel besar sebagai sinyal butuh index.

← Sebelumnya: Oracle   Berikutnya: MongoDB →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact