我决定将我的 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 的回答是正确的 :)
感谢所有参与的人!