【问题标题】:Large SQL transaction: runs out of memory on PostgreSQL, yet works on SQL Server大型 SQL 事务:在 PostgreSQL 上内存不足,但在 SQL Server 上有效
【发布时间】:2011-09-08 11:11:08
【问题描述】:

我决定将我的 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();

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

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

有没有一种方法/选项可以促进查询计划重用,多个查询具有相同的结构但不同的参数值?

更新

我决定研究最简单的情况——直接在 DBMS 上运行 SQL 批处理,而不使用 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 -> 执行)——保持内存使用平稳

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 的回答是正确的 :)

感谢所有参与的人!

【问题讨论】:

  • 为什么要从 SQL Server 迁移出去?
  • 您也可以在 dba.stackexchange.com 上尝试您的问题
  • 如果没有进行 SELECT、UPDATE 或 DELETE,为什么不能将其拆分为多个事务?
  • @svick: 1) TCO 2) 在 Linux 上运行的能力(安全性;一些最好的专用服务器提供商使用 Linux)
  • @letitbee:自然,真正的数据库远大于2个表。我将其剥离为仅带有 INSERT 的两个表,发现内存占用 still 发生 - 所以我将此用作问题描述。但是,在真实的数据库中会发生很多事情,并且不能接受拆分为多个事务。

标签: sql sql-server postgresql dotconnect


【解决方案1】:

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

【讨论】:

  • 文档说“该设置只需要大到足以容纳一个典型事务生成的 WAL 数据量”(强调“典型”)。更大的事务只会将更多的 WAL 写入磁盘,所以这根本不是这个问题的一部分。
【解决方案2】:

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

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

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

【讨论】:

  • @Proglamer:如果您正在执行批量插入,请考虑使用 PgSql COPY 命令或 PgSqlLoader 构造。
【解决方案3】:
  1. 我完全同意 Frank。

  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

【讨论】:

    猜你喜欢
    • 2020-01-20
    • 2016-12-08
    • 2016-12-31
    • 2012-06-11
    • 2015-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多