LINUX.ORG.RU

Rails, кэширование/«прогрев» частых запросов к БД.

 ,


0

1

Есть очень большая таблица с «сырыми» данными. Обработанные данные (статистика, кое-какие расчёты) хранятся в отдельных таблицах, и обновляются каждые несколько минут; за этот счёт обеспечивается приемлемая скорость выполнения запросов.

Но при всём этом нужно делать прямые запросы к основной таблице (с «сырыми» данными).

Запросы будут очень частыми, но простыми, типа: «выдать (все) 20 записей такого-то юзера за прошлый час».

«Прогрев» для статистики я сделал, как выше указано, но тут он не поможет.

Хотелось бы как-то оптимизировать это счастье.


table-based partitioning, postgresql, table inheritance, partition solver constraints. Вот эти штуки позволят тебе сделать хорошо. EXPLAIN мне, например, говорит, что это именно так.

GateKeeper ★★
()
Ответ на: комментарий от GateKeeper

Но в Rails есть кэширование частых запросов. Мне бы просто ссылку на мануал, сдобренную словами «я делал, брат не умер».

daris
() автор топика
Ответ на: комментарий от daris

На мануал слишком много, проще идти на сайт посгреса и читать. На пальцах выглядит так:

Создаешь пустую таблицу для сырых данных. Индексов, констрейнтов на нее не вешай, бесполезно (она все равно останется пустой). Вешай на нее триггер на INSERT (это я исхожу из того, что сырые данные у тебя не меняются, если меняются - два триггера, на инсерт и апдейт). Триггером разруливаешь по полям в NEW, в какую конкретно партицию пихать (я, например, понедельно бью на таблицы), там вычисляется имя реальной таблицы (например, бьешь по пользователям, таблица data, пользователь с id = 1 пойдет в таблицу data_1), пихаешь в вычисленную таблицу. Далее хитрый трюк: инсерт в триггере заверни в exception, что-то типа

insert into data_1 blabla
exception when table_not_found
    create table data_1 inherits data
    create constraint user_id = 1
    create index blabla
    insert into data_1 blabla
все. Бить можно по нескольким полям сразу, таблиц будет много, но работать будет быстро. Одно но: агрегатные функции в констрейнтах использовать нельзя, иначе select from data where blabla будет использовать перебор всех таблиц (или индексов). Более того, если у тебя поле с типом timestamp, то unix_timestamp тоже нельзя указывать, только время в формате timestamp. Вычисляется в триггере при создании. Справишься. Подробности и мой пример смогу показать не ранее среды, когда на работе появлюсь. Напомни в среду, если надо будет.

GateKeeper ★★
()
Ответ на: комментарий от GateKeeper

ты не понял, он хочет не на уровне БД, а на уровне приложения это сделать.
То, что в хибернейте/эклиспслинке давным-давно уже есть (кеши двух уровней), в рельсах, видимо, так просто не делается

JFreeM ★★★☆
()
Ответ на: комментарий от GateKeeper

Спасибо, но слишко навороченно, пока я сам в этом не разобрался, рисковать не буду. Проще вторую машину поставить.

daris
() автор топика
Ответ на: комментарий от JFreeM

В рельсах есть несколько видов кэшей, но часть, работающая с кэшами БД, плохо документирована (была, когда смотрел).

daris
() автор топика
Ответ на: комментарий от daris

А какой-нибудь memcache(d) делу не поможет?

daris
() автор топика
Ответ на: комментарий от baverman

Очень интересно. А выкину лучше я всё, кроме хвоста этой таблицы. Раз в неделю «отделение» неиспользуемых старых данных.

А почему ты уверен что кэши не спасут? Мне бы запросы одного вида держать в памяти по 5-10 минут, это бы помогло.

daris
() автор топика
Ответ на: комментарий от daris

тебя спасёт только просчитанный план исполнения, что можно получить только навтрыкав грамотных prepared statements (что в общем-то рельсы наверняка и делают, просто не дропают их сразу после использования).

P.S. предлагаю partitioning по часам со сбрасыванием архивов в другую таблицу, хотя если у тебя mysql то будет печальненько, т.к. у него с кол-вом партиций грустно.

qnikst ★★★★★
()

Вот, как обещал, если тебе это еще интересно:

CREATE OR REPLACE FUNCTION monitor_partition()
  RETURNS trigger AS
$BODY$DECLARE
	tablename varchar;
	tsyear int;
	tsweek int;
