Большая транзакция SQL: не хватает памяти на PostgreSQL, но работает на SQL Server - PullRequest
15 голосов
/ 04 июня 2011

Я решил переместить свое приложение-демон C # (используя dotConnect в качестве поставщика ADO.NET) с SQL Server 2008 R2 на PostgreSQL 9.0.4 x64 (на Windows Server 2008 R2). Поэтому я немного изменил все запросы, чтобы они соответствовали синтаксису PostgreSQL, и ... застрял в поведении, которое никогда не происходило с такими же запросами на SQL Server (даже на слабо экспресс-выпуске).

Допустим, база данных содержит 2 очень простые таблицы, не имеющие никакого отношения друг к другу. Они выглядят примерно так: ID, Имя, Модель, ScanDate, Заметки. У меня есть процесс преобразования, который читает данные по TCP / IP, обрабатывает их, запускает транзакцию и помещает результаты в вышеупомянутые 2 таблицы, используя ванильные INSERT. Таблицы изначально пусты; нет BLOB-столбцов. В плохой день существует около 500.000 INSERT, все они заключены в одну транзакцию (кстати, их нельзя разделить на несколько транзакций). Никакие SELECT, ОБНОВЛЕНИЯ или УДАЛЕНИЯ никогда не делаются. Пример INSERT (идентификатор bigserial - автоматически инкрементируется):

INSERT INTO logs."Incoming" ("Name", "Model", "ScanDate", "Notes")
VALUES('Ford', 'Focus', '2011-06-01 14:12:32', NULL)

SQL Server спокойно принимает нагрузку, поддерживая разумный рабочий набор ~ 200 МБ. Однако PostgreSQL занимает дополнительно 30 МБ каждую секунду, когда выполняется транзакция (!), И быстро исчерпывает системную память.

Я сделал свой RTFM и попытался поиграться с postgresql.conf: установив «work_mem» на минимум 64 КБ (это немного замедлило переключение памяти), уменьшив «shared_buffers» / «temp_buffers» до минимума (без разницы) - но безрезультатно. Снижение уровня изоляции транзакции до Read Uncommitted не помогло. Там нет индексов, кроме одного на ID BIGSERIAL (PK). SqlCommand.Prepare () не имеет значения. Параллельные соединения никогда не устанавливаются: демон использует базу данных исключительно.

Может показаться, что PostgreSQL не справляется с ошеломляющим простым INSERT-фестом, в то время как SQL Server может это сделать. Может быть, это разница изоляции блокировок снимков PostgreSQL и SQL Server? Для меня это факт: ванильный SQL Server работает, в то время как ни ванильный, ни измененный PostgreSQL не работают.

Что можно сделать, чтобы потребление памяти в PostgreSQL оставалось плоским (как, очевидно, в случае с SQL Server) во время выполнения транзакции на основе INSERT?

РЕДАКТИРОВАТЬ: Я создал искусственный тестовый случай:

DDL

CREATE TABLE sometable
(
  "ID" bigserial NOT NULL,
  "Name" character varying(255) NOT NULL,
  "Model" character varying(255) NOT NULL,
  "ScanDate" date NOT NULL,
  CONSTRAINT "PK" PRIMARY KEY ("ID")
)
WITH (
  OIDS=FALSE
);

C # (требуется Devart.Data.dll и Devart.Data.PostgreSql.dll)

PgSqlConnection conn = new PgSqlConnection("Host=localhost; Port=5432; Database=testdb; UserId=postgres; Password=###########");
conn.Open();
PgSqlTransaction tx = conn.BeginTransaction(IsolationLevel.ReadCommitted);

for (int ii = 0; ii < 300000; ii++)
{
    PgSqlCommand cmd = conn.CreateCommand();
    cmd.Transaction = tx;
    cmd.CommandType = CommandType.Text;
    cmd.CommandText = "INSERT INTO public.\"sometable\" (\"Name\", \"Model\", \"ScanDate\") VALUES(@name, @model, @scanDate) RETURNING \"ID\"";
    PgSqlParameter parm = cmd.CreateParameter();
    parm.ParameterName = "@name";
    parm.Value = "SomeName";
    cmd.Parameters.Add(parm);

    parm = cmd.CreateParameter();
    parm.ParameterName = "@model";
    parm.Value = "SomeModel";
    cmd.Parameters.Add(parm);

    parm = cmd.CreateParameter();
    parm.ParameterName = "@scanDate";
    parm.PgSqlType = PgSqlType.Date;
    parm.Value = new DateTime(2011, 6, 1, 14, 12, 13);
    cmd.Parameters.Add(parm);

    cmd.Prepare();

    long newID = (long)cmd.ExecuteScalar();
}

