— ユーザーID=1のタイムライン(フォロー中のユーザーの投稿)
SELECT
p.post_id,
p.content,
p.image_url,
p.likes_count,
p.comments_count,
p.created_at,
u.username,
u.display_name,
u.avatar_url
FROM posts p
INNER JOIN users u ON p.user_id = u.user_id
INNER JOIN follows f ON f.following_id = p.user_id
WHERE f.follower_id = 1
ORDER BY p.created_at DESC
LIMIT 20;
🚀 パフォーマンス最適化
このクエリを高速化するために、以下のインデックスが必要:
follows(follower_id, following_id):複合主キー
posts(created_at DESC):降順インデックス
posts(user_id):外部キー
いいね数ランキング(人気投稿)
— 過去7日間で最もいいねされた投稿TOP10
SELECT
p.post_id,
p.content,
p.likes_count,
u.username,
u.display_name
FROM posts p
INNER JOIN users u ON p.user_id = u.user_id
WHERE p.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY p.likes_count DESC
LIMIT 10;
ハッシュタグ検索
— #MongoDBタグの付いた投稿を検索
SELECT
p.post_id,
p.content,
p.created_at,
u.username
FROM posts p
INNER JOIN users u ON p.user_id = u.user_id
INNER JOIN post_hashtags ph ON p.post_id = ph.post_id
INNER JOIN hashtags h ON ph.hashtag_id = h.hashtag_id
WHERE h.tag_name = ‘MongoDB’
ORDER BY p.created_at DESC
LIMIT 20;
⚠️ LIKE検索の注意点
LIKE ‘%山田%’(前方一致でない)はインデックスが効かないため遅いです。
実務では全文検索エンジン(Elasticsearch等)を併用することが多いです。
🔄 5. 非正規化の戦略
なぜ非正規化するのか?
SNSでは読み取り頻度が非常に高いため、パフォーマンスを優先して一部を非正規化します。
【正規化済み(理想的だが遅い)】
postsテーブルには likes_count を持たない
→ いいね数を知りたいときは毎回 COUNT(*) でカウント
SELECT post_id, COUNT(*) as likes_count
FROM likes WHERE post_id = 123;
【非正規化(高速だが管理が必要)】
postsテーブルに likes_count カラムを追加
→ いいねされるたびに +1 更新
UPDATE posts SET likes_count = likes_count + 1 WHERE post_id = 123;
カラム
正規化の観点
実務での判断
posts.likes_count
likesテーブルからCOUNTすべき
非正規化OK:頻繁に読まれるため
posts.comments_count
commentsテーブルからCOUNTすべき
非正規化OK:頻繁に読まれるため
users.followers_count
followsテーブルからCOUNTすべき
非正規化OK:プロフィールで表示
hashtags.posts_count
post_hashtagsからCOUNTすべき
非正規化OK:人気タグ表示に使用
💡 非正規化の管理方法
非正規化したカラムは、トリガーまたはアプリケーション側で常に最新に保つ必要があります。
— いいねが追加されたときのトリガー例
DELIMITER //
CREATE TRIGGER after_like_insert
AFTER INSERT ON likes
FOR EACH ROW
BEGIN
UPDATE posts
SET likes_count = likes_count + 1
WHERE post_id = NEW.post_id;
END //
DELIMITER ;
ALTER TABLE posts
ADD COLUMN retweets_count INT UNSIGNED DEFAULT 0,
ADD COLUMN original_post_id BIGINT UNSIGNED NULL,
ADD FOREIGN KEY (original_post_id) REFERENCES posts(post_id) ON DELETE CASCADE;
2. リツイート作成の流れ
— 元の投稿(post_id=100)をリツイート
INSERT INTO posts (user_id, content, original_post_id)
VALUES (2, ”, 100);
— 元の投稿のリツイート数を更新
UPDATE posts SET retweets_count = retweets_count + 1 WHERE post_id = 100;
3. タイムライン取得クエリ
SELECT
p.post_id,
p.content,
p.original_post_id,
p.created_at,
u.username,
op.content as original_content,
ou.username as original_username
FROM posts p
INNER JOIN users u ON p.user_id = u.user_id
LEFT JOIN posts op ON p.original_post_id = op.post_id
LEFT JOIN users ou ON op.user_id = ou.user_id
ORDER BY p.created_at DESC;
演習 2発展
ダイレクトメッセージ(DM)機能を追加してください。
要件:
ユーザー同士が1対1でメッセージを送れる
既読・未読の管理
メッセージ一覧を取得できる
【解答】
1. direct_messagesテーブルを作成
CREATE TABLE direct_messages (
message_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
sender_id BIGINT UNSIGNED NOT NULL,
receiver_id BIGINT UNSIGNED NOT NULL,
content TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
read_at TIMESTAMP NULL,
FOREIGN KEY (sender_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (receiver_id) REFERENCES users(user_id) ON DELETE CASCADE,
INDEX idx_sender_receiver (sender_id, receiver_id),
INDEX idx_receiver_created (receiver_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. メッセージ送信
INSERT INTO direct_messages (sender_id, receiver_id, content)
VALUES (1, 2, ‘こんにちは!’);
3. 未読メッセージ取得
SELECT
dm.message_id,
dm.content,
dm.created_at,
u.username as sender_name
FROM direct_messages dm
INNER JOIN users u ON dm.sender_id = u.user_id
WHERE dm.receiver_id = 2 AND dm.is_read = FALSE
ORDER BY dm.created_at DESC;
4. 既読にする
UPDATE direct_messages
SET is_read = TRUE, read_at = NOW()
WHERE message_id = 123;
DELIMITER //
CREATE TRIGGER after_like_notification
AFTER INSERT ON likes
FOR EACH ROW
BEGIN
DECLARE post_owner_id BIGINT;
SELECT user_id INTO post_owner_id FROM posts WHERE post_id = NEW.post_id;
— 自分自身へのいいねは通知しない
IF post_owner_id != NEW.user_id THEN
INSERT INTO notifications (user_id, actor_id, notification_type, post_id)
VALUES (post_owner_id, NEW.user_id, ‘like’, NEW.post_id);
END IF;
END //
DELIMITER ;
3. 未読通知取得
SELECT
n.notification_id,
n.notification_type,
n.created_at,
u.username as actor_name,
u.avatar_url as actor_avatar,
p.content as post_content
FROM notifications n
INNER JOIN users u ON n.actor_id = u.user_id
LEFT JOIN posts p ON n.post_id = p.post_id
WHERE n.user_id = 1 AND n.is_read = FALSE
ORDER BY n.created_at DESC
LIMIT 20;
用途によります。ユーザー削除時に関連データも全て削除したい場合は適切です。ただし、意図せず大量のデータが削除される可能性があるため、本番環境では慎重に使う必要があります。代替手段として、論理削除(is_deletedフラグ)を使う方法や、ON DELETE SET NULLで参照をNULLにする方法もあります。重要なのは、削除の影響範囲を事前に把握しておくことです。