【问题标题】:Why are SQL aggregate functions so much slower than Python and Java (or Poor Man's OLAP)为什么 SQL 聚合函数比 Python 和 Java(或可怜人的 OLAP)慢得多
【发布时间】:2010-09-08 06:28:48
【问题描述】:

我需要真正的 DBA 的意见。 Postgres 8.3 在我的 Macbook Pro 上执行此查询需要 200 毫秒,而 Java 和 Python 在 20 毫秒(350,000 行)内执行相同的计算:

SELECT count(id), avg(a), avg(b), avg(c), avg(d) FROM tuples;

这是使用 SQL 数据库时的正常行为吗?

架构(表格包含对调查的回复):

CREATE TABLE tuples (id integer primary key, a integer, b integer, c integer, d integer);

\copy tuples from '350,000 responses.csv' delimiter as ','

我用 Java 和 Python 为上下文编写了一些测试,它们压垮了 SQL(纯 python 除外):

java   1.5 threads ~ 7 ms    
java   1.5         ~ 10 ms    
python 2.5 numpy   ~ 18 ms  
python 2.5         ~ 370 ms

即使 sqlite3 与 Postgres 竞争,尽管它假设所有列都是字符串(相比之下:即使在 Postgres 中仅使用切换到数字列而不是整数也会导致 10 倍的减速)

我尝试过但没有成功的调整包括(盲目地遵循一些网络建议):

increased the shared memory available to Postgres to 256MB    
increased the working memory to 2MB
disabled connection and statement logging
used a stored procedure via CREATE FUNCTION ... LANGUAGE SQL

所以我的问题是,我在这里的体验是否正常,这是我在使用 SQL 数据库时可以期待的吗?我可以理解 ACID 必须付出代价,但在我看来这有点疯狂。我不是要求实时游戏速度,但由于 Java 可以在 20 毫秒内处理数百万次双打,我感到有点嫉妒。

有没有更好的方法来便宜地执行简单的 OLAP(在金钱和服务器复杂性方面)?我研究过 Mondrian 和 Pig + Hadoop,但对维护另一个服务器应用程序并不太兴奋,也不确定它们是否会有所帮助。


没有 Python 代码和 Java 代码可以说是在内部完成所有工作。我只生成了 4 个数组,每个数组有 350,000 个随机值,然后取平均值。我不包括时间中的生成,只包括平均步骤。 java 线程计时使用 4 个线程(每个数组平均一个),过大但绝对是最快的。

sqlite3 计时由 Python 程序驱动,从磁盘运行(不是 :memory:)

我意识到 Postgres 在幕后做了更多工作,但大部分工作对我来说并不重要,因为这是只读数据。

Postgres 查询不会更改后续运行的时间。

我已重新运行 Python 测试,包括将其从磁盘中脱机。时间大大减慢到近 4 秒。但我猜 Python 的文件处理代码几乎是用 C 语言编写的(尽管可能不是 csv lib?)所以这向我表明 Postgres 也没有从磁盘流式传输(或者你是正确的,我应该低头在编写存储层之前!)

【问题讨论】:

  • 你的 java/python 代码是做什么的?它是否连接到 Postgres 和 SELECT * 然后总结起来?还是您解析 CSV 文件,或者什么:-)?
  • 我来这里只是为了验证 postgres 聚合函数确实比较慢。

标签: python sql optimization aggregate olap


【解决方案1】:

我会说你的测试方案并不是很有用。为了完成 db 查询,db server 需要经过几个步骤:

  1. 解析 SQL
  2. 制定一个查询计划,即。 e.决定使用哪些索引(如果有)、优化等。
  3. 如果使用了索引,则在其中搜索指向实际数据的指针,然后转到数据中的适当位置或
  4. 如果没有使用索引,则扫描整个表以确定需要哪些行
  5. 将数据从磁盘加载到临时位置(希望但不一定是内存)
  6. 执行 count() 和 avg() 计算