tx.Commit();

Это воссоздает переполнение памяти. ОДНАКО: если переменная 'cmd' создана и .Prepare () d вне цикла FOR, память не увеличивается! Очевидно, что подготовка нескольких команд PgSqlCommands с IDENTICAL SQL, но с разными значениями параметров не приводит к единому плану запросов внутри PostgreSQL, как это происходит в SQL Server.

Проблема остается: если кто-то использует Active Record Фаулера для вставки нескольких новых объектов, подготовленный общий доступ к экземплярам PgSqlCommand не будет элегантным.

Есть ли способ / опция, облегчающая повторное использование плана запросов с несколькими запросами, имеющими одинаковую структуру и разные значения аргументов?

UPDATE

Я решил взглянуть на простейший возможный случай - когда пакет SQL запускается непосредственно в СУБД, без ADO.NET (предложено Джордани). Удивительно, но PostgreSQL не сравнивает входящие запросы SQL и не использует повторно внутренние скомпилированные планы - даже когда входящий запрос имеет одинаковые идентичные аргументы! Например, следующая партия:

PostgreSQL (через pgAdmin -> Выполнить запрос) - захват памяти

BEGIN TRANSACTION;

INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES('somename', 'somemodel', '2011-06-01 14:12:19');
INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES('somename', 'somemodel', '2011-06-01 14:12:19');
-- the same INSERT is repeated 100.000 times

COMMIT;

SQL Server (через Management Studio -> Выполнить) - сохраняет использование памяти на одном уровне

BEGIN TRANSACTION;

INSERT INTO [dbo].sometable ("Name", "Model", "ScanDate") VALUES('somename', 'somemodel', '2011-06-01 14:12:19');
INSERT INTO [dbo].sometable ("Name", "Model", "ScanDate") VALUES('somename', 'somemodel', '2011-06-01 14:12:19');
-- the same INSERT is repeated 100.000 times

COMMIT;

и файл журнала PostgreSQL (спасибо, Sayap!) Содержит:

2011-06-05 16:06:29 EEST LOG:  duration: 0.000 ms  statement: set client_encoding to 'UNICODE'
2011-06-05 16:06:43 EEST LOG:  duration: 15039.000 ms  statement: BEGIN TRANSACTION;

INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES('somename', 'somemodel', '2011-06-01 14:12:19');
INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES('somename', 'somemodel', '2011-06-01 14:12:19');
-- 99998 lines of the same as above
COMMIT;

Очевидно, что даже после передачи всего запроса на сервер как есть сервер не может его оптимизировать.

Альтернативный драйвер ADO.NET

Как предположил Джордани, я попробовал драйвер NpgSql вместо dotConnect - с теми же (отсутствующими) результатами. Однако исходный код Npgsql для метода .Prepare () содержит такие поучительные строки:

planName = m_Connector.NextPlanName();
String portalName = m_Connector.NextPortalName();
parse = new NpgsqlParse(planName, GetParseCommandText(), new Int32[] { });
m_Connector.Parse(parse);

Новый контент в файле журнала:

2011-06-05 15:25:26 EEST LOG:  duration: 0.000 ms  statement: BEGIN; SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
2011-06-05 15:25:26 EEST LOG:  duration: 1.000 ms  parse npgsqlplan1: INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES($1::varchar(255), $2::varchar(255), $3::date) RETURNING "ID"
2011-06-05 15:25:26 EEST LOG:  duration: 0.000 ms  bind npgsqlplan1: INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES($1::varchar(255), $2::varchar(255), $3::date) RETURNING "ID"
2011-06-05 15:25:26 EEST DETAIL:  parameters: $1 = 'SomeName', $2 = 'SomeModel', $3 = '2011-06-01'
2011-06-05 15:25:26 EEST LOG:  duration: 1.000 ms  execute npgsqlplan1: INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES($1::varchar(255), $2::varchar(255), $3::date) RETURNING "ID"
2011-06-05 15:25:26 EEST DETAIL:  parameters: $1 = 'SomeName', $2 = 'SomeModel', $3 = '2011-06-01'
2011-06-05 15:25:26 EEST LOG:  duration: 0.000 ms  parse npgsqlplan2: INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES($1::varchar(255), $2::varchar(255), $3::date) RETURNING "ID"
2011-06-05 15:25:26 EEST LOG:  duration: 0.000 ms  bind npgsqlplan2: INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES($1::varchar(255), $2::varchar(255), $3::date) RETURNING "ID"
2011-06-05 15:25:26 EEST DETAIL:  parameters: $1 = 'SomeName', $2 = 'SomeModel', $3 = '2011-06-01'
2011-06-05 15:25:26 EEST LOG:  duration: 0.000 ms  execute npgsqlplan2: INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES($1::varchar(255), $2::varchar(255), $3::date) RETURNING "ID"
2011-06-05 15:25:26 EEST DETAIL:  parameters: $1 = 'SomeName', $2 = 'SomeModel', $3 = '2011-06-01'
2011-06-05 15:25:26 EEST LOG:  duration: 0.000 ms  parse npgsqlplan3: INSERT INTO public."sometable" ("Name", "Model", "ScanDate") VALUES($1::varchar(255), $2::varchar(255), $3::date) RETURNING "ID"

