<?xml version="1.0" encoding="UTF-8" ?>
<entry
	xmlns="http://www.w3.org/2005/Atom"
	xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/"
	xml:lang="ja-JP"
>
	<title>記事-タグの関係をMySQLicious化</title>
	<id>tag:txqz.net,2008-05-05:blog/2008/05/05/1837</id>
	<link rel="self" href="http://txqz.net/blog/2008/05/05/1837.atom"/>
	<link rel="alternate" type="application/rss+xml" href="http://txqz.net/blog/2008/05/05/1837.rdf"/>
	<link rel="alternate" type="application/xhtml+xml" href="http://txqz.net/blog/2008/05/05/1837.xhtml"/>
	<link rel="alternate" type="text/html" href="http://txqz.net/blog/2008/05/05/1837.html"/>
	<link rel="contents" href="http://txqz.net/blog/2008/05/05/.atom" title="2008年5月5日"/>
	<link rel="first" href="http://txqz.net/blog/2001/08/04/0001.atom" title="地球空冷化"/>
	<link rel="prev" href="http://txqz.net/blog/2008/05/03/2316.atom" title="東方カラオケと「とんでもないものを盗んでいきました」で瞬間風速1705res/min"/>
	<link rel="next" href="http://txqz.net/blog/2008/05/09/1052.atom" title="起こられる"/>
	<link rel="last" href="http://txqz.net/blog/2008/12/19/2152.atom" title="浜松市街地を通り抜けて、ムーンライトながら～の思い出"/>
	<author>
		<name>陽坂智佐</name>
		<email>spambasket@txqz.net</email>
	</author>
	<content type="xhtml">
		<div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="http://txqz.net/blog/2008/03/20/1834" title="1:nなデータを検索しやすくDBに入れたい">先日の正規化ネタ</a>の件で<a href="http://b.hatena.ne.jp/teramako/20080321#bookmark-7947357" title="はてなブックマーク - 特別でないただのブックマーク / 2008年03月21日">id:teramako</a>さんが<a href="http://www.pui.ch/phred/archives/2005/04/tags-database-schemas.html" title="Tags: Database schemas">del.icio.usでの例</a>を紹介して下さったので移行してみた。</p>
