✏️ Explanatory Question
Level: Hard — Tests many-to-many modeling, high-write design, and the read-receipt fan-out problem.
-- A conversation (thread) — works for both 1-on-1 and group
CREATE TABLE conversations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
type ENUM('direct','group') NOT NULL,
title VARCHAR(255), -- group name (NULL for direct)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- Membership (many-to-many: users <-> conversations)
CREATE TABLE conversation_members (
conversation_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_read_message_id BIGINT, -- pointer for unread counts
PRIMARY KEY (conversation_id, user_id), -- a user is in a convo once
INDEX idx_user (user_id) -- "list my conversations"
) ENGINE=InnoDB;
-- Messages (the huge, write-heavy table)
CREATE TABLE messages (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
conversation_id BIGINT NOT NULL,
sender_id BIGINT NOT NULL,
body TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- THE critical index: latest messages per conversation
INDEX idx_convo_time (conversation_id, id)
) ENGINE=InnoDB;
(conversation_id, id) on messages. Since id is auto-increment (time-ordered), "latest 50 messages" becomes a single indexed range scan — no filesort, no full scan.
-- Uses the (conversation_id, id) index directly; keyset pagination for scroll
SELECT id, sender_id, body, created_at
FROM messages
WHERE conversation_id = 42
AND id < 9000000 -- last seen id (keyset, for "load older")
ORDER BY id DESC
LIMIT 50;
"Seen by" is a fan-out problem: in a 100-member group, one message can generate 100 receipt rows. Two common designs:
last_read_message_id per member-- Mark conversation read up to a message (one cheap UPDATE per user)
UPDATE conversation_members
SET last_read_message_id = 9000123
WHERE conversation_id = 42 AND user_id = 7;
-- Unread count for a user in a conversation
SELECT COUNT(*) AS unread
FROM messages m
JOIN conversation_members cm
ON cm.conversation_id = m.conversation_id AND cm.user_id = 7
WHERE m.conversation_id = 42
AND m.id > COALESCE(cm.last_read_message_id, 0);
-- For exact "seen by" in groups, an explicit receipts table (partitioned)
CREATE TABLE message_reads (
message_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
read_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (message_id, user_id)
) ENGINE=InnoDB;
conversation_id — all of a chat's messages live together.direct_key like LEAST(u1,u2) || '-' || GREATEST(u1,u2) with a UNIQUE constraint. That guarantees exactly one direct conversation per pair, regardless of who starts it — and makes "find or create" a single indexed lookup.