← Назад к вопросам

Как с запросом 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 или версия) = узнаёшь об изменениях без передачи всех данных.