因此,在 Python 中创建一个数组并获取平均值基本上会跳过所有这些步骤,只保存最后一个步骤。由于磁盘 I/O 是程序必须执行的最昂贵的操作之一,因此这是测试中的一个主要缺陷(另请参阅我之前在此处询问的 this question 的答案)。即使您在其他测试中从磁盘读取数据,过程也完全不同,很难判断结果的相关性。

要获得有关 Postgres 将时间花在哪里的更多信息,我建议进行以下测试:

  • 将查询的执行时间与不带聚合函数的 SELECT 进行比较(即剪切步骤 5)
  • 如果您发现聚合导致显着变慢,请尝试 Python 是否执行得更快,通过比较中的普通 SELECT 获取原始数据。

要加快查询速度,请先减少磁盘访问。我非常怀疑是聚合需要时间。

有几种方法可以做到这一点:

  • 缓存数据(在内存中!)以供后续访问,无论是通过数据库引擎自己的功能还是使用 memcached 等工具
  • 减少存储数据的大小
  • 优化索引的使用。有时这可能意味着完全跳过索引使用(毕竟,它也是磁盘访问)。对于 MySQL,我似乎记得如果您假设查询获取表中所有数据的 10% 以上,建议跳过索引。
  • 如果您的查询充分利用了索引,我知道对于 MySQL 数据库,将索引和数据放在单独的物理磁盘上会有所帮助。但是,我不知道这是否适用于 Postgres。
  • 如果由于某种原因无法在内存中完全处理结果集,可能还会出现更复杂的问题,例如将行交换到磁盘。但我会放弃这种研究,直到遇到无法找到其他解决方法的严重性能问题,因为它需要了解您的流程中的许多底层细节。

更新:

我刚刚意识到您似乎没有使用上述查询的索引,而且很可能也没有使用任何索引,所以我对索引的建议可能没有帮助。对不起。不过,我想说聚合不是问题,但磁盘访问才是问题。无论如何,我会把索引的东西留在里面,它可能还有一些用处。

【讨论】:

  • 1. Python 还需要解析“查询”。如果重复运行 SQL,则应缓存该步骤。 2. Python 还需要制定一个“查询计划”——也就是说,如果是 JIT-ing,尽管这比 SQL 容易得多。同样,如果重复运行,它应该被缓存。 3. 可能不使用索引 4. Python 还需要扫描整个表 - 350k 行的微不足道的任务。 5. 根据您获取数据的位置,python 也需要从磁盘加载数据——同样,如果重复运行,这应该被缓存。 -- 基本上,这种直截了当的情况下,差异几乎为零。
【解决方案2】:

Postgres 做的比看起来要多得多(首先要保持数据一致性!)

如果值不必 100% 正确,或者表很少更新,但您经常运行此计算,则可能需要查看物化视图以加快计算速度。

(注意,我没有在 Postgres 中使用物化视图,它们看起来有点笨拙,但可能适合您的情况)。

Materialized Views

还要考虑实际连接到服务器的开销以及将请求发送到服务器并返回所需的往返行程。

我认为这样的事情需要 200 毫秒非常好,在我的 oracle 服务器上进行快速测试,相同的表结构有大约 500k 行且没有索引,大约需要 1 - 1.5 秒,这几乎都是 oracle从磁盘中吸取数据。

真正的问题是,200ms 够快吗?

------------- 更多--------

我对使用物化视图解决这个问题很感兴趣,因为我从来没有真正玩过它们。这是在 oracle 中。

首先我创建了一个每分钟刷新的 MV。

create materialized view mv_so_x 
build immediate 
refresh complete 
START WITH SYSDATE NEXT SYSDATE + 1/24/60
 as select count(*),avg(a),avg(b),avg(c),avg(d) from so_x;

刷新时,没有返回任何行

SQL> select * from mv_so_x;

no rows selected

Elapsed: 00:00:00.00

一旦刷新,它比执行原始查询快得多

SQL> select count(*),avg(a),avg(b),avg(c),avg(d) from so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:05.74
SQL> select * from mv_so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:00.00
SQL> 

如果我们插入到基表中,结果是不能立即查看查看MV的。

SQL> insert into so_x values (1,2,3,4,5);

1 row created.

Elapsed: 00:00:00.00
SQL> commit;

