17

我决定将我的 C# 守护程序应用程序(使用 dotConnect 作为 ADO.NET 提供程序)从 SQL Server 2008 R2 移动到 PostgreSQL 9.0.4 x64(在 Windows Server 2008 R2 上)。因此,我稍微修改了所有查询以匹配 PostgreSQL 语法,并且......陷入了 SQL Server 上相同查询从未发生过的行为(甚至在低速版上也没有)。

假设数据库包含 2 个非常简单的表,彼此之间没有任何关系。它们看起来有点像这样:ID、名称、型号、ScanDate、Notes。我有一个转换过程,它通过 TCP/IP 读取数据、处理它、启动事务并使用 vanilla INSERT 将结果放入上述 2 个表中。表格最初是空的;没有 BLOB 列。糟糕的一天大约有 500.000 个 INSERT,全部包含在单个事务中(顺便说一句,不能拆分为多个事务)。从未进行过 SELECT、UPDATE 或 DELETE。INSERT 的一个例子(ID 是 bigserial - autoincremented 自动):

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

SQL Server 平静地接受负载,同时保持约 200 MB 的合理工作集。然而,PostgreSQL 每秒事务运行会占用额外的 30 MB (!) 并迅速耗尽系统 RAM。

我已经完成了我的 RTFM 并尝试摆弄 postgresql.conf:将“work_mem”设置为最小 64 kB(这稍微减慢了 RAM 占用),将“shared_buffers”/“temp_buffers”减少到最小(没有区别),-但是无济于事。将事务隔离级别降低到未提交读取并没有帮助。除了 ID BIGSERIAL (PK) 上的索引之外,没有其他索引。SqlCommand.Prepare() 没有区别。没有建立任何并发连接:守护进程独占使用数据库。

似乎 PostgreSQL 无法应对令人麻木的简单 INSERT-fest,而 SQL Server 可以做到这一点。也许这是 PostgreSQL 快照与 SQL Server 锁隔离的区别?对我来说这是一个事实:原版 SQL Server 可以工作,而原版和经过调整的 PostgreSQL 都不能。

在基于 INSERT 的事务运行时,我可以做些什么来使 PostgreSQL 内存消耗保持平稳(显然是 SQL Server 的情况)?

编辑:我创建了一个人工测试用例:

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();

这会重新创建内存占用。但是:如果在 FOR 循环之外创建了“cmd”变量并且 .Prepare()d ,则内存不会增加!显然,用相同的 SQL 准备多个 PgSqlCommands 但不同的参数值不会在 SQL Server 中那样在 PostgreSQL 中产生单个查询计划。

问题依然存在:如果使用 Fowler 的 Active Record dp 插入多个新对象,则准备好的 PgSqlCommand 实例共享并不优雅。

有没有办法/选项来促进查询计划重用具有相同结构但不同参数值的多个查询?

更新

我决定看看最简单的情况——SQL 批处理直接在 DBMS 上运行,没有 ADO.NET(由 Jordani 建议)。令人惊讶的是,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 -> Execute)——保持内存使用平稳

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 驱动程序替代

正如 Jordani 建议的那样,我尝试了 NpgSql 驱动程序而不是 dotConnect - 结果相同(缺乏)。但是, .Prepare() 方法的 Npgsql 源代码包含以下启发性的行:

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"

在这段日志摘录中,效率低下非常明显......

结论(例如它们)

Frank 关于 WAL 的注释是另一个觉醒:SQL Server 对典型的 MS 开发人员隐藏的其他配置。

NHibernate(即使在其最简单的用法中)正确地重用准备好的 SqlCommands...如果从一开始就使用它...

很明显,SQL Server 和 PostgreSQL 之间存在架构差异,并且专门为SQL Server 构建的代码(因此幸福地没有意识到“无法重用相同 sql”的可能性)在 PostgreSQL 上不会有效地工作重构。并且重构 130 多个旧 ActiveRecord 类以在杂乱的多线程中间件中重用准备好的 SqlCommand 对象并不是“只需替换 dbo-with-public”类型的事情。

不幸的是,对于我的加班,Eevar 的回答是正确的 :)

感谢所有参与的人!

4

3 回答 3

9

减少 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

于 2011-06-04T18:59:55.603 回答
8

我怀疑你自己想通了。您可能正在创建 500k 不同的准备好的语句、查询计划等等。实际上,情况比这更糟;准备好的语句存在于事务边界之外并一直持续到连接关闭。像这样滥用它们会消耗大量内存。

如果您想多次执行查询但避免每次执行的计划开销,请创建一个准备好的语句并使用新参数重用它。

如果您的查询是唯一的和临时的,只需使用 postgres 对绑定变量的正常支持;不需要准备好的语句的额外开销。

于 2011-06-05T00:04:24.257 回答
1
  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 服务器提供程序工作,则不好(已修复最新版本/错误)。您可以使用其他提供商吗?

您必须检查谁“正在吃”内存 - 数据库服务器或提供者。如果生成sql脚本和“psql.exe”也可以测试PostgreSql

于 2011-06-05T00:01:48.963 回答