BEGIN
	tsyear := extract(year from to_timestamp(NEW.time))::int;
	tsweek := extract(week from to_timestamp(NEW.time))::int;
	tablename := 'monitor_y'
	|| tsyear::varchar
	|| '_w'
	|| tsweek::varchar;
	BEGIN
		EXECUTE 'INSERT INTO '
		|| tablename
		|| ' VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)'
		USING
			NEW.time,
			NEW.flowt,
			NEW.unit_oid,
			NEW.proto,
			NEW.src,
			NEW.srcport,
			NEW.dst,
			NEW.dstport,
			NEW.if_in,
			NEW.if_out,
			NEW.as_src,
			NEW.as_dst,
			NEW.dpkts,
			NEW.len,
			NEW.layer7;
		EXCEPTION
			WHEN undefined_table THEN
				EXECUTE 'CREATE TABLE '
				|| tablename
				|| '( CHECK ( '
				||	' time >= ' || weekstart_unixtimestamp(tsyear, tsweek)::varchar
				|| 	' AND '
				||	' time < ' || nextweekstart_unixtimestamp(tsyear, tsweek)::varchar
				|| ')) INHERITS (monitor)';
				EXECUTE 'CREATE INDEX ' || tablename || '_time_idx ON ' || tablename || ' USING btree ("time")';
				EXECUTE 'CREATE INDEX ' || tablename || '_unit_oid_idx ON ' || tablename || ' USING btree (unit_oid)';
				EXECUTE 'INSERT INTO '
				|| tablename
				|| ' VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)'
				USING
					NEW.time,
					NEW.flowt,
					NEW.unit_oid,
					NEW.proto,
					NEW.src,
					NEW.srcport,
					NEW.dst,
					NEW.dstport,
					NEW.if_in,
					NEW.if_out,
					NEW.as_src,
					NEW.as_dst,
					NEW.dpkts,
					NEW.len,
					NEW.layer7;
	END;
	RETURN NULL;
END;$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;
Результат работы триггера:
CREATE TABLE monitor_y2012_w38
(
-- Унаследована from table monitor:  "time" integer NOT NULL,
-- Унаследована from table monitor:  flowt integer,
-- Унаследована from table monitor:  unit_oid integer,
-- Унаследована from table monitor:  proto smallint,
-- Унаследована from table monitor:  src bigint NOT NULL,
-- Унаследована from table monitor:  srcport integer,
-- Унаследована from table monitor:  dst bigint NOT NULL,
-- Унаследована from table monitor:  dstport integer,
-- Унаследована from table monitor:  if_in integer,
-- Унаследована from table :  if_out integer,
-- Унаследована from table :  as_src integer,
-- Унаследована from table :  as_dst integer,
-- Унаследована from table :  dpkts bigint,
-- Унаследована from table :  len bigint,
-- Унаследована from table :  layer7 character varying(80),
  CONSTRAINT monitor_y2012_w38_time_check CHECK ("time" >= 1347825600 AND "time" < 1348430400)
)
INHERITS (monitor)
WITH (
  OIDS=FALSE
);
CREATE INDEX monitor_y2012_w38_time_idx
  ON monitor_y2012_w38
  USING btree
  ("time" );

Это postgres.

explain select * from monitor where "time" >= 1347825600;
 Result  (cost=0.00..731294.96 rows=27188718 width=71)
   ->  Append  (cost=0.00..731294.96 rows=27188718 width=71)
         ->  Seq Scan on monitor  (cost=0.00..0.00 rows=1 width=71)
               Filter: ("time" >= 1347825600)
         ->  Seq Scan on monitor_y2012_w38 monitor  (cost=0.00..731294.96 rows=27188717 width=71)
               Filter: ("time" >= 1347825600)

Насчет seq scan - это у меня, кажется, индексы в pgadmin отключены. Заметь, меня не парит, из какой таблицы выбирать данные, я делаю селект из родительской, посгрес автоматом аппендит наследников.

GateKeeper ★★
()
Ответ на: комментарий от special-k

Хм... вот в этм мануале вообще ничего про кэширование запросов. Есть кэш страничек, но это не то. Извини.

daris
() автор топика
Ответ на: комментарий от Apple-ch

Тут можно кэшировать запросы? Не нашёл как. Я думал лучше разобраться с этим на уровне БД.

daris
() автор топика
Ответ на: комментарий от GateKeeper

У меня мало знаний чтобы это поддерживать, не могу взять незнакомый код. Но идею уже копаю. Спасибо, в общем :)

daris
() автор топика
Ответ на: комментарий от daris

Я как раз идею кинул, брать мой код 1-в-1 было бы глупо и бесполезно. В общем, велкам.

GateKeeper ★★
()
Ответ на: комментарий от daris

key-value store есть в рельсах - сохраняешь что хочешь, так долго как хочешь (если тебе это о чем-то скажет). Нет, запросы оно не кеширует лол.

special-k ★★★
()
Ответ на: комментарий от daris

Почти все движки и обертки к базе поддерживают так или иначе в итоге prepare. Для БД, года Вы пишите конструкцию вида:

SELECT * FROM DATA WHERE ID=12345;
...
SELECT * FROM DATA WHERE ID=54321;
...

это не однотипные запросы, на них будут выделятся отдельные ресурсы (в некоторых СУБД отдельный процесс)

С другой стороны: сделав однажды prepare выполняем один и тот-же запрос только меняя параметры. Примерно будет так выглядеть:

db.prepare('SELECT * FROM DATA WHERE ID=?;')
...
db.execute(12345);
...
db.execute(54321);

swwwfactory ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.