Commit complete.

Elapsed: 00:00:00.00
SQL> select * from mv_so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:00.00
SQL> 

但是稍等片刻,MV 会在后台更新,结果会很快返回。

SQL> /

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899460 7495.35823 22.2905352 5.00276078 2.17647059

Elapsed: 00:00:00.00
SQL> 

这并不理想。首先,它不是实时的,插入/更新不会立即可见。此外,无论您是否需要,您都会运行一个查询来更新 MV(这可以调整到任何时间范围,或按需调整)。但是,这确实表明了一个 MV 可以让最终用户觉得它有多快,如果你可以接受不完全达到秒精度的值。

【讨论】:

  • 我喜欢它,我认为这适合我的情况,我必须检查 Postgres 支持或 MV,它看起来确实不如 Oracle 的可靠。这可能是答案的 75%,但我仍然想知道是否可以加快实时聚合查询。
  • 我很想看看 200ms 是如何建立的。我同意网络往返会产生开销。此外,将代码放入 StoredProc 可以消除解析、编译等 SQL 查询的时间。以我的经验,聚合函数在 SQL 中并不慢...
【解决方案3】:

我用 MySQL 重新测试,指定 ENGINE = MEMORY,它没有改变任何东西(仍然是 200 毫秒)。使用内存数据库的 Sqlite3 也提供了类似的时间(250 毫秒)。

here 的数学看起来是正确的(至少是大小,因为这就是 sqlite 数据库的大小 :-)

我只是不购买磁盘原因缓慢的论点,因为有所有迹象表明这些表在内存中(postgres 人都警告不要太努力地将表固定到内存,因为他们发誓操作系统会做得更好比程序员)

为了澄清时间,Java 代码不是从磁盘读取,如果 Postgres 从磁盘读取并计算一个复杂的查询,这将是一个完全不公平的比较,但这真的不是重点,数据库应该足够聪明将一个小表带入内存并预编译存储过程恕我直言。

更新(回应下面的第一条评论):

我不确定如何在不使用聚合函数的情况下以公平的方式测试查询,因为如果我选择所有行,它将花费大量时间序列化和格式化所有内容.我并不是说缓慢是由于聚合功能,它仍然可能只是并发性、完整性和朋友的开销。我只是不知道如何将聚合隔离为唯一的自变量。

【讨论】:

  • 我必须说我很惊讶,但在我看来你假设表数据不是从磁盘读取是正确的。不过,您确定这是聚合部分,而不是检索部分吗?即,如果您执行简单的 SELECT * 并聚合程序中的行,您会获得速度提升吗?
【解决方案4】:

这些是非常详细的答案,但他们大多是在回避问题,我如何在不离开 Postgres 的情况下获得这些好处,因为数据很容易放入内存,需要并发读取但不需要写入,并且一遍又一遍地使用相同的查询进行查询再次。

是否可以预编译查询和优化计划?我原以为存储过程会这样做,但它并没有真正的帮助。

为了避免磁盘访问,需要在内存中缓存整个表,我可以强制 Postgres 这样做吗?我认为它已经在这样做了,因为查询在重复运行后仅 200 毫秒内执行。

我可以告诉 Postgres 该表是只读的,因此它可以优化任何锁定代码吗?

我认为可以用空表估算查询构建成本(时间范围为 20-60 毫秒)

我仍然不明白为什么 Java/Python 测试无效。 Postgres 并没有做更多的工作(尽管我还没有解决并发方面,只是缓存和查询构造)

更新: 我认为通过将 350,000 个通过驱动程序和序列化步骤拉入 Python 以运行聚合来比较 SELECTS 是不公平的,甚至也不能省略聚合,因为格式化和显示的开销很难与时间分开。如果两个引擎都在内存数据中运行,那应该是苹果对苹果的比较,但我不确定如何保证这已经发生了。

我不知道如何添加 cmets,也许我没有足够的声誉?