<h2>今までの構造</h2>
<p>今までこのブログで記事とタグの関係がどうなっていたかというと、教科書どおりに正規化していた。ただ、タグそのものにIDをふって [items] - [item_tag] - [tags] という構造ではなくて、[items] - [tags]という形になっていた。以下のような感じ:</p>
<table>
<caption>items</caption>
<thead>
<tr><th>id</th><th>title</th><th>body</th><th>date</th></tr>
</thead>
<tbody>
<tr><th>1</th><td>地球空冷化</td><td>&lt;p&gt;最近、私の ...</td><td>2001-08-04 00:01:00</td></tr>
<tr><th>2</th><td>台風一過</td><td>&lt;p&gt;幸運なことに ...</td><td>2001-08-22 00:01:00</td></tr>
</tbody>
</table>
<table>
<caption>tags</caption>
<thead>
<tr><th>item_id</th><th>tag</th></tr>
</thead>
<tbody>
<tr><th>1</th><td>生活</td></tr>
<tr><th>1</th><td>自分のこと</td></tr>
<tr><th>2</th><td>2ch</td></tr>
<tr><th>2</th><td>コロッケ</td></tr>
<tr><th>2</th><td>台風</td></tr>
</tbody>
</table>
<p>この構造の問題点は、たとえば「Opera」と「ノウハウ」を含んで「演劇」を含まない記事を出したい! というのが面倒なこと。そのかわり正規化されているからとってもリレーショナル。ある時期に頻出するタグを求めるみたいなことがTF-IDFの応用ですぐできる。<a href="http://www.r.dl.itc.u-tokyo.ac.jp/~nakagawa/infoDB/ir-vector.pdf">HarmanとSparck Jonesによる正規化</a>を組み合わせると、ある期間に頻出するタグ<var>i</var>は、<code class="math">log<sub>2</sub>(タグ<var>i</var>のある期間内での登場数+1) / log<sub>2</sub>(ある期間内に登場したタグの種類数) * (log<sub>2</sub>(全記事数 / タグ<var>i</var>がつけられた記事数) + 1)</code>で求められる。<a href="http://txqz.net/blog/2006/12/19/2347" title="MySQLでTF-IDFの計算、あと2つのベクトルの内積の計算">ちゃんと正規化されていればこの計算をSQLで行うのは簡単</a>だが、1セルにすべて閉じ込めてしまうととたんに面倒になる。ただ、そんな頻度の少ない処理が面倒になることよりもいつも行う処理が1つのテーブルを参照するだけですむことのほうが重要だ。記事のCRUDが1テーブルでできるようになれば、記事の更新をphpMyAdminから行うことだってできる。</p>
<h2>移行作業</h2>
<p>ということで、"MySQLicious" solution を目指す。以下のような感じ:</p>
<table>
<caption>items</caption>
<thead>
<tr><th>id</th><th>title</th><th>body</th><th>date</th><th>tags</th></tr>
</thead>
<tbody>
<tr><th>1</th><td>地球空冷化</td><td>&lt;p&gt;最近、私の ...</td><td>2001-08-04 00:01:00</td><td>生活 自分のこと</td></tr>
<tr><th>2</th><td>台風一過</td><td>&lt;p&gt;幸運なことに ...</td><td>2001-08-22 00:01:00</td><td>2ch コロッケ 台風</td></tr>
</tbody>
</table>
<p>これをSQLだけでやるにはどうすればいいか。カラムの更新なんだからUPDATEだろjkと思い、下のようなクエリを発行したら<samp>#1242 - Subquery returns more than 1 row</samp>と怒られた:</p>
<pre><code class="mysql">UPDATE items SET tags = concat(tags, ' ', (SELECT tag FROM tags WHERE items.id = tags.id));</code></pre>
<p>これが数値だったら、group byしてsum()したのをupdateすれば終わってたのになー。そんなこといってもしょうがないのでもう少し考えてみたところ、on duplicate keyを使う案をひらめいた:</p>
<pre><code class="mysql">INSERT INTO items(id, tags) SELECT old_items.id, tag FROM old_items LEFT JOIN tags USING (id) ON duplicate KEY UPDATE tags = concat(tags, ' ', tag);</code></pre>
<p>いやーon duplicate keyって便利ですねー。</p>
<p>ただ、これ逆のことをやろうと思ったらSQLだけじゃできないだろうなあ。</p>
<ins class="ps" datetime="2008-10-16T19:10:36+09:00" id="PS1224151836">
<p>4.1以降なら<a href="http://dev.mysql.com/doc/refman/4.1/ja/group-by-functions.html" title="MySQL :: MySQL 4.1 リファレンスマニュアル :: 6.3.7.1 GROUP BY 関数">group_concat()関数</a>が使える。この関数の存在に今日気づいたので追記。</p>
</ins>
<h2>副作用</h2>
<p>先に触れた、「ある期間中に特に現れるタグ」の計算が面倒になった。今まではこんな感じで計算していた (一部テヌキのためにテキトーな数字で決め打ちしているところがある):</p>
<pre><code class="mysql">SELECT tag, log(count(tag) + 1) / log(5) * (log(2240/count) + 1) score FROM tags LEFT JOIN df USING(tag) LEFT JOIN items USING(id) WHERE date between ? and ? and count > 1 GROUP BY tag ORDER BY score desc, count(tag) desc LIMIT 10;</code></pre>
<p>変更後はこうなった:</p>
<pre><code class="mysql">SELECT tag, log(sum(CASE WHEN length(tags) = length(replace(tags, tag, '')) THEN 0 ELSE 1 END) + 1 ) / log(5) * (log(3350/count) + 1) score FROM df LEFT JOIN items ON tags like concat('%', tag ,'%') WHERE date between ? and ? and count > 1 GROUP BY tag ORDER BY score desc LIMIT 10;</code></pre>
<p>ということで、はじめてCASE式を使ってみた。<a href="http://www.geocities.jp/mickindex/database/db_case.html" title="CASE式のススメ">CASE式は面白い</a>ので、ぜひSELECT句で分岐させていきたいですね。</p>
<p>副作用だといえるものは、多分これくらいだと思う。LIKE検索で検索が遅くなるのは、このサイトくらいの記事保持数やアクセス数だったらそこまで問題にはならないだろうし、それより内部処理が簡単になる利点を求めたい。</p>
		</div>
	</content>
	<category term="このサイト"/>
	<category term="MySQL"/>
	<category term="スキーム"/>
	<category term="SQL"/>
	<trackback:ping>http://txqz.net/blog/2008/05/05/1837/tb</trackback:ping>
	<published>2008-05-05T18:37:20+09:00</published>
	<updated>2008-10-16T19:10:36+09:00</updated>
	<rights>Attribution-Noncommercial-Share Alike 3.0 Unported</rights>
</entry>