【入門】Rails エンジニアが知っておきたい配列・ハッシュ・Set の違いと使い分け
はじめに
皆さんは配列・ハッシュ・Set をどのように使い分けていますか?
「なんとなく使い分けている」という方も多いかと思いますが、選び方ひとつでパフォーマンスに大きく差が出ることがあります。
本記事では、基本的な違いから Rails での実践的な使い分けまでを整理しました。
少しでも皆様の参考になりますと幸いです。

注意点
環境
配列(Array)
配列とは
配列とは、複数の値を順番に並べて管理するデータ構造です。
各要素にはインデックス(0始まり)でアクセスします。
fruits = ["りんご", "みかん", "ぶどう"]
fruits[0] #=> "りんご"
fruits[1] #=> "みかん"
fruits[2] #=> "ぶどう"
配列の特徴
- 順序がある:要素の並び順が保証される
- インデックスでアクセス:0, 1, 2, ... の数値で取得する
- 重複を許容する:同じ値を複数格納できる
- 任意の型を混在できる:文字列、数値、オブジェクトなどを混ぜて格納できる
# 重複 OK
numbers = [1, 2, 2, 3, 3, 3]
# 型の混在も OK
mixed = ["文字列", 100, true, nil]
配列のよく使うメソッド
要素の追加
fruits = ["りんご", "みかん", "ぶどう"]
fruits.push("もも") #=> ["りんご", "みかん", "ぶどう", "もも"]
fruits << "いちご" #=> ["りんご", "みかん", "ぶどう", "もも", "いちご"]
要素数の取得
fruits = ["りんご", "みかん", "ぶどう"]
fruits.length #=> 3(配列の要素数を返す)
fruits.count #=> 3(条件付きでも使える)
# count は条件を指定できる
numbers = [1, 2, 2, 2, 3, 3]
numbers.count(2) #=> 3(値が2の要素数)
繰り返し処理
fruits = ["りんご", "みかん", "ぶどう"]
# 各要素に対して処理を実行
fruits.each do |fruit|
puts fruit
end
# 各要素を変換して新しい配列を作成
fruits.map { |fruit| "#{fruit}ジュース" }
#=> ["りんごジュース", "みかんジュース", "ぶどうジュース"]
検索・絞り込み
fruits = ["りんご", "みかん", "ぶどう"]
fruits.include?("みかん") #=> true
# 条件に一致する要素を抽出
fruits.select { |fruit| fruit.include?("ん") }
#=> ["りんご", "みかん"]
並べ替え
numbers = [3, 1, 4, 1, 5]
numbers.sort #=> [1, 1, 3, 4, 5]
numbers.sort.reverse #=> [5, 4, 3, 1, 1]
ハッシュ(Hash)
ハッシュとは
ハッシュとは、キーと値のペアでデータを管理するデータ構造です。
各要素にはキーでアクセスします。
user = { name: "佐藤", age: 25, role: "エンジニア" }
user[:name] #=> "佐藤"
user[:age] #=> 25
user[:role] #=> "エンジニア"
ハッシュの特徴
- キーと値のペア:名前付きでデータを管理できる
- キーは一意:同じキーが重複すると、後から追加した値で上書きされる
- 順序がある:挿入順が保持される
- 任意の型をキーにできる:シンボル、文字列、数値など
# キーが重複すると上書き
hash = { a: 1, b: 2, a: 3 }
hash[:a] #=> 3
# 文字列キーも使える
hash = { "name" => "佐藤", "age" => 25 }
ハッシュのよく使うメソッド
要素の追加・更新
user = { name: "佐藤", age: 25, role: "エンジニア" }
user[:email] = "example@test.com"
user[:age] = 26
キー・値の取得
user = { name: "佐藤", age: 25, role: "エンジニア" }
user.keys #=> [:name, :age, :role]
user.values #=> ["佐藤", 25, "エンジニア"]
繰り返し処理
user = { name: "佐藤", age: 25, role: "エンジニア" }
# 各キーと値に対して処理を実行
user.each do |key, value|
puts "#{key}: #{value}"
end
# 値だけを変換して新しいハッシュを作成
user.transform_values { |value| value.to_s }
#=> { name: "佐藤", age: "25", role: "エンジニア" }
検索・判定
user = { name: "佐藤", age: 25, role: "エンジニア" }
user.key?(:name) #=> true
user.value?("エンジニア") #=> true
# 条件に一致する要素を抽出
user.select { |key, value| value.is_a?(String) }
#=> { name: "佐藤", role: "エンジニア" }
マージ
defaults = { role: "一般", active: true }
user = { name: "佐藤", role: "エンジニア" }
# 同じキーがある場合、後のハッシュの値で上書きされる
defaults.merge(user)
#=> { role: "エンジニア", active: true, name: "佐藤" }
Set(集合)
Set とは
Set とは、重複のない要素の集合を管理するデータ構造です。
内部的にはハッシュで実装されており、検索がハッシュと同様に高速です(O(1)については「パフォーマンスの違い」で解説します)。
fruits = Set.new(["りんご", "みかん", "ぶどう"])
# 重複は自動で排除される
fruits.add("りんご")
#=> #<Set: {"りんご", "みかん", "ぶどう"}>
# 高速な検索(O(1))
fruits.include?("みかん") #=> true
Set の特徴
- 重複を許容しない:同じ値を追加しても自動で排除される
-
検索が高速:内部がハッシュのため
include?が O(1) で動作する - 順序がある:Ruby の実装上、挿入順が保持される
- 集合演算ができる:積集合・和集合・差集合などが使える
Set のよく使うメソッド
要素の追加・削除
fruits = Set.new(["りんご", "みかん"])
fruits.add("ぶどう") #=> #<Set: {"りんご", "みかん", "ぶどう"}>
fruits.delete("みかん") #=> #<Set: {"りんご", "ぶどう"}>
集合演算
a = Set.new([1, 2, 3])
b = Set.new([2, 3, 4])
a & b #=> #<Set: {2, 3}> (積集合:両方に含まれる要素)
a | b #=> #<Set: {1, 2, 3, 4}> (和集合:どちらかに含まれる要素)
a - b #=> #<Set: {1}> (差集合:a にだけ含まれる要素)
部分集合の判定
a = Set.new([1, 2])
b = Set.new([1, 2, 3, 4])
a.subset?(b) #=> true(a は b の部分集合)
b.superset?(a) #=> true(b は a の上位集合)
配列・ハッシュ・Set の違い早見表
| 項目 | 配列(Array) | ハッシュ(Hash) | Set(集合) |
|---|---|---|---|
| アクセス方法 | インデックス(数値) | キー(シンボル・文字列など) | 値そのもの |
| 用途 | 順序のあるデータの集合 | 名前付きデータの集合 | 重複のないデータの集合 |
| 重複 | 値の重複 OK | キーの重複 NG(上書き) | 値の重複 NG(自動排除) |
| 検索速度 |
include? は O(n) |
キー検索は O(1) |
include? は O(1) |
| 適した場面 | 一覧・リスト・繰り返し処理 | マッピング・集計・設定管理 | 一意な値の管理・存在チェック |
Rails での使い分け
ここからは、Rails の実務でよく見る配列・ハッシュ・Set の使い分けを具体例で紹介します。
配列が適しているケース
1. 一覧データの取得と加工
# pluck で取得したデータは配列として返される
emails = User.where(active: true).pluck(:email)
#=> ["user1@example.com", "user2@example.com", "user3@example.com"]
# 配列のメソッドで加工・繰り返し処理ができる
emails.each do |email|
NotificationMailer.weekly_report(email).deliver_later
end
2. セレクトボックスの選択肢
# ビューで使う選択肢の一覧
PREFECTURES = ["北海道", "青森県", "岩手県", "宮城県", "秋田県"]
# enum の定義
class Post < ApplicationRecord
enum :status, [:draft, :published, :archived]
end
3. バッチ処理での一括操作
# ID の一覧を配列で管理し、一括処理
target_ids = [1, 5, 10, 15]
Post.where(id: target_ids).update_all(published: true)
ハッシュが適しているケース
1. 値のマッピング
# ステータスコードと表示名の対応表
# キーで直接アクセスできる(配列では実現しにくい)
STATUS_LABELS = {
draft: "下書き",
published: "公開中",
archived: "アーカイブ"
}
STATUS_LABELS[:published] #=> "公開中"
2. デフォルト値の管理
# デフォルト設定をハッシュで定義
DEFAULT_CONFIG = {
theme: "light",
language: "ja",
per_page: 20
}
# ユーザーごとの設定で上書き(merge は後のハッシュの値で上書きする)
user_config = { theme: "dark", per_page: 50 }
config = DEFAULT_CONFIG.merge(user_config)
#=> { theme: "dark", language: "ja", per_page: 50 }
3. データの集計
# カテゴリごとの商品数をハッシュで集計
# キーがカテゴリ名、値が件数 → 配列では表現しにくい構造
category_counts = Product.pluck(:category).tally
#=> { "食品" => 15, "飲料" => 8, "日用品" => 12 }
# 特定のカテゴリの件数に直接アクセスできる
category_counts["食品"] #=> 15
Set が適しているケース
1. 重複排除が必要な一覧の管理
# タグの一覧を重複なしで管理
tags = Set.new
posts.each do |post|
post.tags.each { |tag| tags.add(tag.name) }
end
# 同じタグ名が何度追加されても重複しない
2. 大量データに対する存在チェック
# 公開済み記事の ID を Set で保持し、高速に判定
published_ids = Set.new(Post.where(status: :published).pluck(:id))
posts.each do |post|
if published_ids.include?(post.id)
# 公開済みの処理
end
end
3. 権限やロールの判定
# ユーザーの権限を Set で管理
admin_permissions = Set.new([:read, :write, :delete, :manage])
editor_permissions = Set.new([:read, :write])
# 権限の差分を確認
admin_permissions - editor_permissions
#=> #<Set: {:delete, :manage}>
# 特定の権限を持っているか判定
editor_permissions.include?(:write) #=> true
パフォーマンスの違い
配列・ハッシュ・Set では、検索のパフォーマンスに大きな差があります。
O(n) と O(1) とは?
なぜ処理時間が変わるのか?
配列とハッシュ・Set では、データの内部的な保持の仕方が異なります。
配列の場合(O(n)):先頭から順に確認
配列は、データを一列に並べて保持しています。
特定の値を探すには、先頭から順番に 1 つずつ確認していく必要があります。
配列: [りんご] → [みかん] → [ぶどう] → [もも] → [いちご]
①確認 ②確認 ③確認 ④確認 ⑤見つかった!
「いちご」を探す → 先頭から5回の確認が必要
→ データが増えるほど、最悪の場合の確認回数も増える
ハッシュ・Set の場合(O(1)):格納場所を計算して直接アクセス
ハッシュ・Set は、値から「格納場所」を計算(ハッシュ関数)して保持しています。
探したい値からも同じ計算で格納場所がわかるため、直接アクセスできます。
"いちご" → ハッシュ関数で格納場所を計算 → 場所[3]に直接アクセス!
格納場所: [0:ぶどう] [1:みかん] [2:もも] [3:いちご] [4:りんご]
↑ 1回でアクセス!
→ データが何件あっても、計算1回でアクセスできる
実測で比べてみる
以下のコードで、100万件のデータに対する検索速度を比較できます。
n = 1_000_000
array = (1..n).to_a
hash = (1..n).each_with_object({}) { |i, h| h[i] = true }
set = Set.new(1..n)
target = n # 配列の末尾にある要素を検索
Benchmark.bm(8) do |x|
x.report("Array:") { 1000.times { array.include?(target) } }
x.report("Hash:") { 1000.times { hash.key?(target) } }
x.report("Set:") { 1000.times { set.include?(target) } }
end
実行結果の例(100万件のデータを 1,000 回検索):
user system total real
Array: 2.525581 0.000313 2.525894 ( 2.525909)
Hash: 0.000050 0.000002 0.000052 ( 0.000052)
Set: 0.000049 0.000002 0.000051 ( 0.000051)
配列の検索は約 2.5 秒かかっているのに対し、ハッシュ・Set は約 0.00005 秒で完了しています。
約 50,000 倍の差があり、データ量が多く検索頻度が高い場合は大きなパフォーマンス問題になります。
メモリ使用量の違い
検索速度だけでなく、メモリ使用量にも違いがあります。
ハッシュ・Set は高速な検索を実現するために、内部でハッシュテーブルという仕組みを保持しているため、配列より多くのメモリを消費します。
以下のコードでメモリ使用量を確認できます。
n = 1_000_000
array = (1..n).to_a
hash = (1..n).each_with_object({}) { |i, h| h[i] = true }
set = Set.new(1..n)
puts "Array: #{ObjectSpace.memsize_of(array)} bytes"
puts "Hash: #{ObjectSpace.memsize_of(hash)} bytes"
puts "Set: #{ObjectSpace.memsize_of(set.instance_variable_get(:@hash))} bytes"
実行結果の例(100万件の整数データ):
Array: 11636312 bytes
Hash: 33554592 bytes
Set: 33554592 bytes
迷ったときの判断基準
最後に、配列・ハッシュ・Set のどれを使うか迷ったときの判断基準をまとめます。
配列を選ぶとき:
- データの順序が重要
- 繰り返し処理がメインの用途
- 要素に名前が不要
ハッシュを選ぶとき:
- キーで直接アクセスしたい
- データに名前を付けたい
- キーと値のマッピングで高速にアクセスしたい
Set を選ぶとき:
- 重複のない値の集合を扱いたい
- 「含まれるかどうか」の判定を高速に行いたい
- 集合演算(積集合・和集合・差集合)を使いたい
まとめ
配列・ハッシュ・Set の違いと使い分けについてまとめてみました。
- 配列は「順番のあるデータの集まり」に適している
- ハッシュは「名前付きデータの管理」や「高速な検索」に適している
- Set は「重複のない値の集合」や「高速な存在チェック」に適している
- 検索頻度が高い場合は、配列よりもハッシュや Set を使うことでパフォーマンスが向上する
適切に配列・ハッシュ・Set を使い分けることでパフォーマンスを向上させていきましょう。
最後までお読みいただき、ありがとうございました。
Discussion