将Python的Postgres psycopg2查询性能改进到与Java的JDBC驱动程序相同的级别
概观
我试图改进我们的SQLAlchemy的数据库查询的性能。 我们正在使用psycopg2。 在我们的生产系统中,我们select使用Java,因为它至less快50%,如果不是接近100%的话。 所以我希望堆栈溢出社区有人可以提高我的performance。
我想我的下一步将是修补psycopg2库,使其像JDBC驱动程序一样工作。 如果是这样的话,有人已经这样做了,那就没事了,但是我希望我仍然可以从Python那里得到一些设置或者重构的调整。
细节
我有一个简单的“SELECT * FROM someLargeDataSetTable”查询运行。 数据集大小为GB。 一个快速的performance图如下:
时间表
logging| JDBC | SQLAlchemy [1] | SQLAlchemy [2] | PSQL -------------------------------------------------- ------------------ 1(4kB)| 200ms | 300ms | 250ms | 10毫秒 10(8kB)| 200ms | 300ms | 250ms | 10毫秒 100(88kB)| 200ms | 300ms | 250ms | 10毫秒 1,000(600kB)| 300ms | 300ms | 370ms | 100毫秒 10,000(6MB)| 800ms | 830ms | 730ms | 850ms 100,000(50MB)| 4s | 5s | 4.6s | 787-8 1,000,000(510MB)| 30s | 50s | 50s | 1分32秒 10,000,000(5.1GB)| 4m44s | 7m55s | 6m39s | N / A -------------------------------------------------- ------------------ 5,000,000(2.6GB)| 2m30s | 4m45s | 3m52s | 14m22s -------------------------------------------------- ------------------ [1] - 具有processrowfunction [2] - 没有processrow函数(直接转储)
我可以增加更多的数据(我们的数据可能高达TB),但是我认为从数据中可以看出变化的斜率。 随着数据集大小的增加,JDBC仅仅执行得更好。 有些说明…
时间表注释:
- 数据的大小是近似的,但他们应该给你一个数据量的想法。
- 我使用Linux bash命令行中的'time'工具。
- 时间是挂钟时间(即真实)。
- 我正在使用Python 2.6.6,我用
python -u
运行 - 抓取大小是10,000
- 我并不担心Psql的时机,只是作为一个参考点。 我可能没有正确设置fetch的大小。
- 我也并不担心低于获取大小的时间,因为我的应用程序less于5秒是微不足道的。
- Java和Psql似乎需要大约1GB的内存资源; Python更像100MB(耶!)。
- 我正在使用[cdecimals]库。
- 我注意到一个[最近的文章]讨论类似的东西。 看起来JDBC驱动程序devise与psycopg2devise完全不同(考虑到性能差异,我认为这是相当烦人的)。
- 我的用例基本上是我必须在非常大的数据集上运行一个日常的进程(大约有两万个不同的步骤…多个查询),而且我有一个非常特定的时间窗口,可以完成这个过程。 我们使用的Java不是简单的JDBC,它是在JDBC引擎之上的“智能”包装器…我们不想使用Java,我们想停止使用它的“智能”部分。
- 我正在使用我们的生产系统的一个盒子(数据库和后端进程)来运行查询。 所以这是我们最好的时机。 我们有质量保证和开发框运行速度慢,额外的查询时间可以变得重要。
testSqlAlchemy.py
#!/ usr / bin / env python #testSqlAlchemy.py 导入系统 尝试: 导入cdecimal sys.modules中[ “小数”] = cdecimal 除了ImportError,e: 打印>> sys.stderr,“错误:cdecimal没有正确加载” 提高SystemExit 从sqlalchemy导入create_engine 来自sqlalchemy.orm导入sessionmaker def processrow(row,delimiter =“|”,null =“\ N”): newrow = [] 对于行中的x: 如果x是None: x = null newrow.append(STR(X)) return delimiter.join(newrow) fetchsize = 10000 connectionString =“postgresql + psycopg2:// usr:pass @ server:port / db” eng = create_engine(connectionString,server_side_cursors = True) session = sessionmaker(bind = eng)() 打开(“test.sql”,“r”)作为queryFD: 打开(“/ dev / null”,“w”)为nullDev: query = session.execute(queryFD.read()) cur = query.cursor 而cur.statusmessage不在['FETCH 0','CLOSE CURSOR']: for query.fetchmany(fetchsize)中的行: 打印>> nullDev,processrow(row)
时间过后,我也跑了一个cProfile,这是最糟糕的罪犯的转储:
时序configuration文件(带进程)
星期五三月4 13:49:45 sqlAlchemy.prof 415757706在563.923 CPU秒内调用函数(415756424原始调用) 顺序:累计时间 ncalls tottime percall cumtime percall文件名:lineno(函数) 1 0.001 0.001 563.924 563.924 {execfile} 1 25.151 25.151 563.924 563.924 testSqlAlchemy.py:2() 1001 0.050 0.000 329.285 0.329 base.py:2679(fetchmany) 1001 5.503 0.005 314.665 0.314 base.py:2804(_fetchmany_impl) 10000003 4.328 0.000 307.843 0.000 base.py:2795(_fetchone_impl) 10011 0.309 0.000 302.743 0.030 base.py:2790(_buffer_rows) 10011 233.620 0.023 302.425 0.030 {'psycopg2._psycopg.cursor'对象}的方法'fetchmany' 10000000 145.459 0.000 209.147 0.000 testSqlAlchemy.py:13(process)
时序configuration文件(无处理)
Fri Mar 4 14:03:06 2011 sqlAlchemy.prof 305460312在536.368 CPU秒内调用函数(305459030原始调用) 顺序:累计时间 ncalls tottime percall cumtime percall文件名:lineno(函数) 1 0.001 0.001 536.370 536.370 {execfile} 1 29.503 29.503 536.369 536.369 testSqlAlchemy.py:2() 1001 0.066 0.000 333.806 0.333 base.py:2679(fetchmany) 1001 5.444 0.005 318.462 0.318 base.py:2804(_fetchmany_impl) 10000003 4.389 0.000 311.647 0.000 base.py:2795(_fetchone_impl) 10011 0.339 0.000 306.452 0.031 base.py:2790(_buffer_rows) 10011 235.664 0.024 306.102 0.031 {'psycopg2._psycopg.cursor'对象}的方法'fetchmany' 10000000 32.904 0.000 172.802 0.000 base.py:2246(_repr__)
最终评论
不幸的是,除非在SQLAlchemy中有一种方法来指定输出的null ='userDefinedValueOrString'和delimiter ='userDefinedValueOrString',否则processrow函数需要保留。 我们目前使用的Java已经做到了这一点,所以与苹果的比较(与processrow)需要苹果。 如果有一种方法可以通过纯Python或设置调整来提高processrow或SQLAlchemy的性能,那么我非常感兴趣。
这不是一个开箱即用的答案,所有客户端/数据库的东西,你可能需要做一些工作,以确定究竟什么是错误的
备份postgresql.conf改变
log_min_duration_statement to 0 log_destination = 'csvlog' # Valid values are combinations of logging_collector = on # Enable capturing of stderr and csvlog log_directory = 'pg_log' # directory where log files are written, log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, debug_print_parse = on debug_print_rewritten = on debug_print_plan output = on log_min_messages = info (debug1 for all server versions prior to 8.4)
停止并重新启动您的数据库服务器(重新加载可能无法获取更改)重现您的testing,确保服务器时间和客户端时间匹配,并logging开始时间等
将日志文件复制到您select的编辑器中(excel或其他电子表格可用于获取sql和计划等的高级操作)
现在从服务器端检查时间,并注意:
-
是在每种情况下在服务器上报告的SQL相同
-
如果相同,你应该有相同的时间
-
是客户端生成一个游标,而不是传递SQL
-
是一个在字符集之间进行大量的转换/转换的驱动程序,或者是其他types(如date或时间戳)的隐式转换。
等等
计划数据将包含完整性,这可能会通知客户提交的SQL是否存在重大差异。
下面的内容大概是超出你的想法或者是你认为在你的环境中被认为是可以接受的,但是我会把这个选项放在桌面上以防万一。
-
test.sql
中每个SELECT
的目的地是否真的很简单|
分离的结果文件? - 不可移植性(Postgres特异性)是否可以接受?
- 你的后端Postgres 8.2或更新?
- 该脚本是否会与数据库后端在同一主机上运行,或者生成
|
是否可以接受 从后端内部分离结果文件(例如分享?)
如果上述问题的答案是肯定的 ,那么你可以将你的SELECT ...
语句转换为COPY ( SELECT ... ) TO E'path-to-results-file' WITH DELIMITER '|' NULL E'\\N'
COPY ( SELECT ... ) TO E'path-to-results-file' WITH DELIMITER '|' NULL E'\\N'
。
另一种方法是使用ODBC。 这假设Python ODBC驱动程序执行得很好。
PostgreSQL具有用于Windows和Linux的ODBC驱动程序。
作为一个主要用汇编编程的人,有一件事情是突出的。 你正在失去时间的开销,而开销是需要去的。
而不是使用python,它把自己封装在其他的东西里,这些东西是一个围绕着数据库的C封装的东西。用C写代码我的意思是,需要多长时间? Postgres并不难与之交互(完全相反)。 C是一个简单的语言。 你正在执行的操作看起来很简单。 你也可以使用embedded在C中的SQL,这只是一个预编译的问题。 没有必要翻译你在想什么 – 只需在C处编写它,然后使用提供的ECPG编译器(阅读postgres手册第29章iirc)。
把尽可能多的界面东西拿出来,剪掉中间人,然后和本地数据库交谈。 在我看来,试图使系统更简单,实际上使其变得比需要的复杂。 当事情变得非常混乱时,我通常会问自己这个问题:“我最怕碰到什么代码?” 这通常指出我需要改变。
抱歉bab呀,但也许倒退一些,有些新鲜空气会帮助;)