PostgreSQL adapter를 사용하기 위해서는 최소 9.3 버전이 설치되어 있어야 합니다. 이전 버전은 지원되지 않습니다.
PostgreSQL을 시작하려면 Rails 설정 가이드를 참고하세요. PostgreSQL용 Active Record를 적절하게 설정하는 방법이 설명되어 있습니다.
1 데이터타입
PostgreSQL은 다양한 특정 데이터타입을 제공합니다. 다음은 PostgreSQL adapter에서 지원하는 타입 목록입니다.
1.1 Bytea
# db/migrate/20140207133952_create_documents.rb
create_table :documents do |t|
t.binary "payload"
end
# app/models/document.rb
class Document < ApplicationRecord
end
# 사용법
data = File.read(Rails.root + "tmp/output.pdf")
Document.create payload: data
1.2 Array
# db/migrate/20140207133952_create_books.rb
create_table :books do |t|
t.string "title"
t.string "tags", array: true
t.integer "ratings", array: true
end
add_index :books, :tags, using: "gin"
add_index :books, :ratings, using: "gin"
# app/models/book.rb
class Book < ApplicationRecord
end
# 사용법
Book.create title: "Brave New World",
tags: ["fantasy", "fiction"],
ratings: [4, 5]
## 단일 tag에 대한 Books
Book.where("'fantasy' = ANY (tags)")
## 다중 tags에 대한 Books
Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"])
## 3개 이상의 ratings를 가진 Books
Book.where("array_length(ratings, 1) >= 3")
1.3 Hstore
참고: hstore를 사용하려면 hstore
extension을 활성화해야 합니다.
# db/migrate/20131009135255_create_profiles.rb
class CreateProfiles < ActiveRecord::Migration[8.1]
enable_extension "hstore"가 extension_enabled?("hstore")가 아닌 경우에만 실행
create_table :profiles do |t|
t.hstore "settings"
end
end
# app/models/profile.rb
class Profile < ApplicationRecord
end
irb> Profile.create(settings: { "color" => "blue", "resolution" => "800x600" })
irb> profile = Profile.first
irb> profile.settings
=> {"color"=>"blue", "resolution"=>"800x600"}
irb> profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
irb> profile.save!
irb> Profile.where("settings->'color' = ?", "yellow")
=> #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]>
1.4 JSON과 JSONB
# db/migrate/20131220144913_create_events.rb
# ... json datatype을 위해:
create_table :events do |t|
t.json "payload"
end
# ... 또는 jsonb datatype을 위해:
create_table :events do |t|
t.jsonb "payload"
end
# app/models/event.rb
class Event < ApplicationRecord
end
irb> Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]})
irb> event = Event.first
irb> event.payload
=> {"kind"=>"user_renamed", "change"=>["jack", "john"]}
## JSON 문서 기반 쿼리
# -> 연산자는 원래 JSON 타입(객체일 수 있음)을 반환하는 반면, ->>는 텍스트를 반환합니다
irb> Event.where("payload->>'kind' = ?", "user_renamed")
1.5 Range Types
이 타입은 Ruby Range
객체에 매핑됩니다.
# db/migrate/20130923065404_create_events.rb
create_table :events do |t|
t.daterange "duration" # 기간
end
# app/models/event.rb
class Event < ApplicationRecord
end
irb> Event.create(duration: Date.new(2014, 2, 11)..Date.new(2014, 2, 12))
irb> event = Event.first
irb> event.duration
=> 2014년 2월 11일 화요일...2014년 2월 13일 목요일
## 특정 날짜의 모든 Event
irb> Event.where("duration @> ?::date", Date.new(2014, 2, 12))
## range 경계값 다루기
irb> event = Event.select("lower(duration) AS starts_at").select("upper(duration) AS ends_at").first
irb> event.starts_at
=> 2014년 2월 11일 화요일
irb> event.ends_at
=> 2014년 2월 13일 목요일
1.6 Composite Types
현재 composite type에 대한 특별한 지원은 없습니다. 이들은 일반 text 컬럼으로 매핑됩니다:
CREATE TYPE full_address AS
(
city VARCHAR(90),
street VARCHAR(90)
);
# db/migrate/20140207133952_create_contacts.rb
execute <<-SQL
CREATE TYPE full_address AS
(
city VARCHAR(90),
street VARCHAR(90)
);
SQL
create_table :contacts do |t|
t.column :address, :full_address
end
# app/models/contact.rb
class Contact < ApplicationRecord
end
irb> Contact.create address: "(Paris,Champs-Élysées)"
irb> contact = Contact.first
irb> contact.address
=> "(Paris,Champs-Élysées)"
irb> contact.address = "(Paris,Rue Basse)"
irb> contact.save!
1.7 Enumerated Types
이 type은 일반 text 컬럼으로 매핑하거나 ActiveRecord::Enum
으로 매핑할 수 있습니다.
# db/migrate/20131220144913_create_articles.rb
def change
create_enum :article_status, ["draft", "published", "archived"]
create_table :articles do |t|
t.enum :status, enum_type: :article_status, default: "draft", null: false
end
end
또한 enum 타입을 생성하고 기존 테이블에 enum 컬럼을 추가할 수 있습니다:
# db/migrate/20230113024409_add_status_to_articles.rb
def change
create_enum :article_status, ["draft", "published", "archived"]
add_column :articles, :status, :enum, enum_type: :article_status, default: "draft", null: false
end
위의 migration들은 모두 되돌릴 수 있지만, 필요한 경우 별도의 #up
과 #down
메서드를 정의할 수 있습니다. enum 타입을 삭제하기 전에 해당 enum 타입에 의존하는 모든 컬럼이나 테이블을 제거했는지 확인하세요:
def down
drop_table :articles
# 또는: remove_column :articles, :status
drop_enum :article_status
end
모델에서 enum 속성을 선언하면 헬퍼 메서드가 추가되고 클래스의 인스턴스에 유효하지 않은 값이 할당되는 것을 방지합니다:
# app/models/article.rb
class Article < ApplicationRecord
enum :status, {
draft: "draft", published: "published", archived: "archived"
}, prefix: true
end
irb> article = Article.create
irb> article.status
=> "draft" # migration에 정의된 대로 PostgreSQL의 기본 status입니다
irb> article.status_published!
irb> article.status
=> "published"
irb> article.status_archived?
=> false
irb> article.status = "deleted"
ArgumentError: 'deleted'는 유효한 status가 아닙니다
enum의 이름을 변경하려면 모델 사용을 업데이트하면서 rename_enum
을 사용할 수 있습니다:
# db/migrate/20150718144917_rename_article_status.rb
def change
rename_enum :article_status, :article_state
end
새로운 값을 추가하려면 add_enum_value
를 사용할 수 있습니다:
# db/migrate/20150720144913_add_new_state_to_articles.rb
def up
add_enum_value :article_state, "archived" # published 뒤에 추가됨
add_enum_value :article_state, "in review", before: "published"
add_enum_value :article_state, "approved", after: "in review"
add_enum_value :article_state, "rejected", if_not_exists: true # 값이 이미 존재하는 경우 에러가 발생하지 않음
end
Enum value들은 삭제할 수 없으며, 이는 add_enum_value가 되돌릴 수 없다는 것을 의미합니다. 그 이유는 여기에서 확인할 수 있습니다.
값의 이름을 변경하려면 rename_enum_value
를 사용할 수 있습니다:
# db/migrate/20150722144915_rename_article_state.rb
def change
rename_enum_value :article_state, from: "archived", to: "deleted"
end
Hint: 사용 중인 모든 enum의 값들을 보기 위해서는, bin/rails db
나 psql
콘솔에서 다음 쿼리를 실행하면 됩니다:
SELECT n.nspname AS enum_schema,
t.typname AS enum_name,
e.enumlabel AS enum_value
FROM pg_type t
JOIN pg_enum e ON t.oid = e.enumtypid
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
1.8 UUID
PostgreSQL 13.0 이전 버전을 사용하는 경우 UUID를 사용하기 위해 특별한 extension을 활성화해야 할 수 있습니다. pgcrypto
extension(PostgreSQL >= 9.4) 또는 uuid-ossp
extension(더 이전 버전의 경우)을 활성화하세요.
# db/migrate/20131220144913_create_revisions.rb
create_table :revisions do |t|
t.uuid :identifier
end
# app/models/revision.rb
class Revision < ApplicationRecord
end
irb> Revision.create identifier: "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"
irb> revision = Revision.first
irb> revision.identifier
=> "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
migration에서 참조를 정의할 때 uuid
타입을 사용할 수 있습니다:
# db/migrate/20150418012400_create_blog.rb
extension_enabled?("pgcrypto")가 없는 경우 enable_extension "pgcrypto" 실행
create_table :posts, id: :uuid
create_table :comments, id: :uuid do |t|
# t.belongs_to :post, type: :uuid
t.references :post, type: :uuid
end
# app/models/post.rb
class Post < ApplicationRecord
has_many :comments
end
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
end
UUID를 primary key로 사용하는 것에 대한 자세한 내용은 이 섹션을 참조하세요.
1.9 Bit String Types
# db/migrate/20131220144913_create_users.rb
create_table :users, force: true do |t|
t.column :settings, "bit(8)"
end
# app/models/user.rb
class User < ApplicationRecord
end
irb> User.create settings: "01010011"
irb> user = User.first
irb> user.settings
=> "01010011"
irb> user.settings = "0xAF"
irb> user.settings
=> "10101111"
irb> user.save!
1.10 Network Address Types
inet
과 cidr
타입은 Ruby IPAddr
객체로 매핑됩니다. macaddr
타입은 일반 텍스트로 매핑됩니다.
# db/migrate/20140508144913_create_devices.rb
create_table(:devices, force: true) do |t|
t.inet "ip"
t.cidr "network"
t.macaddr "address"
end
# app/models/device.rb
class Device < ApplicationRecord
end
irb> macbook = Device.create(ip: "192.168.1.12", network: "192.168.2.0/24", address: "32:01:16:6d:05:ef")
irb> macbook.ip
=> #<IPAddr: IPv4:192.168.1.12/255.255.255.255>
irb> macbook.network
=> #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
irb> macbook.address
=> "32:01:16:6d:05:ef"
1.11 Geometric Types
points
를 제외한 모든 geometric type은 일반 text로 매핑됩니다.
point는 x
와 y
좌표를 포함하는 배열로 변환됩니다.
1.12 Interval
이 타입은 ActiveSupport::Duration
객체로 매핑됩니다.
# db/migrate/20200120000000_create_events.rb
create_table :events do |t|
t.interval "duration"
end
# app/models/event.rb
class Event < ApplicationRecord
end
irb> Event.create(duration: 2.days)
irb> event = Event.first
irb> event.duration
=> 2일
2 UUID Primary Keys
랜덤 UUID를 생성하기 위해서는 pgcrypto
(PostgreSQL >= 9.4) 또는 uuid-ossp
extension을 활성화해야 합니다.
# db/migrate/20131220144913_create_devices.rb
"pgcrypto" extension이 활성화되어 있지 않은 경우 활성화
enable_extension "pgcrypto" unless extension_enabled?("pgcrypto")
create_table :devices, id: :uuid do |t|
t.string :kind
end
# app/models/device.rb
class Device < ApplicationRecord
end
irb> device = Device.create
irb> device.id
=> "814865cd-5a1d-4771-9306-4268f188fe9e"
create_table
에 :default
옵션이 전달되지 않은 경우 gen_random_uuid()
(pgcrypto
에서 제공)가 사용됩니다.
UUID를 primary key로 사용하는 테이블을 위한 Rails model generator를 사용하려면, model generator에 --primary-key-type=uuid
를 전달하세요.
예시:
$ rails generate model Device --primary-key-type=uuid kind:string
UUID를 참조하는 foreign key로 모델을 만들 때는 uuid
를 네이티브 필드 타입으로 취급하세요. 예를 들어:
$ rails generate model Case device_id:uuid
3 Indexing
PostgreSQL은 다양한 index 옵션을 포함하고 있습니다. 다음 옵션들은 일반적인 index 옵션에 더해 PostgreSQL adapter에서 지원됩니다.
3.1 Include
새로운 index를 생성할 때, non-key 컬럼들을 :include
옵션으로 포함시킬 수 있습니다. 이러한 key들은 검색을 위한 index scan에서는 사용되지 않지만, 연관된 테이블을 방문하지 않고도 index only scan 중에 읽을 수 있습니다.
# db/migrate/20131220144913_add_index_users_on_email_include_id.rb
add_index :users, :email, include: :id
여러 개의 컬럼이 지원됩니다:
# db/migrate/20131220144913_add_index_users_on_email_include_id_and_created_at.rb
add_index :users, :email, include: [:id, :created_at]
4 Generated Columns
Generated column은 PostgreSQL 버전 12.0부터 지원됩니다.
# db/migrate/20131220144913_create_users.rb
create_table :users do |t|
t.string :name
t.virtual :name_upcased, type: :string, as: "upper(name)", stored: true
end
# app/models/user.rb
class User < ApplicationRecord
end
# 사용 예시
user = User.create(name: "John")
User.last.name_upcased # => "JOHN"
5 Deferrable Foreign Keys
기본적으로 PostgreSQL의 테이블 제약조건은 각 문장 실행 직후에 바로 체크됩니다. 이는 참조된 레코드가 참조 테이블에 아직 없는 레코드를 생성하는 것을 의도적으로 허용하지 않습니다. 하지만 foreign key 정의에 DEFERRABLE
을 추가하면 트랜잭션이 커밋될 때까지 이 무결성 검사를 나중으로 미룰 수 있습니다. 기본적으로 모든 검사를 연기하려면 DEFERRABLE INITIALLY DEFERRED
로 설정할 수 있습니다. Rails는 add_reference
와 add_foreign_key
메서드의 foreign_key
옵션에 :deferrable
키를 추가함으로써 이 PostgreSQL 기능을 제공합니다.
이의 한 예시로, foreign key를 생성했더라도 트랜잭션 내에서 순환 의존성을 생성할 수 있습니다:
add_reference :person, :alias, foreign_key: { deferrable: :deferred }
add_reference :alias, :person, foreign_key: { deferrable: :deferred }
두 테이블이 서로를 참조하면서 foreign key가 있는 경우, 위와 같이 deferrable: :deferred
옵션을 사용하여 순환 의존성 문제를 해결할 수 있습니다.
foreign_key: true
옵션으로 참조가 생성된 경우, 첫 번째 INSERT
문을 실행할 때 다음 트랜잭션이 실패하게 됩니다. 하지만 deferrable: :deferred
옵션이 설정되어 있으면 실패하지 않습니다.
ActiveRecord::Base.lease_connection.transaction do
person = Person.create(id: SecureRandom.uuid, alias_id: SecureRandom.uuid, name: "John Doe")
Alias.create(id: person.alias_id, person_id: person.id, name: "jaydee")
end
:deferrable
옵션을 :immediate
로 설정하면, foreign key가 제약조건을 즉시 체크하는 기본 동작을 유지하면서도 트랜잭션 내에서 set_constraints
를 사용하여 수동으로 체크를 연기할 수 있습니다. 이는 트랜잭션이 커밋될 때 foreign key를 체크하게 됩니다:
ActiveRecord::Base.lease_connection.transaction do
ActiveRecord::Base.lease_connection.set_constraints(:deferred)
person = Person.create(alias_id: SecureRandom.uuid, name: "John Doe")
Alias.create(id: person.alias_id, person_id: person.id, name: "jaydee")
end
기본적으로 :deferrable
은 false
이며 제약 조건은 항상 즉시 확인됩니다.
6 Unique Constraint
# db/migrate/20230422225213_create_items.rb
create_table :items do |t|
t.integer :position, null: false
t.unique_constraint [:position], deferrable: :immediate
end
기존의 unique index를 deferrable로 변경하려면, :using_index
를 사용하여 deferrable unique 제약 조건을 생성할 수 있습니다.
add_unique_constraint :items, deferrable: :deferred, using_index: "index_items_on_position"
position에 대한 items 테이블의 deferrable unique constraint를 기존 인덱스를 사용하여 추가합니다.
Foreign key처럼 unique constraint도 :deferrable
을 :immediate
또는 :deferred
로 설정하여 지연시킬 수 있습니다. 기본적으로 :deferrable
은 false
이며 constraint는 항상 즉시 확인됩니다.
7 Exclusion Constraints
# db/migrate/20131220144913_create_products.rb
create_table :products do |t|
t.integer :price, null: false
t.daterange :availability_range, null: false
t.exclusion_constraint "price WITH =, availability_range WITH &&", using: :gist, name: "price_check"
end
외래 키와 마찬가지로 exclusion constraints는 :deferrable
을 :immediate
또는 :deferred
로 설정하여 지연시킬 수 있습니다. 기본적으로 :deferrable
은 false
이며 제약 조건은 항상 즉시 확인됩니다.
8 Full Text Search
# db/migrate/20131220144913_create_documents.rb
create_table :documents do |t|
t.string :title
t.string :body
end
add_index :documents, "to_tsvector('english', title || ' ' || body)", using: :gin, name: "documents_idx"
# app/models/document.rb
class Document < ApplicationRecord
end
# 사용방법
Document.create(title: "Cats and Dogs", body: "are nice!")
## 'cat & dog'와 일치하는 모든 document
Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)",
"cat & dog")
Optionally, PostgreSQL 12.0부터는 자동으로 생성된 컬럼으로 vector를 저장할 수 있습니다:
다음 코드는 새롭게 생성되는 documents
테이블에 full-text search를 설정하는 migration입니다. 이를 통해 제목과 본문에서 검색할 수 있습니다.
테이블 생성과 동시에 title
과 body
컬럼을 포함하여 textsearchable_index_col
이라는 이름의 virtual 컬럼을 생성합니다. 이 컬럼은 영어 형태의 tsvector
타입으로, 제목과 본문을 공백으로 연결한 텍스트를 저장합니다.
그런 다음 검색 성능을 높이기 위해 gin
인덱스를 추가합니다.
사용 예시로, 제목이 "Cats and Dogs"이고 내용이 "are nice!"인 문서를 생성하고, "cat & dog" 키워드로 검색하는 예시를 보여줍니다.
9 데이터베이스 Views
레거시 데이터베이스에 다음과 같은 테이블이 포함되어 있는 상황을 가정해봅시다:
rails_pg_guide=# \d "TBL_ART"
Table "public.TBL_ART"
컬럼 | 타입 | 제약 조건
------------+-----------------------------+------------------------------------------------------------
INT_ID | integer | not null default nextval('"TBL_ART_INT_ID_seq"'::regclass)
STR_TITLE | character varying |
STR_STAT | character varying | default 'draft'::character varying
DT_PUBL_AT | timestamp without time zone |
BL_ARCH | boolean | default false
인덱스:
"TBL_ART_pkey" PRIMARY KEY, btree ("INT_ID")
이 테이블은 Rails 규칙을 전혀 따르지 않습니다. 단순 PostgreSQL view는 기본적으로 업데이트가 가능하기 때문에, 다음과 같이 래핑할 수 있습니다:
db/migrate/20131220144913_create_articles_view.rb
execute <<-SQL CREATE VIEW articles AS SELECT "INT_ID" AS id, "STR_TITLE" AS title, "STR_STAT" AS status, "DT_PUBL_AT" AS published_at, "BL_ARCH" AS archived FROM "TBL_ART" WHERE "BL_ARCH" = 'f' SQL
# app/models/article.rb
class Article < ApplicationRecord
self.primary_key = "id"
def archive!
update_attribute :archived, true
end
end
irb> first = Article.create! title: "Winter is coming", status: "published", published_at: 1.year.ago
irb> second = Article.create! title: "Brace yourself", status: "draft", published_at: 1.month.ago
irb> Article.count
=> 2
irb> first.archive!
irb> Article.count
=> 1
이 애플리케이션은 보관되지 않은 Articles
만 다룹니다. view는 보관된 Articles
를 직접 제외할 수 있도록 조건을 허용합니다.
10 Structure Dumps
만약 config.active_record.schema_format
이 :sql
이라면, Rails는 structure dump를 생성하기 위해 pg_dump
를 호출합니다.
ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags
를 사용하여 pg_dump
를 설정할 수 있습니다.
예를 들어, structure dump에서 주석을 제외하려면 initializer에 다음을 추가하세요:
ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = ["--no-comments"]
structure dump를 실행할 때 flags를 지정합니다. PostgreSQL의 경우, pg_dump 명령어에 추가될 것입니다.
11 Explain
표준 explain
옵션과 함께, PostgreSQL 어댑터는 buffers
를 지원합니다.
Company.where(id: owning_companies_ids).explain(:analyze, :buffers)
#=> EXPLAIN (ANALYZE, BUFFERS) SELECT "companies".* FROM "companies"
# ...
# 회사 테이블에 대한 순차적 스캔 (cost=0.00..2.21 rows=3 width=64)
# ...
자세한 내용은 관련 문서를 참조하세요.