Структура базы данных для доски объявлений (BBS) на PHP
Проектирование таблиц для BBS на PHP
Основное эффективное решение
Для доски объявлений оптимальна нормализованная схема с разделением на таблицы: users, categories, threads, posts. Это обеспечивает целостность данных, удобство запросов и масштабирование.
Как создать базовые таблицы для форума с категориями и темами?
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT
);
CREATE TABLE threads (
id INT AUTO_INCREMENT PRIMARY KEY,
category_id INT NOT NULL,
user_id INT NOT NULL,
title VARCHAR(200) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_category (category_id),
INDEX idx_user (user_id)
);
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
thread_id INT NOT NULL,
user_id INT NOT NULL,
parent_id INT DEFAULT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES posts(id) ON DELETE SET NULL,
INDEX idx_thread (thread_id),
INDEX idx_parent (parent_id)
);Bbs php table (таблицы для bbs (доски объявлений) на php)
Пояснение: таблица posts использует parent_id для вложенных ответов (adjacency list). Индексы ускоряют выборку по темам и родительским сообщениям.
Типичные ошибки:
- Забывают про индексы на внешние ключи, что приводит к медленным запросам при росте данных.
- Использование ON DELETE CASCADE на parent_id может привести к потере дочерних записей при удалении родительской. В примере использовано SET NULL, чтобы сохранить цепочку, или можно запретить удаление с проверкой.
- Отсутствие уникального ключа на username или email вызывает дубликаты пользователей.
Как хранить иерархию сообщений без рекурсивных запросов?
Вариант nested sets (вложенные множества) позволяет получать все подсообщения одним SELECT. Схема:
CREATE TABLE posts_nested (
id INT AUTO_INCREMENT PRIMARY KEY,
thread_id INT NOT NULL,
user_id INT NOT NULL,
content TEXT,
lft INT NOT NULL,
rgt INT NOT NULL,
FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE,
INDEX idx_lft_rgt (lft, rgt)
);Запрос для получения всех ответов на сообщение: SELECT * FROM posts_nested WHERE lft > :lft AND rgt < :rgt ORDER BY lft.
Проблема: сложность вставки и перемещения сообщений, требуется пересчёт lft/rgt. Подходит для статичных или редко изменяемых данных.
Какие ещё подходы существуют для организации сообщений?
Материализованный путь (materialized path) - хранение полного пути в строке, например '1/3/7/. Запрос на потомков: LIKE '1/3/%'.
CREATE TABLE posts_mpath (
id INT AUTO_INCREMENT PRIMARY KEY,
thread_id INT NOT NULL,
user_id INT NOT NULL,
content TEXT,
path VARCHAR(255) NOT NULL,
INDEX idx_path (path(255))
);Вставка: новый path = parent_path + id + '/'.
Проблема: размер строки может расти, неэффективные LIKE запросы при большом количестве уровней. Индекс на VARCHAR работает только для префикса.
Что делать, если нужно быстро получать количество сообщений в теме?
Денормализация: добавить поле posts_count в таблицу threads и обновлять его триггерами или при каждом добавлении/удалении поста.
ALTER TABLE threads ADD COLUMN posts_count INT DEFAULT 0;
-- и триггер или код PHP для обновленияПроблема: риск рассинхронизации данных. Решение: периодически запускать скрипт для пересчёта.
Расширенные примеры работы с таблицами BBS
Как создать полнотекстовый поиск по сообщениям?
Добавление FULLTEXT индекса на поле content таблицы posts.
ALTER TABLE posts ADD FULLTEXT INDEX ft_content (content);
-- Поиск:
SELECT * FROM posts WHERE MATCH(content) AGAINST('php mysql' IN BOOLEAN MODE);Как вывести все сообщения темы в виде дерева с отступами (рекурсивная функция на PHP)?
$stmt = $pdo->prepare('SELECT * FROM posts WHERE thread_id = ? ORDER BY created_at');
$stmt->execute([$threadId]);
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
$tree = buildTree($posts, 0);
function buildTree(&$list, $parentId) {
$branch = [];
foreach ($list as &$item) {
if ($item['parent_id'] == $parentId) {
$children = buildTree($list, $item['id']);
if ($children) $item['children'] = $children;
$branch[] = $item;
}
}
return $branch;
}Пример результата (массив):
[
{
"id": 1,
"content": "Первый пост",
"children": [
{
"id": 2,
"content": "Ответ",
"children": []
}
]
}
]Как пагинировать темы с учётом количества ответов?
Использование подзапроса для подсчёта постов в теме.
SELECT t.*, (SELECT COUNT(*) FROM posts WHERE posts.thread_id = t.id) as post_count
FROM threads t
WHERE t.category_id = :category_id
ORDER BY t.created_at DESC
LIMIT :limit OFFSET :offset;Результат: таблица строк с id, title, post_count.
Как реализовать вложенные множества на PHP для вставки нового сообщения?
// Вставка нового корневого поста (первого в теме)
$pdo->exec('LOCK TABLES posts_nested WRITE;');
$maxRgt = $pdo->query("SELECT COALESCE(MAX(rgt), 0) FROM posts_nested WHERE thread_id = $threadId")->fetchColumn();
$pdo->prepare("INSERT INTO posts_nested (thread_id, user_id, content, lft, rgt) VALUES (?, ?, ?, ?, ?)")
->execute([$threadId, $userId, $content, $maxRgt + 1, $maxRgt + 2]);
$pdo->exec('UNLOCK TABLES;');
// Вставка вложенного ответа
$parent = $pdo->query("SELECT lft, rgt FROM posts_nested WHERE id = $parentId")->fetch();
$pdo->exec('LOCK TABLES posts_nested WRITE;');
$pdo->exec("UPDATE posts_nested SET rgt = rgt + 2 WHERE rgt >= ".$parent['rgt']." AND thread_id = $threadId");
$pdo->exec("UPDATE posts_nested SET lft = lft + 2 WHERE lft > ".$parent['rgt']." AND thread_id = $threadId");
$pdo->prepare("INSERT INTO posts_nested (thread_id, user_id, content, lft, rgt) VALUES (?, ?, ?, ?, ?)")
->execute([$threadId, $userId, $content, $parent['rgt'], $parent['rgt'] + 1]);
$pdo->exec('UNLOCK TABLES;');После вставки дерево сохраняет корректные значения lft/rgt.
Как получить топ-10 пользователей по количеству сообщений?
SELECT u.username, COUNT(p.id) as post_count
FROM users u
JOIN posts p ON u.id = p.user_id
GROUP BY u.id
ORDER BY post_count DESC
LIMIT 10;Результат: список имён и количество сообщений.
Как избежать потери данных при удалении пользователя?
Использовать ON DELETE SET NULL на внешние ключи в таблице posts (user_id), а затем в коде PHP при выводе показывать 'Пользователь удалён'.
ALTER TABLE posts MODIFY COLUMN user_id INT NULL;
-- Удалить старый внешний ключ и добавить новый:
ALTER TABLE posts DROP FOREIGN KEY fk_posts_user;
ALTER TABLE posts ADD CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;