それ単一SQLで出来るよ
*注意:勘違いでした。以下はまちがってます。
PostgreSQLだけど。MySQLはサブクエリが微妙だったはずなので出来るかわからない。
使用したバージョンはPostgreSQL 8.2ですが、それほど新しくないPostgreSQLでもいけると思う。
Kazuho@Cybozu Labs: フレンド・タイムライン処理の原理と実践
1. フォローしている各ユーザーについて、そのメッセージ ID の最大値を取得
2. 1 のリストをメッセージ ID の降順でソートし、その先頭 20 件 (1ページ分) 以外を破棄
3. 2 のリストの全ユーザーについて、最新 20 件のメッセージを取得し、マージ
4. 3 のリストの先頭 20 件が、クライアントに返すべきフレンド・タイムライン
テーブル定義はこんな感じ。serial型はinteger AUTO_INCREMENT(と同じとみなしていい)。
CREATE TABLE follower ( id serial NOT NULL, followed_user_id integer NOT NULL, -- フォローされてるユーザID following_user_id integer NOT NULL,-- フォローしてるユーザID CONSTRAINT follower_pkey PRIMARY KEY (id) ); CREATE TABLE messages ( id serial NOT NULL, user_id integer NOT NULL,-- 発言した人のユーザID message character varying(255) NOT NULL,-- メッセージ本体。character varying=varchar CONSTRAINT messages_pkey PRIMARY KEY (id) );
データを入れていく。user_id=1の人のフレンドタイムラインを構築する予定。
-- user_id=1の人が、user_id 2〜19までをフォローする。 -- 特に19である必要も連番である意味もない。 INSERT INTO follower (following_user_id,followed_user_id) VALUES (1,2) ,(1,3) ,(1,4) ,(1,5) ,(1,6) ,(1,7) ,(1,8) ,(1,9) ,(1,10) ,(1,11) ,(1,12) ,(1,13) ,(1,14) ,(1,15) ,(1,16) ,(1,17) ,(1,18) ,(1,19); /* |following_user_id|followed_user_id| |1|2| |1|3| |1|4| |...|...| |1|19| */ -- 以下のようなSQLを最低でも1000回くらいは繰り返してダミーデータ生成。 INSERT INTO messages (user_id,message) VALUES (floor(random()*50)+1,random()::text); -- user_id 1〜50の発言をランダムに生成
準備が出来たらいよいよ取得。user_id BETWEEN 2 AND 19じゃないやつが出たら失敗。
select main.* from messages as main inner join ( select user_id from messages where user_id IN ( select followed_user_id from follower where following_user_id=1 -- user_id=1の人のフレンドラインを取得するので。 ) group by user_id order by max(id) DESC -- 手順1:メッセージID最大値 limit 20 -- 手順2:21件目以降は破棄する ) as tmp on (tmp.user_id=main.user_id) -- 手順3:マージ order by id desc limit 20 -- 手順4:結果20件
うまくいった(はず)。
ちなみに2回出てくるlimit 20と、following_user_id=1がそれぞれ変数になります。
MySQL の場合は、一番外側の各 user_id 毎に select * from message してマージする所において、外側で limit 20 していても、一旦 (その user_id に属する) 全行にアクセスしてしまう感じです。PostgreSQL だと、そういうことは発生しないのでしょうか?
すみません。手順 3 の表現が微妙だったかと思うのですが、「2 のリストの各ユーザーについて、それぞれ最新 20 件のメッセージを取得し、マージ」ということが PostgreSQL だと1行のクエリで実行できるのでしょうか? だとすると PostgreSQL のオプティマイザは、すばらしいですね。
あ、根本的に勘違いしてました。申し訳ないです。
2時間ほどいろいろ試してみましたがやっぱり不可能な気がします。
こちらこそです。tt25 さんのエントリに刺激をうけて、MySQL で1ステートメントでなんとかならないか、とがんばってみましたが、パフォーマンスは向上しませんでした。残念。
http://d.hatena.ne.jp/kazuhooku/20080609/1213030182
select tt.* from (
select user_id ,max(id) as maxid
from messages as tmp
where user_id IN (
select followed_user_id from follower where following_user_id=1
)
group by user_id
order by max(id) DESC
limit 20
) as users
inner join messages as tt on (
tt.user_id=users.user_id
and tt.id IN (
select id from messages
where user_id=tt.user_id
order by id desc
limit 20
)
)
order by tt.id desc
limit 20
本文のものとの違いは一番外のINNER JOINのONに条件を追加しただけですが、これで遅くなるってことは余計なことしてオプティマイザの気を散らしたってことなのかなあ。
すいません、あんまり動作がよくわかってないのでめちゃくちゃなことを書いてるかもしれません。
EXPLAIN ANALYZEもしてみたんですが、僕ではいまいち見方がわからないのでいちおう置いておきます。
記事中に書いたSQL
http://tt25.org/blog/attachment/sql-a.txt
このコメントで書いたSQL
http://tt25.org/blog/attachment/sql-a2.txt