【讨论】:

  • 我想说用空表估计基本开销是好的。由于您正在执行全表扫描,因此索引的使用可能无关紧要,这会将 200 毫秒的大部分(160 到 180 毫秒)留给从磁盘和聚合加载数据。您是否按照我的建议比较了两个 SELECT?
  • 您需要 50 声望才能发表评论。
【解决方案5】:

我本人是一名 MS-SQL 专家,我们将使用 DBCC PINTABLE 来保持表的缓存,并使用 SET STATISTICS IO 来查看它是从缓存中读取的,而不是从磁盘中读取的。

我在 Postgres 上找不到任何模仿 PINTABLE 的内容,但 pg_buffercache 似乎提供了有关缓存中内容的详细信息 - 您可能需要检查一下,看看您的表是否真的被缓存了。

信封计算的快速回溯让我怀疑您正在从磁盘分页。假设 Postgres 使用 4 字节整数,则每行有 (6 * 4) 字节,因此您的表最小为 (24 * 350,000) 字节 ~ 8.4MB。假设您的 HDD 上的持续吞吐量为 40 MB/s,那么您正在查看大约 200 毫秒来读取数据(as pointed out,这应该是几乎所有时间都花在的地方)。

除非我在某个地方搞砸了我的数学运算,否则我看不出您如何能够将 8MB 读入您的 Java 应用程序并在您显示的时间处理它 - 除非该文件已经被驱动器或您的操作系统。

【讨论】:

    【解决方案6】:

    我认为您的结果并不那么令人惊讶——如果有的话,Postgres 是如此之快。

    一旦有机会缓存​​数据,Postgres 查询是否第二次运行得更快?为了更公平一点,您对 Java 和 Python 的测试应该首先包括获取数据的成本(最好从磁盘加载)。

    如果此性能级别在实践中对您的应用程序来说是个问题,但您出于其他原因需要 RDBMS,那么您可以查看memcached。这样您就可以更快地访问原始数据并在代码中进行计算。

    【讨论】:

      【解决方案7】:

      RDBMS 通常为您做的另一件事是通过保护您免受另一个进程的同时访问来提供并发性。这是通过放置锁来完成的,这会产生一些开销。

      如果您要处理完全不变的静态数据,尤其是在基本“单用户”场景中,那么使用关系数据库不一定会给您带来太多好处。

      【讨论】:

        【解决方案8】:

        您是否使用 TCP 访问 Postgres?在那种情况下,Nagle 会扰乱你的时间安排。

        【讨论】:

        • 是的,我正在使用 TCP,但它在本地主机上。简单的查询也可以非常快地执行,在 15-60 毫秒之间。
        • 如果它的本地主机,那么 Nagle 不会启动。很抱歉没有任何帮助:(
        【解决方案9】:

        您需要将 postgres 的缓存增加到可以将整个工作集放入内存的程度,然后才能看到与使用程序在内存中执行它的性能相当。

        【讨论】:

          【解决方案10】:

          感谢 Oracle 的时间安排,这正是我正在寻找的东西(虽然令人失望 :-)

          物化视图可能值得考虑,因为我认为我可以为大多数用户预先计算最有趣的查询形式。

          我不认为查询往返时间应该很高,因为我在运行 Postgres 的同一台机器上运行查询,所以它不会增加太多延迟?

          我还检查了缓存大小,似乎 Postgres 依赖操作系统来处理缓存,他们特别提到 BSD 是理想的操作系统,所以我认为 Mac OS 应该非常聪明地引入表进入内存。除非有人考虑到更具体的参数,否则我认为更具体的缓存是我无法控制的。

          最后我大概可以忍受 200 毫秒的响应时间,但知道 7 毫秒是一个可能的目标让我感到不满意,因为即使是 20-50 毫秒的时间也会让更多的用户有更多的最新查询和摆脱大量缓存和预先计算的黑客攻击。

          我刚刚使用 MySQL 5 检查了时间,它们比 Postgres 稍差。所以除非有一些重大的缓存突破,我想这就是我可以期待的关系数据库路线。

          我希望我能对你的一些答案投赞成票,但我还没有足够的分数。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-06-15
            • 2021-03-10
            • 1970-01-01
            • 2019-10-08
            相关资源
            最近更新 更多