script/console の履歴機能が上手く動かなくなった...ので対応
現象
- irb は通常通り~/.irb_history に履歴をためていく
- rails のscript/console を起動すると、~/.irb_history の内容が上書きされ、先程までirb_history にあった最新の履歴が消えてしまう
script/console の実行中も、~/.irb_history をtail すると、どんどん履歴が書き込まれていっているのだけれど、何故かscript/console を終了して、再度起動したタイミングでやはり元の内容に戻ってしまう。~/.irb_history を空にしてもscript/console を実行すると中身が復活することから、どこからかコピーされている感じ。
原因
- 未だソースとかを追っていない
- もしかしたらrvm 関連の可能性もあるかも。rvm 使っていないときには何も発生しなかった
- あくまで可能性です。原因を調べていないのでなんとも言えないです
対応
- dotfiles/irb-1.8-history-fix.rb at master · gmarik/dotfiles · GitHubのパッチをモンキーパッチ形式で読み込んだら直った
- ~/.irbrc の履歴関連の部分
# http://github.com/gmarik/dotfiles/blob/84073cf564b601c99dc4b3b7910bd91234ff94f5/.ruby/lib/gmarik/irb-1.8-history-fix.rb # http://stackoverflow.com/questions/2065923/irb-history-not-working require 'irb/ext/save-history' module IRB # use at_exit hook instead finalizer to save history # as finalizer is NOT guaranteed to run def HistorySavingAbility.extended(obj); Kernel.at_exit{ HistorySavingAbility.create_finalizer.call } obj.load_history #TODO: super? obj end end if IRB::HistorySavingAbility.respond_to?(:create_finalizer) # http://rvm.beginrescueend.com/workflow/irbrc/ # for RVM IRB.conf[:PROMPT_MODE] = :DEFAULT # ヒストリーを有効にする IRB.conf[:EVAL_HISTORY] = 1000 IRB.conf[:SAVE_HISTORY] = 100 HISTFILE = "~/.irb_history" MAXHISTSIZE = 100 begin if defined? Readline::HISTORY histfile = File::expand_path( HISTFILE ) if File::exists?( histfile ) lines = IO::readlines( histfile ).collect {|line| line.chomp} puts "Read %d saved history commands from %s." % [ lines.nitems, histfile ] if $DEBUG || $VERBOSE Readline::HISTORY.push( *lines ) else puts "History file '%s' was empty or non-existant." % histfile if $DEBUG || $VERBOSE end Kernel::at_exit { lines = Readline::HISTORY.to_a.reverse.uniq.reverse lines = lines[ -MAXHISTSIZE, MAXHISTSIZE ] if lines.nitems > MAXHISTSIZE $stderr.puts "Saving %d history lines to %s." % [ lines.length, histfile ] if $VERBOSE || $DEBUG File::open( histfile, File::WRONLY|File::CREAT|File::TRUNC ) {|ofh| lines.each {|line| ofh.puts line } } } end end
vim でファイルを保存した時にGoogle Chrome で開いているページをリロードする
HTML を編集している時に必須の機能。今回試した方法は、
- AppleScript を使う
- ChromeReplを使う
- livereload
の3つです。最終的に、(不満ながらも)AppleScript 版を使っています。
1. AppleScript を使う
http://blog.cohtan.org/2008/03/vimhtmlyacss.htmlを参考にしました。
AppleScript の設置
- ~/bin/chrome_reload.scpt
tell application "Google Chrome" to activate
tell application "System Events" to keystroke "r" using {command down}
- ~/bin/terminal_focus.scpt
tell application "Terminal" to activate
vim コマンドの定義
command! -bar ChromeReload silent !osascript $HOME/bin/chrome_reload.scpt && osascript $HOME/bin/terminal_focus.scpt command! -bar ChromeStartObserve ChromeStopObserve | autocmd BufWritePost <buffer> ChromeReload command! -bar ChromeStopObserve autocmd! BufWritePost <buffer>
※ id:tyru さんにご指摘いただき、ChromeStartObserve を複数回実行しても、リロードが一回しか行われないよう修正
スクリプトを2つに分けているのは、ひとつのスクリプトで、Chrome をactivate ⇒ リロード ⇒ Terminal をactivate とすると、たまにキーストロークが実行される前にTerminal に戻ってきてしまいリロードが走らないためです。
別の解決方法がある気がするのですが・・・。
使い方
- :ChromeReload で現在開いているページをリロード
- :ChromeStartObserve を実行すると、以降そのバッファの内容を保存した時にChrome のリロードが実行されるようになる
- :ChromeStopObserve は、StartObserver の実行後、保存とリロードの連携を解除したい時に実行する
vim の方は、id:ursm さんの、Firefox を自動的にリロードする Vim スクリプト - ursmの日記を参考にしました。ursm++。
2. ChromeReplを使う
blog.8-p.info: Google Chrome で「保存したらリロード」を参考にしました。
ChromeRepl 拡張をChrome にインストール
ChromeRepl のクライアントをインストール
gem install google-chrome-client
vim コマンドの定義
ChromeStartObserve、ChromeStopObserve コマンドはAppleScript の時と同じで、ChromeReload だけを書き換えてあげればOKです。
command! -bar ChromeReload silent !chrome-repl -e "chrome.tabs.getSelected(null, function (t) { chrome.tabs.executeScript(t.id, { code: 'location.reload()' }) });"
使い方
- Chrome をオプションつきで起動
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-shell-port=9222
alias の例。
alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-shell-port=9222 > /dev/null 2>&1 &"
3. livereloadを使う
Twiwt:Blog / jugyo : LiveReload & guard-livereloadを参考にしました。
が、まだきちんと動いていないので、こうすれば動くはずという手順を書きます。
livereload をインストール
gem install livereload -v 1.3
最新版の1.4 を使うと、RubyCocoa のライブラリをロードしようとしてエラーしてしまうので、Issues - RubyCocoa is not reliable, get rid of using it (osx/foundation issues etc)にあるとおり、1.3 を使います。
また、ruby < 1.8.7 を使っている場合、livereload.rb 内でEnumerable#reduce を使っている部分でエラーするので、reduce をinject に書き換えてあげます。
livereload のChrome 拡張をインストール
現在Web で配布されている拡張は1.4 向けのものなので、livereload クライアント1.3 と一緒には使えないので、mockko's livereload at v1.3 - GitHubの拡張をインストールします。
使い方
指定したディレクトリ以下のファイルに変更があったらリロードするという仕様なので、ディレクトリを指定して起動。指定しなかった場合は、カレントディレクトリ以下のファイルを監視します。
cd path/to/yourapp livereload
ファイルに変更があったときに、ブラウザのWebSocket をたたくサーバが起動します。次に、ブラウザ側でリロードしたいページを開き、livereload の拡張をON にします。
あとは、ファイルに変化があればブラウザーをリロードしてくれる。
本当はこうすると動くはずなのですが、現在僕の環境ですとファイルの変化が検知されずブラウザにリロードの命令が飛びません。うーん。
考察
- メリット
- デメリット
以上、vim でファイルを保存した時にChrome のページをリロードする3つの方法でした。僕の環境ですと、AppleScript 版以外の2つは、動作に問題がありますので、AppleScript 版を使っています(´ω`;)
ChromeRepl、livereload は使い勝手がよいので、いずれこれらに移行したらまたBlog を書きたいと思います。
Use FasterCSV or Ruby 1.9's CSV library

Rubyベストプラクティス -プロフェッショナルによるコードとテクニック
- 作者: Gregory Brown,高橋征義,笹井崇司
- 出版社/メーカー: オライリージャパン
- 発売日: 2010/03/26
- メディア: 大型本
- 購入: 9人 クリック: 307回
- この商品を含むブログ (47件) を見る
を読んでいたら、標準CSV ライブラリの紹介が載っていた。知らなかったのだけれど、CSV ライブラリは1.9 系でFasterCSV に置き換えられたとのこと。
1.8 系以前のCSV ライブラリはそこまで使いやすいって感じじゃなかった。けれど、FasterCSV、1.9 系のCSV ライブラリを使うことで、CSV の操作をずっと簡単に行えることができるようになる!
CSV 形式のファイルを読み込む
例えば、ヘッダつきのCSV ファイルを読み込むなら、次のように書く必要があった。
require 'csv' csv = CSV::Reader.parse(DATA) header = nil csv.inject([]) do |result, line| unless header header = line else result.push Hash[*header.zip(line).flatten] end result end __END__ id,name,age 1,kai,19 2,hong,20 3,bob,
これが、FasterCSV(または1.9 系のCSV) を使うと次のように書けるようになる。(DATA の内容は先のDATA の内容と同じ)
require 'rubygems' require 'fastercsv' require 'ruby-debug' csv = FasterCSV.new(DATA, :headers => true, :header_converters => :symbol) csv.first[:id] # => "1" csv.first[:age] # => "20"
また、属性値を変換するConverter も指定することができる。ライブラリには日付と数値の属性値を変換するConverter が含まれていて、その他自分で作成したConverter も簡単に組み込める。
例えば、数値に変換できる値を変換するには、Converter として:integer を指定する。
csv = FasterCSV.new(DATA, :headers => true, :header_converters => :symbol, :converters => :integer) csv.first[:id] # => 1 csv.first[:age] # => 20
CSV 形式のファイルを書き出す
書き出す場合も、以前のCSV ライブラリだと使いにくい部分があって、例えば以下のように使えるラッパーをつくってた。
User = Struct.new(:id, :name, :age) elements = [ User.new(1, 'kai', 19), User.new(2, 'hong', 20) ] FileUtility.to_csv(elements, :header => %w[id name age]) do |e| [ e.id, e.name, e.age ] end # => "id,name,age\n1,kai,19\2,hong,20\n"
この場合も、FasterCSV なら次のように書ける。
FasterCSV.generate(:headers => %w[id name age], :write_headers => true){|csv| elements.inject(csv){|c, e| c << e c } } # => "id,name,age\n1,kai,19\2,hong,20\n"
Web アプリを書いていてよく使うデザインパターン(Composite パターン)
- DB に日々のブックマーク数を保存している
- 指定した期間のブックマークの日別の数を表示
- ヘッダには指定した期間のブックマークの合計を表示
というような場合を想定。
このような場合は、DB から取ってきた結果をComposite パターンを使った箱に入れると便利。
このくらいの用途だと「sum すればいいじゃん」という話になるけれど、
- 複数のテーブルからデータを取ってきた結果をComposite に入れて、ひとつのオブジェクトにまとめられて便利
- Component とComposite の統一インターフェース(上の例ではCounts#count, Count#count)を持てるので、ポリモーフィズムを利用しやすい
- 入れ物に集計機能がついているので、DB 以外にmemcached から取得した結果に対して使えたりできる
等の利点はあるかなと思います。
また、Composite パターンはruby と相性がいいかなーと思っていて、上の例のCounts に次のようなeach を実装してあげてEnumerable をinclude してあげるとか。で、Counts#each で指定した期間の日付順にCount を取得できるようにするとかできる。
class Counts include Enumerable def each @counts.values.each{|c| yield(c) } end end
RubyKaigi2010 に行ってきた(2, 3日目)
RubyKaigi2010 に行ってきた(1日目) - Slow Danceに続き、今年のRubyKaigi に行ってきたレポートを書きたいと思います!例によって興味があった内容をまとめました。Chad Fowler の基調講演の内容は情熱プログラマー ソフトウェア開発者の幸せな生き方を読むのが一番いいと思います。
また、内容に誤りがありましたらお手数ですが指摘いただけますと助かります。
Ruby powering 9 million dining tables(Cookpad)
Cookpad CTO の橋本さん(@hashikem)によるCookpad のサービス、システム全般のお話と、id:secondlife さんが何故Cookpad に入社したかというお話を聴きました。
規模
- 9.89 M UU / month
- 83万レシピ
システム構成
- MySQL 5.5 + Triton
- 5.1 だったような気もする
- スライドみようと思ったけど公開されていない
- Ruby On Rails 2.3
- Cent OS 5.3
LVS - Web ---------------- LVS -- App(rails)
|_ Cache_fs |_ Cache(フラグメントキャッシュ)
- PC版とモバイル版はWeb もApp も別
- 99% 以上のリクエストを200ms で返すことが出来ている
キャッシュ
使っているキャッシュは2種類。
- ページキャッシュ
- フラグメントキャッシュ
- 3つのできないキャッシュ
- 検索結果のキャッシュ
- 講演内容に検索結果もページキャッシュしているという部分があり、「クエリパターンを全部キャッシュしているの?」とか「新しいデータ作成されたらキャッシュの再生性を行うの?」とか疑問に思ったので、講演後に中の人に伺いました
- 1日1回バッチでキャッシュを作成する
- ログから上位x 件の検索パターンに対して検索結果のキャッシュを生成
- 検索パターンはいうほど多く無いらしい
- 1日1回しか作らないので、更新されても次の日までは検索結果に反映されない
- 本当は良くないけど現在はそうやっいる
DB レプリケーションとacts_as_readonlyable
- 更新直後はマスタからselect
- 更新処理はpost で実行しているので、ApplicationController のbefore_filter を使って、post 以外はslave を見るようacts_as_readonlyable のフラグをセットしている
DSR とコネクションプーリングの問題
この問題についてはRails(ActiveRecord)でデータベースへのコネクションプーリングをさせなくする - ククラフトに詳細が書かれている。
- cookpad ではDB へのアクセスもバランシングしていて、各slave の負荷が均等になるようにしている
App - LVS - DB
マスタDBの分散
- デュアルマスタ
- マスター1がダウンした場合はマスター2 に自動切り替えするようにしている
- ホットスタンバイ状態かな?
- 切り離された元のマスタは手動で戻す
- マスター1がダウンした場合はマスター2 に自動切り替えするようにしている
- 他に、機能別にマスタを分割
ヘルスチェックの罠
- とある時にやたらマスタDB の負荷が増える
- App からマスタDB が生きているか確認している処理があって、その処理にstatus コマンドを使っていた。status コマンドは重い
- 加えて、App がどんどん増えていくにつれマスタへの負荷が増えていた
- 対策
- ping でステータスチェックをするよう変更
やはり、問題が発生したら、原因分析と解決をしっかりとやっているという印象。
画像ファイル運用上の工夫
- 画像系の規約として画像(パス)を保存しているtable にはphoto_saved_at というカラムを付けるようにしている
- このカラムの値がNULL なら画像はなし
- 画像を表示させる部分には全て同じヘルパを使うことでこの規約を適用している
- このカラムの値がNULL なら画像はなし
監視
-
- etc.
- Content Analytics and Insights for Digital Publishing | Chartbeatという外部システムも使っている。昨日との差分が見られたりして便利
オフィス
- 以前は合宿をやっていたけど、今はオフィスが快適なので毎日合宿のようなもの
- 壁一面がホワイトボート
質問
- デザイナ - プログラマ間のコミュニケーションの取り方
- デザイン専門というのはいない
- デザイン面では公開前に必ず社長のチェックを受ける
- ユーザにヒットするモノを作るには
- 最初に実装されたものは大抵使いづらい。モデルユーザに使ってもらい使いやすくしていく
- 90% の機能を捨てるという覚悟。本質に集中する
- 公開している機能もどんどの削る
- ミッションの共有をどうやっているか
- 技術のための技術をしない
- 目的ありきの技術
- ここの認識がずれていると感じたら話しあって考える
ユーザ視点を本気でつらぬているということがヒシヒシ伝わってくる講演でした。
何故Cookpad に入ったのか(id:secondlife さん)
- ものづくりの考え方に共感
- エンジニア全員が技術+αを持っている
- 話をしていてとても面白い
- +αとは
- コードの価値観
- ここのコードがバグるのは絶対にあってはならないとかビジネスの視点を持っている
- 多様な視点
- ものづくりに真剣
- コードの価値観
- 本気でユーザのことを思っていれば+αはついてくる
橋本さんへの質問
講演終了後にその場に来ているCookpad のエンジニアが紹介され、質問がある方は直接話かけてみて下さいという形式のフリータイムに突入。
早速橋本さんに質問。
- 初心者エンジニアが育つにはどうすればいい?
個人的にも聞きたいこと。また、今自分がいる会社のエンジニアは大学時代にプログラムに触れていなかった人が大半のため、その人達が育ちやすい環境をどのように作ればいいと思うか、というあたりを伺ってみました。
- 基本的にcookpad は中途しかとらない
- 自身の経験を話すと期日までに作ると決めたら死ぬ気で作っていた
- でも作っている時はとても楽しい
- それこそ電車の中でも常にそのことを考えている。周りから見れば変な人だと思う
- しかし、実際こうやっている時が一番のびた
講演内容は素晴らしいもので、何故大ホール枠じゃないんだろうと思ったほどです。
逆に、大ホールでないから、30分以上の時間を使えるし、質問タイムも長めに設けて下さったので企画部屋枠が一番だったのかもしれません。
本当に勉強になりました。ありがとうございます。
RWikiと怠惰な私の10年間
dRuby の@m_sekiさんによるRWiki の活用事例、どうやって開発してきたのかの話。
NoSQL(ネタ)
- DB とかを使わないでログファイルに書き込む形式
- DB とかいらない。なんでもプロセスに入れている
- RWiki のプロセスが起動時にログファイルを読み込む
- 基本的にデータはメモリから返して書き込み操作のみをDisk に書いている
- メモリにはERB オブジェクトだけを保持
- メモリで処理する
- 全てのデータをオブジェクトにしてひとつのプロセスに配置
- 要素が数万程度のArray やHash は普通に使える
活用事例
問題点
- 起動が遅い
- 起動時にページをメモリに読みながらそのページにあるリンクも再帰的に読み込んで展開していた
- リンクの展開にはキューを使い、同時に生成される木はひとつになるようにした
- キャッシュとして、オブジェクトをMarshal.dump した状態でログファイルに書き込むようにした
- ログには最低限の情報しか保存しないという方針だったけどあまりこだわらないことにした
まとめ
- 全てのオブジェクトをひとつのプロセスに配置するのは自然
- 自然な設計なので変なトラブルにはまらない
- メモリが大きくなるが100 ページとかなら全然問題ない
これで、RubyKaigi2010 のレポートは終わりです。
RubyKaigi からそろそろ1ヶ月が経ちますが、本当に良いイベントでした。昨年より多くの人と話すことができたのはシャイなRubyist界隈の自分にとって良い試みだったと思います。その甲斐あって、得られたものはとても大きかったです。
(あと、デモでto_f するところがto_i になっていてテストが落ちるという現場に遭遇したので「to_f 必要です」とツッコんだw)
最後に、RubyKaigi スタッフの皆さんを初め、スピーカー、スポンサー、参加者の全ての皆さんに感謝します。ありがとうございました。
RubyKaigi2010 に行ってきた(1日目)
今年もRubykaigi に行ってきました。とても勉強になることが多かったので興味があった内容をまとめます。
まずは1日目から。2,3日目も後に書きます!
もし間違い等ありましたら指摘いただけると助かります。
Conflicts and Resolutions in Ruby and Rails
- ruby1.9 対応をする上で難しかったこと
- encoding。常にencoding について考えなければならなくなった
- 逆に正しくencoding を扱うプログラムを書けるようになった
- encoding。常にencoding について考えなければならなくなった
- Rails アプリは早急に1.9 に対応する必要はあるか
- アプリについては直ぐに対応する必要はないかもしれない
- 一方ライブラリは早急に対応する必要がある
- 全てのライブラリがencoding 対応していないとユーザが混乱してしまう
- Rails3 の良いところ
- モジュール化は素晴らしい
- Arel によってRack のように抽象層が入ったことで、ORM 層を切り替えられる仕組みができた
- Rails3 がまだ改善すべきところ
- ActiveRecord をRails と切り離して使うことは難しい。
- Ruby2.0 について
- より正しくなるとしても、今までのコードの大半が動かなくなるような変更はあまりして欲しくはない
jpmobile on Rails 3 の作り方
- @conceal_rs さん
- jpmobile コミッタ
- らくらく連絡網を作っている
- らくらく連絡網
- 開発の仕方
- Heroku でテストしている
- 基本的にRails のコードを読むだけ
-
- ビューのレンダリングを行っている部分を探す
- hook を挟める場所を探す
- 挟む
リアルタイムウェブができるまで
一部の内容は、個別に質問させていただいたものです。ミントいただきました( ´艸`)
- @makoto_inoue さん(id:makotoi)
- ロンドン在住!
- セッション保存のフロントとしてredis を使っている
- 個人的にはTC を使いたかったが、redis 好きがいるのでredis を使っている
- 現在の規模ならMySQL だけでも十分いけると思う
- redis
- ソート処理等色々できる
- 一方、redis はシングルスレッドでイベントループがまわっているので、重いソートを実行してしまうと、他の処理をブロックしてしまう
Head First ふつうのシステム開発
お客さんから要望を受けて、その機能をリリースするまでをライブ形式で行う。会社でプログラムを書いている身としては、他の会社の開発環境はかなり興味深く、最高の企画のひとつでした。こういうのどんどん増えるといいなぁ。
- フェーズ
-
- 計画
- 朝会
- 開発
- レビュー
- リリース
- 環境
- Scrum
- nginx + Passennger
- 計画
- 日頃から要件、バグをBTS に溜めてある
- その一覧からスプリントでやることを決めておく
- 見積りはある程度の精度で
- チームがコミットできることを宣言する
- 要件、仕様、受け入れ条件を明確にする
- 要件
- 要件(ストーリ)を最小単位に分割する
- 開発
- テスト書く
- Q&A
- 新人がursm さんとペアプロやるのって大変じゃない?
- 最初は速いと感じたがなれる。あと、丁寧に教えてくれるので理解できる
- コントローラのテストを厳密に書くか
- 不安に思わないなら書かない。ursm さんは書かない
- ヘルパのテストは書いた方がいいと思う(kakutani さん)
- 見積りはどうやってやるのか
- 自分の質問。当初予定していなかった点が開発中に見えてきて遅れることが多いので質問
- 見積りは絶対にひとりではしない。皆でやる
- 見積りはクイズなので当たらない
- 遅れたらできるだけ速く謝る
- 恒常的に遅れているプロジェクトならチームで何が問題なのか話しあう
- 皆どのくらいの時間に帰るのか
- ursm さんが大体最後。21時くらい
- お客さんの仕事は集中力が切れる19時くらいに終える。その後社内環境改善とかしている
コミュニティナイト
ほぼ懇親会。懇親会のチケットを持っていなかった自分にとってはとても助かりました。
未踏のid:kazuk_i さんを発見。アーキテクチャの話、スケーラビリティについて伺う
- 最近読み込みを非同期に行うためにcramp を使っている
- memcached を使いつつソート等の処理を行う方法
- MySQL をインデクスのストレージとして使い、データをmemcahed にもたせた(Webアプリをとりまく最近のKVS事情、雑感 - Tous Les Jours 攻防記)
- 読み込みと書き込みの高速化
- 読み込みは並列にStorage に問い合わせてアプリ側でマージするのが最速なんじゃないかと考えている
- 書き込みは非同期処理は使えない
- コールバックが実行される順番を予測できないから
- 書き込みの高速化にはキューを使う
- キューがつまらないよう工夫する必要はある
- Life is beautiful: マルチスレッド・プログラミングの落とし穴、その2が詳しい。コメント欄もあわせて
- memcached活用は、格納オブジェクトの”粒度”がキモ - Tous Les Jours 攻防記が詳しい
- ライブラリの作り方について
- 他のライブラリを読んだり、ライブラリの対象のドキュメントを詳細に読んだりとかはあまりしていない。結構適当に作っている
- 学習の仕方
- 基本大切。ものがどういう仕組みで動いているかを把握しておく必要はあると思う
永和のid:bekkou68 さんを発見。セッション中に質問しきれなかったことについて伺う
- プログラミングは入社前からやっていた
- ruby を始めたのは入社3ヶ月前くらいから
- 帰るのは6時半くらい
- 自分の時間をもたないと何をやっているのか分からなくなる
- 毎日昼休みに何か作っている
- 皆プログラミング好き
- 皆で色々な仕組みを作って社内環境を良くしている
- プロジェクト間の情報共有はML
@nippondanji さん発見!!Rubykaigi は漢のイベントだった!
- Blog を通じて質問させていただいたり、Blog にとても助けられているのでお礼を言う
- とても柔和な方だった
- Amazon CAPTCHAはとっても良い本なので、自分がMySQL を管理している立場の人は是非
普段疑問に思っていることについて色々質問することができてよかった。やっぱり直接話せるのと空気を感じることができるのが現地に行く最大のメリット!
質問にお答えくださった皆様ありがとうございましたー丶(´▽`)ノ
MySQL のNULL ではまったことあれこれ
MySQL に限らず、SQL のNULL の仕様には何回か「えっ」と驚くことがあったのでメモしておこうと思います。5.1 版の日本語マニュアルがなかったものについては、4.1 のマニュアルを参照しました。
そもそもNULL は何を意味するか
NULL は未定義または、不明を意味する。「電話番号を持たない」ということを表現する場合は、NULL ではなく、空の文字列を使う。
NULL 値というものを SQL 初心者はよく混乱します。SQL 初心者は、多くの場合、NULL が空文字 "" と同じであると考えてしまいます。これは違います。たとえば、以下のステートメントは完全に別のものです。
mysql> INSERT INTO my_table (phone) VALUES (NULL);
mysql> INSERT INTO my_table (phone) VALUES ("");どちらのステートメントも、値を phone カラムに挿入しています。しかし、最初のステートメントは NULL 値を挿入し、2 つ目は空文字を挿入しています。最初のステートメントは ``電話番号が不明'' であると考えることができ、2番目は``電話を持っていない'' と考えることができます。
MySQL :: MySQL 4.1 リファレンスマニュアル :: A.5.3 NULL 値の問題
NULL との比較演算結果は常にNULL
これは多分、誰もが一度ははまる。
MySQL :: MySQL 4.1 リファレンスマニュアル :: A.5.3 NULL 値の問題
- =, <, > 等、値のどちらかにNULL が含まれる場合、結果は常にNULL
- つまり、NULL = NULL はTRUE ではなく、NULL
- 値がNULL のカラムを検索する場合は、IS NULL を使う
- CASE val WHEN NULL はヒットしない
- COALESCE や、CASE WHEN val IS NULL などで対応
- うっかりJOIN の条件内で= で比較してしまったり
余談として、rails のfind_by_xxx メソッド等動的ファインダは、引数にnil を与えると、IS NULL を生成するようにできている。
NULL と論理演算子
比較演算子以上にはまる。
MySQL :: MySQL 5.1 リファレンスマニュアル :: 11.1.4 論理演算子
- 論理演算の結果は、TRUE(1)、FALSE(0)、NULL の3つのいずれかになる
- !NULL はNULL
- FALSE(0) AND NULL は0、TRUE(1) AND NULL はNULL
- FALSE(0) OR NULL はNULL、TRUE(1) OR NULL は1
単体だと「ふーん」という感じしかしないかもしれないけど、今回は以下のような例ではまった。
- タスク管理アプリケーション
- タスクにはタスク開始、終了予定日を設定できる
- タスクは、「2010/03/01 - 2010/03/31」のように日付の範囲を指定して検索できる
- 但し、指定する日付の上限、下限共に「なし」を指定することができる。
で、以下のようなSQL を構築していた。:period_from, :period_to が検索時に指定した値に置き変えらえる。
SELECT * FROM tasks WHERE !((period_from < :period_from AND period_to < :period_from) OR (:period_to < period_from AND :period_to < period_to))
上記のSQL は、指定する期間の上限、下限両方が指定された場合を想定して組んだもの。ここで、ふと思う。
「:period_from か:period_to のどちらかがNULL だったら、結果は!NULL、つまりTRUE になって、条件式の結果が必ずTRUE になってしまう気がする」
でも、ならない。先程書いた通り、!NULL はNULL だから。:period_from, :period_to の少なくとも1つがNULL だった場合、先程のSQL の結果がTRUE になることはない。FALSE かNULL になってしまう。このように複数の条件式の組合わせによって求められる結果があって、そのどこかの条件式の結果がNULL になる場合、予想外の結果になってしまうことがある。
教訓として、NULL が結果に含まれる場合は「IS NULL」等を使ってNULL を区別して扱う必要がある。NULL の性質を上手く使って…とかダメ。
NULL とインデックス
- IS NULL を使用した検索では、インデックスが使用される
- NULL を許可するカラムにユニークインデックスを貼った場合、各々のNULL は別の値として区別され、重複エラーを発生させない
NULL とソート
MySQL :: MySQL 4.1 リファレンスマニュアル :: A.5.3 NULL 値の問題
- ORDER BY を使用する際、降順でソートするように DESC を指定すると、NULL 値が最初または最後に表示される
- 5.0.67 では、DESC の場合はNULL が最後に、ASC の場合は最初にヒットした
NULL と集計
MySQL :: MySQL 4.1 リファレンスマニュアル :: A.5.3 NULL 値の問題
- COUNT()、MIN()、SUM() などの集約関数では、NULL 値は無視される
- COUNT(*) は、個々のカラム値ではなくレコードをカウントするので値がNULL のレコードもカウントされる
- GROUP BY を使用すると、すべての NULL 値が同じと見なされる
TIMESTAMP とNULL
MySQL :: MySQL 5.1 リファレンスマニュアル :: 10.3.1.1 TIMESTAMP MySQL 4.1での性質
- TIMESTAMP 型はNULL を許可しない
- TIMESTAMP 型をNULL 許可で定義すると、「NOT NULL DEFAULT CURRENT_TIMESTAMP」と定義される
- このように定義されたTIMESTAMP 型にNULL を指定すると、現在の時刻がセットされる
- ADD COLUM で上記のTIMESTAMP 型を追加した場合、それまでに作成されたレコードのカラム値は「0000-00-00 00:00:00」となる
ちなみに、4.1 のマニュアルには以下のように書かれている。
TIMESTAMP 型カラムでは、他のカラム型とは異なる方法で NULL 値が処理される。TIMESTAMP 型カラムには、NULL は格納できない。カラムに NULL を挿入すると、カラムの値として現在の日時が設定される。TIMESTAMP 型のカラムはこのように動作するため、NULL 属性と NOT NULL 属性は通常どおりには適用されず、それらを指定しても無視される。
その一方で、TIMESTAMP 型のカラムを MySQL クライアントにとって使用しやすくするために、サーバは、(実際には、TIMESTAMP 型カラムには NULL 値は格納されないにもかかわらず)TIMESTAMP 型カラムについて、NULL 値の割り当てが可能(true)と報告する。DESCRIBE tbl_name を使用してテーブルに関する記述を取得すると、これを確認できる。
注意: TIMESTAMP 型のカラムに、値として 0 を設定するのは、NULL を設定するのとは異なる。0 は TIMESTAMP 型の有効な値である。
MySQL :: MySQL 4.1 リファレンスマニュアル :: 6.5.3 CREATE TABLE 構文
追記
id:sugibuchi さんが、ブコメでNULL撲滅委員会の記事を紹介して下さいました。
NULL の何が問題か、NULL を回避するにはどうしたらいいか等が書かれていて参考になります。記事が若干前のものなので、古い情報もあります。
例えば、IS NULL、IS NOT NULL ではインデックスが使われないと書かれているのですが、少なくともMySQL 4.1.22ではIS NULL、IS NOT NULL でもインデックスが使われることを確認しました。EXPLAIN にもインデックスを使う旨が表示され、パフォーマンスも実際に向上します。
参考になる部分はNULL の回避策。
- 数値カラムはNULL の変わりに0 を使うとSUM 等に影響がなくて大体上手くいく
- 日付等は最大値、最小値を使う('0001-01-01', '9999-12-31')といい
「'0001-01-01'」なんて使えるんですね。2038 年問題とかに影響されるのかと思っていました。
NULL オブジェクトパターン的発想ですね。勉強になりました!