«Настройка» без 10-секундного ожидания: статистика npr.history по первичному ключу
Каждый рендер «Настройки» ждал /section службы bj-quikexport, а та считала min(ts) и count(*) за сутки по npr.history (18,5 млн строк, индекса по ts нет): 6 + 27 с на 28, срез по таймауту 10 с. Теперь начало истории — первая строка по id, строки за сутки/час — двоичный поиск по id (история только дописывается): ~45 мс. То же в /admin/npr/api/freshness. bj-server ждёт раздел QUIK-экспорта не дольше 3 с и иначе отдаёт последний удачный; прогрев версий, пользователей ЛК и раздела после запуска. «Обновления» перезагружают страницу, только если что-то изменилось. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
+31
-1
@@ -454,7 +454,18 @@ func (s *Server) Handler() http.Handler {
|
||||
_ = p.QueryRow(ctx, "SELECT last_tick, tick_count FROM npr.heartbeat WHERE id=1").Scan(&lastTick, &ticks)
|
||||
var lastTS *time.Time
|
||||
var total, hour, day int64
|
||||
_ = p.QueryRow(ctx, `SELECT max(ts), count(*), count(*) FILTER (WHERE ts >= NOW() - INTERVAL '1 hour'), count(*) FILTER (WHERE ts >= NOW() - INTERVAL '24 hours') FROM npr.history`).Scan(&lastTS, &total, &hour, &day)
|
||||
// По первичному ключу, без полного прохода npr.history (десятки миллионов строк, индекса по ts нет):
|
||||
// последнее событие — max(id), всего — оценка планировщика, за час/сутки — двоичный поиск по id.
|
||||
var minID, maxID *int64
|
||||
_ = p.QueryRow(ctx, `SELECT (SELECT ts FROM npr.history ORDER BY id DESC LIMIT 1), COALESCE((SELECT reltuples::bigint FROM pg_class WHERE oid='npr.history'::regclass),0), (SELECT min(id) FROM npr.history), (SELECT max(id) FROM npr.history)`).Scan(&lastTS, &total, &minID, &maxID)
|
||||
if minID != nil && maxID != nil {
|
||||
now := time.Now()
|
||||
hour = *maxID - nprHistIDAt(ctx, p, now.Add(-time.Hour), *minID, *maxID+1) + 1
|
||||
day = *maxID - nprHistIDAt(ctx, p, now.Add(-24*time.Hour), *minID, *maxID+1) + 1
|
||||
if total < day {
|
||||
total = day
|
||||
}
|
||||
}
|
||||
out := map[string]any{"last_tick": nil, "tick_count": ticks, "last_event_ts": nil, "total_rows": total, "events_last_hour": hour, "events_last_24h": day, "server_now": time.Now().Format(time.RFC3339)}
|
||||
if lastTick != nil {
|
||||
out["last_tick"] = lastTick.Format(time.RFC3339)
|
||||
@@ -582,3 +593,22 @@ func (s *Server) Handler() http.Handler {
|
||||
})
|
||||
return modkit.RequireKey(s.cfg.KeyPath, mux)
|
||||
}
|
||||
|
||||
// nprHistIDAt — первый id в npr.history со временем не раньше t. История только дописывается (id растёт
|
||||
// вместе с ts), поэтому двоичный поиск по первичному ключу: ~25 коротких запросов вместо полного прохода
|
||||
// таблицы (индекса по ts нет). lo, hi — min(id) и max(id).
|
||||
func nprHistIDAt(ctx context.Context, p *pgxpool.Pool, t time.Time, lo, hi int64) int64 {
|
||||
for lo < hi {
|
||||
mid := lo + (hi-lo)/2
|
||||
var ts time.Time
|
||||
if err := p.QueryRow(ctx, `SELECT ts FROM npr.history WHERE id >= $1 ORDER BY id LIMIT 1`, mid).Scan(&ts); err != nil {
|
||||
return hi
|
||||
}
|
||||
if ts.Before(t) {
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
return lo
|
||||
}
|
||||
|
||||
@@ -920,6 +920,25 @@ func quikExportPool(s QuikExportSettings) (*pgxpool.Pool, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// nprHistIDAt — первый id в npr.history со временем не раньше t. История только дописывается (id растёт
|
||||
// вместе с ts), поэтому двоичный поиск по первичному ключу: ~25 коротких запросов вместо полного прохода
|
||||
// таблицы (индекса по ts нет). lo, hi — min(id) и max(id).
|
||||
func nprHistIDAt(ctx context.Context, p *pgxpool.Pool, t time.Time, lo, hi int64) int64 {
|
||||
for lo < hi {
|
||||
mid := lo + (hi-lo)/2
|
||||
var ts time.Time
|
||||
if err := p.QueryRow(ctx, `SELECT ts FROM npr.history WHERE id >= $1 ORDER BY id LIMIT 1`, mid).Scan(&ts); err != nil {
|
||||
return hi
|
||||
}
|
||||
if ts.Before(t) {
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
return lo
|
||||
}
|
||||
|
||||
func quikExportStats(s QuikExportSettings) quikExportDBStats {
|
||||
st := quikExportDBStats{ExporterUser: s.DBUser}
|
||||
if s.ROPassword == "" {
|
||||
@@ -954,10 +973,18 @@ func quikExportStats(s QuikExportSettings) quikExportDBStats {
|
||||
}
|
||||
var histBytes int64
|
||||
var from *time.Time
|
||||
if err := p.QueryRow(ctx, `SELECT COALESCE((SELECT reltuples::bigint FROM pg_class WHERE oid='npr.history'::regclass),0), pg_total_relation_size('npr.history'), (SELECT min(ts) FROM npr.history), (SELECT count(*) FROM npr.history WHERE ts >= now() - interval '24 hours')`).Scan(&st.HistoryRows, &histBytes, &from, &st.Rows24h); err == nil {
|
||||
var minID, maxID *int64
|
||||
// Начало истории и строки за сутки — по первичному ключу (см. nprHistIDAt), без прохода по таблице.
|
||||
if err := p.QueryRow(ctx, `SELECT COALESCE((SELECT reltuples::bigint FROM pg_class WHERE oid='npr.history'::regclass),0), pg_total_relation_size('npr.history'), (SELECT ts FROM npr.history ORDER BY id LIMIT 1), (SELECT min(id) FROM npr.history), (SELECT max(id) FROM npr.history)`).Scan(&st.HistoryRows, &histBytes, &from, &minID, &maxID); err == nil {
|
||||
if from != nil {
|
||||
st.HistoryFrom = *from
|
||||
}
|
||||
if minID != nil && maxID != nil {
|
||||
st.Rows24h = *maxID - nprHistIDAt(ctx, p, time.Now().Add(-24*time.Hour), *minID, *maxID+1) + 1
|
||||
if st.Rows24h < 0 {
|
||||
st.Rows24h = 0
|
||||
}
|
||||
}
|
||||
if st.HistoryRows > 0 {
|
||||
st.GrowthMBDay = float64(histBytes) / float64(st.HistoryRows) * float64(st.Rows24h) / (1 << 20)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user