Неэффективность вполне очевидна в этом отрывке из журнала ...

Выводы (такие как они)

Замечание Фрэнка о WAL - еще одно пробуждение: что-то еще, что настраивает этот SQL Server, скрывается от типичного разработчика MS.

NHibernate (даже в самом простом использовании) правильно использует подготовленные SqlCommands ... если только он использовался с самого начала ...

очевидно, что существует архитектурное различие между SQL Server и PostgreSQL и кодом , специально созданным для SQL Server (и, следовательно, блаженно не подозревающим о возможности невозможности повторного использования идентичного sql). ) не будет эффективно работать на PostgreSQL без серьезного рефакторинга. И рефакторинг 130+ устаревших классов ActiveRecord для повторного использования подготовленных объектов SqlCommand в грязном многопоточном промежуточном программном обеспечении не является делом типа «просто заменишь dbo-with-public».

К сожалению, из-за моего сверхурочного, Эивар ответит правильно:)

Спасибо всем, кто принял участие!

Ответы [ 3 ]

8 голосов
/ 04 июня 2011

Сокращение work_mem и shared_buffers не очень хорошая идея, базы данных (включая PostgreSQL) любят RAM.

Но это может быть не самой большой проблемой, а как насчет настроек WAL? wal_buffers должен быть достаточно большим, чтобы вместить всю транзакцию, все 500K INSERT. Какова текущая настройка? А что насчет checkpoint_segments?

500k INSERT не должно быть проблемой, PostgreSQL может справиться с этим без проблем с памятью.

http://www.postgresql.org/docs/current/interactive/runtime-config-wal.html

6 голосов
/ 05 июня 2011

Я подозреваю, что вы сами это поняли. Вы, вероятно, создаете 500 000 различных подготовленных операторов, планов запросов и все такое. На самом деле, это хуже, чем это; подготовленные заявления живут за пределами границ транзакции и сохраняются до тех пор, пока соединение не будет закрыто. Злоупотребление ими таким образом приведет к потере большого количества памяти.

Если вы хотите выполнить запрос несколько раз, но избежать накладных расходов на планирование для каждого выполнения, создайте один подготовленный оператор и повторно используйте его с новыми параметрами.

Если ваши запросы уникальны и уникальны, просто используйте обычную поддержку postgres для переменных связывания; нет необходимости в дополнительных затратах на подготовленные операторы.

1 голос
/ 05 июня 2011
  1. Я полностью согласен с Фрэнком.

  2. подготовленный общий доступ к экземплярам PgSqlCommand не элегантен.

Почему ?? Разве нельзя иметь внешний цикл:

    cmd = conn.CreateCommand(); 
    parm1 = cmd.CreateParameter();
    parm1.ParameterName = "@name";
    parm2 = cmd.CreateParameter();
    parm2.ParameterName = "@model";
    parm3 = cmd.CreateParameter(); 
    parm3.ParameterName = "@scanDate"; 

Также я нашел это в MSDN:

// NOTE:
// For optimal performance, make sure you always set the parameter
// type and the maximum size - this is especially important for non-fixed
// types such as NVARCHAR or NTEXT;

Если dotConnect не работает в качестве поставщика SQL-сервера, это не очень хорошо (последняя версия / исправлена ​​ошибка). Вы можете использовать другого провайдера?

Вы должны проверить, кто "ест" память - сервер БД или провайдер. Вы также можете протестировать PostgreSql, если сгенерируете скрипт sql и "psql.exe".

...