← Назад к вопросам
Как с запросом SELECT определить изменилось что-либо или нет?
2.0 Middle🔥 172 комментариев
#JavaScript Core
Комментарии (2)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
SELECT запросы и определение изменений: стратегии оптимизации
Этот вопрос касается оптимизации: как минимизировать трафик и нагрузку на сервер, когда нужно узнать, изменились ли данные. Это критично для кэширования, синхронизации и performance. Разбираю несколько подходов.
1. HTTP Conditional Requests (ETag и Last-Modified)
Это встроенный в HTTP механизм для проверки изменений БЕЗ передачи всех данных:
// Первый запрос
const firstFetch = async () => {
const response = await fetch('/api/posts');
// Сервер отправляет уникальный идентификатор
const etag = response.headers.get('ETag'); // "w/123abc"
const lastModified = response.headers.get('Last-Modified'); // дата
// Сохраняем для следующего запроса
localStorage.setItem('posts_etag', etag);
localStorage.setItem('posts_modified', lastModified);
return response.json();
};
// Второй запрос - проверяем без загрузки данных
const checkIfChanged = async () => {
const etag = localStorage.getItem('posts_etag');
const response = await fetch('/api/posts', {
headers: {
'If-None-Match': etag // "если тот же ETag"
}
});
if (response.status === 304) {
// Not Modified - данные не изменились!
console.log('Данные в кэше актуальны');
return null; // используй кэшированные данные
} else if (response.status === 200) {
// Данные изменились - загружаем новые
const newEtag = response.headers.get('ETag');
localStorage.setItem('posts_etag', newEtag);
return response.json();
}
};
// Практический пример в React
function useCachedData(url) {
const [data, setData] = useState(null);
const fetchData = useCallback(async () => {
const cached = localStorage.getItem(`data_${url}`);
const etag = localStorage.getItem(`etag_${url}`);
const response = await fetch(url, {
headers: etag ? { 'If-None-Match': etag } : {}
});
if (response.status === 304) {
// Не изменилось, используем кэш
setData(JSON.parse(cached));
} else {
// Изменилось или первый раз
const newData = await response.json();
const newEtag = response.headers.get('ETag');
localStorage.setItem(`data_${url}`, JSON.stringify(newData));
localStorage.setItem(`etag_${url}`, newEtag);
setData(newData);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
return data;
}
2. Версионирование: добавить колонку версии в БД
Для более контролируемого подхода добавляем версию к данным:
-- В схеме БД
CREATE TABLE posts (
id UUID PRIMARY KEY,
title TEXT,
content TEXT,
updated_at TIMESTAMPTZ,
version INT, -- версия содержимого
CHECK (version >= 0)
);
-- Таблица для отслеживания последней версии по типу ресурса
CREATE TABLE resource_versions (
resource_type VARCHAR,
resource_id UUID,
current_version INT,
PRIMARY KEY (resource_type, resource_id)
);
// На фронтенде
const checkVersion = async () => {
// Отправляем текущую версию, которая у нас
const myVersion = localStorage.getItem('posts_version');
const response = await fetch('/api/posts/version', {
method: 'GET',
headers: {
'X-Current-Version': myVersion || '0'
}
});
const { version, hasChanges, data } = await response.json();
if (!hasChanges) {
// Версия та же - ничего не изменилось
console.log('Используем кэш');
return null;
}
// Версия изменилась - обновляем
localStorage.setItem('posts_version', version);
return data;
};
// API endpoint на сервере
app.get('/api/posts/version', (req, res) => {
const clientVersion = req.headers['x-current-version'] || '0';
const currentVersion = db.query(
'SELECT current_version FROM resource_versions WHERE resource_type = ?',
['posts']
);
if (currentVersion === clientVersion) {
// Одинаковые версии
res.json({ hasChanges: false, version: currentVersion });
} else {
// Версии различаются - отправляем свежие данные
const posts = db.query('SELECT * FROM posts');
res.json({
hasChanges: true,
version: currentVersion,
data: posts
});
}
});
3. Hash/Checksum подход
Если нет встроенной версии - вычисляем хеш данных:
// На сервере
const crypto = require('crypto');
app.get('/api/posts', (req, res) => {
const posts = db.query('SELECT * FROM posts');
const dataString = JSON.stringify(posts);
// Вычисляем SHA-256 хеш данных
const hash = crypto
.createHash('sha256')
.update(dataString)
.digest('hex');
res.set('X-Data-Hash', hash);
// Проверяем, если клиент отправил хеш
const clientHash = req.headers['x-expected-hash'];
if (clientHash === hash) {
res.status(304).send(); // Not Modified
} else {
res.json(posts);
}
});
// На фронтенде
const fetchWithHash = async () => {
const lastHash = localStorage.getItem('posts_hash');
const response = await fetch('/api/posts', {
headers: lastHash ? { 'X-Expected-Hash': lastHash } : {}
});
if (response.status === 304) {
// Хеш совпадает - данные не изменились
return null;
}
const data = await response.json();
const newHash = response.headers.get('X-Data-Hash');
localStorage.setItem('posts_hash', newHash);
return data;
};
4. Polling с минимальной нагрузкой
Регулярная проверка, но с оптимизацией:
// Endpoint для быстрой проверки
app.get('/api/posts/check', (req, res) => {
// Возвращаем ТОЛЬКО информацию об изменениях
// Не весь контент!
const version = db.query(
'SELECT version FROM resource_versions WHERE type = ?',
['posts']
);
const count = db.query('SELECT COUNT(*) FROM posts')[0].count;
const lastModified = db.query(
'SELECT MAX(updated_at) FROM posts'
)[0].max;
res.json({
version: version,
count: count,
lastModified: lastModified,
size: '2.5MB' // размер полного ответа
});
});
// На фронтенде
const usePollCheck = (url, interval = 30000) => {
const [needsRefresh, setNeedsRefresh] = useState(false);
useEffect(() => {
const storedVersion = localStorage.getItem(`${url}_version`);
const checkFn = async () => {
const response = await fetch(`${url}/check`);
const metadata = await response.json();
if (metadata.version !== storedVersion) {
setNeedsRefresh(true);
}
};
const timer = setInterval(checkFn, interval);
return () => clearInterval(timer);
}, [url, interval]);
return needsRefresh;
};
5. WebSocket для real-time уведомлений
Для данных, которые меняются часто - реактивный подход:
// На сервере (Node.js + WebSocket)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
let currentVersion = 1;
wss.on('connection', (ws) => {
// Отправляем текущую версию при подключении
ws.send(JSON.stringify({ type: 'version', version: currentVersion }));
// Слушаем обновления БД (через PubSub или polling БД)
const checkUpdates = setInterval(() => {
const newVersion = db.query(
'SELECT version FROM resource_versions WHERE type = ?',
['posts']
);
if (newVersion > currentVersion) {
currentVersion = newVersion;
// Оповещаем всех клиентов об изменении
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'changed',
version: newVersion
}));
}
});
}
}, 5000);
ws.on('close', () => clearInterval(checkUpdates));
});
// На фронтенде
const useRealTimeUpdates = (url) => {
const [data, setData] = useState(null);
const [needsRefresh, setNeedsRefresh] = useState(false);
useEffect(() => {
const ws = new WebSocket('ws://api.example.com:8080');
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'changed') {
// Данные изменились - пора обновить
setNeedsRefresh(true);
}
};
return () => ws.close();
}, []);
return { data, needsRefresh };
};
6. Database triggers для отслеживания изменений
В PostgreSQL можно использовать triggers:
-- Таблица для отслеживания версий
CREATE TABLE change_log (
id SERIAL PRIMARY KEY,
resource_type VARCHAR,
resource_id UUID,
changed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
version INT
);
-- Trigger для отслеживания изменений
CREATE FUNCTION update_version()
RETURNS TRIGGER AS $$
BEGIN
NEW.version = NEW.version + 1;
NEW.updated_at = CURRENT_TIMESTAMP;
INSERT INTO change_log (resource_type, resource_id, version)
VALUES ('posts', NEW.id, NEW.version);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER posts_update_trigger
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION update_version();
7. Сравнение подходов
const approaches = {
// 1. ETag/Last-Modified
etag: {
трафик: 'Минимальный (только заголовки при 304)',
задержка: 'Одна задержка на проверку',
сложность: 'Просто (встроено в HTTP)',
реактивность: 'Не реактивно (polling)'
},
// 2. Версионирование
version: {
трафик: 'Очень минимальный (одно число)',
задержка: 'Одна задержка',
сложность: 'Просто (простая логика)',
реактивность: 'Не реактивно'
},
// 3. WebSocket
websocket: {
трафик: 'Минимальный (только уведомления)',
задержка: 'Минимальная (push, не poll)',
сложность: 'Сложнее (нужен сервер)',
реактивность: 'Реактивно (push уведомления)'
},
// 4. Polling
polling: {
трафик: 'Может быть большим',
задержка: 'Зависит от интервала',
сложность: 'Просто',
реактивность: 'Низкая реактивность'
}
};
// Выбор подхода
const chooseApproach = () => {
const scenarios = {
staticContent: 'ETag + HTTP кэширование',
frequentUpdates: 'WebSocket для real-time',
periodicalChecks: 'Версионирование + polling',
manySmallUpdates: 'WebSocket + incremental sync'
};
};
8. Практический пример для PrepBro
// На PrepBro: вопросы редко меняются, но коммент часто
const useCachedQuestions = () => {
const [questions, setQuestions] = useState(null);
useEffect(() => {
const loadQuestions = async () => {
// Версионирование для вопросов
const cachedVersion = localStorage.getItem('questions_version');
const response = await fetch('/api/questions/version', {
headers: {
'X-Current-Version': cachedVersion || '0'
}
});
const { hasChanges, data, version } = await response.json();
if (!hasChanges && cachedVersion) {
// Используем кэш
setQuestions(JSON.parse(localStorage.getItem('questions_cache')));
} else {
// Обновляем
setQuestions(data);
localStorage.setItem('questions_cache', JSON.stringify(data));
localStorage.setItem('questions_version', version);
}
};
loadQuestions();
}, []);
return questions;
};
// Для комментариев - WebSocket для real-time
const useRealtimeComments = (questionId) => {
const [comments, setComments] = useState([]);
useEffect(() => {
const ws = new WebSocket('ws://api.example.com:8080');
ws.send(JSON.stringify({
type: 'subscribe',
topic: `question:${questionId}:comments`
}));
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'new_comment') {
setComments(prev => [...prev, message.comment]);
}
};
return () => ws.close();
}, [questionId]);
return comments;
};
Итог: когда что использовать
- ETag/Last-Modified: Статические данные, простая кэширование
- Версионирование: Бизнес-данные, простая проверка
- Polling версии: Периодическая проверка без реактивности
- WebSocket: Real-time данные, частые обновления
- Комбинация: Версия + WebSocket для гибридного подхода
Ответ: SELECT запрос + условная проверка (ETag или версия) = узнаёшь об изменениях без передачи всех данных.