在 sql 中执行计算和在应用程序中执行计算的利弊如何

shopkeeper表有以下字段:

id (bigint),amount (numeric(19,2)),createddate (timestamp)

比方说,我有上面的表。我想要昨天和 通过将金额打印成美分来生成一份报告。

一种方法是在我的 Java 应用程序中执行计算并执行一个简单的查询

Date previousDate ;// $1 calculate in application


Date todayDate;// $2 calculate in application


select amount where createddate between $1 and $2

然后在我的 java 应用程序中循环遍历记录并将金额转换为美分并生成报告

另一种方法类似于在 sql 查询本身中执行计算:

select cast(amount * 100 as int) as "Cents"
from shopkeeper  where createddate  between date_trunc('day', now()) - interval '1 day'  and  date_trunc('day', now())

然后循环遍历记录并生成报告

在某种程度上,我所有的处理都是在 Java 应用程序中完成的,并且触发了一个简单的查询。 在其他情况下,所有转换和计算都在 Sql 查询中完成。

上面的用例只是一个例子,在真实的场景中,一个表可能有许多列需要处理类似的类型。

你能告诉我哪种方法在性能和其他方面更好吗? 为什么

97210 次浏览

这取决于很多因素,但最关键的是:

  • 计算的复杂性(更喜欢在 app-server 上进行复杂的计算,因为它可以扩展 出去; 而不是扩展 起来的 db 服务器)
  • 数据量(如果需要访问/聚合大量数据,在 db 服务器上进行这项工作可以节省带宽,如果聚合可以在索引内进行,则可以节省磁盘 IO)
  • 方便性(sql 对于复杂的工作来说不是最好的语言——特别是对于过程性的工作来说不是很好,但是对于基于集的工作来说非常好; 但是错误处理非常糟糕)

一如既往,如果您将 数据带回到应用服务器,最小化列和行将对您有利。确保对查询进行了优化并适当地建立了索引,这对任何一种情况都有帮助。

关于你的留言:

然后循环查看记录

通过记录循环 几乎总是 sql 中的错误做法——最好编写基于集的操作。

作为一般规则 ,我倾向于将数据库的工作保持在最低限度“存储这个数据,获取这个数据”——然而,总有一些场景的例子,在服务器上一个优雅的查询可以节省大量带宽。

还要考虑: 如果这在计算上很昂贵,是否可以将其缓存到某个地方?

如果您想要一个 准确无误“这是更好的”; 编码它的两种方式,并比较它(注意,无论是第一个草案可能不是100% 调优)。但是考虑到典型的用法: 如果,在现实中,它被一次调用5次(分别) ,那么模拟: 不要只比较一个“这些中的1对那些中的1”。

一般来说,如果同一个或其他项目中的其他模块或组件也需要获得这些结果,则使用 SQL 完成操作。完成服务器端的原子操作也更好,因为您只需要从任何 db 管理工具调用存储的 proc 来获取最终值,而不需要进一步处理。

在某些情况下,这并不适用,但当它适用时,它是有意义的。而且总的来说,数据库盒具有最好的硬件和性能。

在这种情况下,使用 SQL 进行计算比使用 可能吧稍微好一些,因为数据库引擎可能比 Java 具有更高效的十进制算术例程。

一般而言,对于行级计算,没有太大的差别。

它真正起作用的地方是:

  • 像 SUM ()、 AVG ()、 MIN ()、 MAX ()这样的聚合计算在这里,数据库引擎将比 Java 实现快一数量级。
  • 计算用于筛选行的任何位置。在数据库中进行筛选比读取一行然后丢弃它要有效得多。

如果我们能够确定业务实现中的目标,那么是在前端执行计算还是在后端执行计算在很大程度上取决于我们。有时候,Java 代码可能比 sql 代码执行得更好,两者都写得很好,或者反之亦然。但仍然如果困惑,你可以尝试先确定-

  1. 如果您可以通过数据库 sql 实现一些简单的东西,那么最好尝试一下,因为 db 将执行得更好,并在那里进行计算,然后获取结果。然而,如果实际的计算需要太多的计算,那么您可以使用应用程序代码。为什么?因为在大多数情况下,场景就像循环一样,sql 并不能很好地处理这些问题,而前端语言就是为这些问题而设计的。
  2. 如果需要从很多地方进行类似的计算,那么显然将计算代码放在数据库的末端会更好地保持在同一个地方。
  3. 如果要通过许多不同的查询来获得最终结果,需要进行大量的计算,那么也可以使用 db 端,因为你可以将相同的代码放在存储过程中,这样比从后端检索结果,然后在前端进行计算效果更好。

在决定将代码放置在何处之前,您可以考虑许多其他方面。有一种观点是完全错误的——任何事情都可以在 Java (应用程序代码)中完成得最好,或者任何事情都可以由 db (sql 代码)完成得最好。

PostgreSQL而言,您可以在服务器上非常有效地完成很多工作。RDBMS 本身就擅长排序、聚合、强制转换和格式化数据。(我认为 Postgres 做得特别好。)RDBMS 可以处理来自原始存储的原始数据类型。经过处理之后,通常需要传输的数据要少得多。
客户端通常需要比最终使用/显示更多的数据来执行上述操作。他们通常使用“文本”协议与服务器通信,来回强制转换增加了成本。许多工作必须重复。这不是一个公平的竞争环境。

对于过程需求,您可以从各种 服务器端脚本语言中进行选择: tcl、 python、 perl 等等。不过,大多数情况下使用的是 PL/pgSQL

让我用一个 隐喻: 如果你想在巴黎买一个 金项链,金匠可以坐在开普敦或巴黎,这是一个技巧和品味的问题。但是 永远不会会为此从南非把成吨的金矿石运到法国。矿石在矿场(或至少在一般地区)加工,只有黄金才能运输。应用程序和数据库也应该如此。

最坏的情况 场景将是对于较大集合中的每一行重复访问服务器。(这相当于每次运送一吨矿石。)

第二行 ,如果发送一系列查询,每个查询取决于之前的查询,而所有查询都可以在服务器上的一个查询或过程中完成。(这就像把黄金和每一颗珠宝按顺序分别装在一艘船上运输一样。)

在应用程序和服务器之间来回切换是相对昂贵的。服务器 还有客户端。尽量减少这种情况的发生,你就会取得胜利——因此,在必要的地方使用服务器端过程和/或复杂的 SQL。

我们刚刚完成了一个项目,在这个项目中,我们将几乎所有复杂的查询打包到 Postgres 函数中。应用程序移交参数并获得所需的数据集。快速,干净,简单(对于应用程序开发者来说) ,输入输出减少到最低限度... 一个低碳足印的闪亮项链。

对于应该在 SQL 中执行哪些数据访问逻辑部分以及应该在应用程序中执行哪些部分,没有黑白分明的规定。我喜欢 Mark Gravell 的的措辞,区分

  • 复杂的计算
  • 数据密集型计算

SQL 的能力和表达能力被严重低估了。自从引入 窗口函数以来,很多非严格面向集合的计算可以在数据库中非常容易和优雅地执行。

无论整个应用程序体系结构如何,都应该始终遵循以下三条经验法则:

  • 保持数据库和应用程序之间传输的数据量较小(有利于在数据库中计算数据)
  • 保持数据库从磁盘加载的数据量较小(有利于让数据库优化语句,以避免不必要的数据访问)
  • 不要使用复杂的并发计算将数据库推到 CPU 的极限(有利于将数据拉入应用程序内存并在那里执行计算)

根据我的经验,拥有一个不错的 DBA 和一些关于您不错的数据库的不错的知识,您不会很快遇到 DBS CPU 的限制。

下面是对这些事情的进一步解释:

如果您正在 ORM 之上编写或者正在编写一些低性能的应用程序,那么可以使用任何模式来简化应用程序。如果您正在编写一个高性能的应用程序,并且仔细考虑了规模,那么您将通过将处理转移到数据来获胜。我强烈主张将处理转移到数据上。

让我们分两个步骤来考虑这个问题: (1) OLTP (少量记录)事务。(2) OLAP (多条记录的长扫描)。

在 OLTP 情况下,如果希望快速(每秒10k-100k 事务) ,必须从数据库中删除闩锁、锁和死锁争用。这意味着您需要消除事务中的长时间停顿: 从客户机到数据库将处理转移到客户机的往返过程就是这样一个长时间停顿。您不可能拥有长期生存的事务(使读/更新成为原子)并具有非常高的吞吐量。

回复: 水平缩放。现代数据库横向扩展。这些系统已经实现了 HA 和容错。利用这一点,尽量简化您的应用程序空间。

让我们来看看 OLAP ——在这种情况下,很明显,将大量数据拖回应用程序是一个糟糕的想法。这些系统是专门用来对压缩的、预先组织的柱状数据进行高效操作的。现代 OLAP 系统还具有水平伸缩性,并且具有水平分散工作的复杂查询规划器(内部移动数据处理)。

从性能的角度来看: 这是一个非常简单的算术操作,几乎可以肯定,它的执行速度要比实际从数据库底下的磁盘获取数据快得多。此外,在任何运行时计算 where 子句中的值都可能非常快。总之,瓶颈应该是磁盘 IO,而不是值的计算。

根据可读性,我认为如果您使用 ORM,您应该在您的应用服务器环境中进行,因为 ORM 允许您使用基于集合的操作非常容易地处理底层数据。如果你无论如何都要编写原始 SQL,在那里进行计算没有什么错,如果格式正确的话,你的 SQL 看起来也会更好一些,更容易阅读。

至关重要的是,“表现”并没有定义。

对我来说最重要的是开发人员的时间。

编写 SQL 查询。如果速度太慢或数据库成为瓶颈,那么请重新考虑。到那时,您将能够对这两种方法进行基准测试,并根据与设置相关的实际数据(硬件和所在的堆栈)做出决策。

我认为,如果没有具体的例子和基准,就无法推断性能差异,但我有另一种看法:

你能更好地维护哪一个?例如,您可能希望将前端从 Java 切换到 Flash、 HTML5、 C + + 或其他操作。大量的程序已经经历了这样的变化,甚至一开始就以多种语言存在,因为它们需要在多种设备上工作。

即使您有一个合适的中间层(从给出的例子来看,情况似乎并非如此) ,该层可能会发生变化,JBoss 可能会变成 Ruby/Rails。

另一方面,用 SQL 替换 SQL 后端的可能性不大,即使用 SQL 替换 SQL 后端,也必须从头开始重写前端,因此这个问题没有实际意义。

我的想法是,如果您在数据库中进行计算,以后编写第二个前端或中间层将会容易得多,因为您不必重新实现所有内容。然而,在实践中,我认为“我在哪里可以用人们能够理解的代码做到这一点”是最重要的因素。

要简化如何回答这个问题,可以查看负载平衡。您希望将负载放在最大容量的位置(如果这样做有意义的话)。在大多数系统中,SQL 服务器很快就会成为瓶颈,因此可能的答案是,您不希望 SQL 多做一分钟的工作。

而且在大多数体系结构中,SQL 服务器构成了系统的核心,而外部系统则被添加到 SQL 服务器上。

但是上面的数学是如此的微不足道,以至于除非你把你的系统推到极限,否则最好的放置地点就是你想放置它的地方。如果数学不是琐碎的,比如计算一个距离计算的 sin/cos/tan,那么这个工作可能就不是琐碎的,需要仔细的计划和测试。

这个问题的其他答案都很有趣。 令人惊讶的是,没有人回答你的问题。你想知道:

  1. 在查询中使用 Cents 是否更好? 我不认为是强制转换 在您的查询中添加任何内容。
  2. 在查询中使用 now ()是否更好?我更愿意将日期传递到查询中,而不是在查询中计算它们。

更多信息: 对于第一个问题,你要确保聚合分数 没有舍入误差。我认为数字19,2是合理的 在第二种情况下,整数是可以的。由于这个原因,使用浮动汇率是错误的。

对于问题二,作为一个程序员,我喜欢完全控制 日期被认为是“现在”。它可能很难写自动单位 使用像 now ()这样的函数进行测试 事务脚本设置一个变量等于 now ()并使用这个变量是很好的 所有的逻辑都使用完全相同的值。

让我举一个真实的例子来解决这个问题

我需要在我的 ohlc 数据上计算一个加权移动平均值,我有大约134000根蜡烛,每根蜡烛都有一个符号

  1. 选项1在 Python/Node 等中执行
  2. 选项2使用 SQL 本身执行此操作!

哪个更好

  • 如果我必须在 Python 中完成这项工作,实际上,我必须在最坏的情况下获取所有存储的记录,执行计算并将所有内容保存回来,在我看来,这是一个巨大的 IO 浪费
  • 加权移动平均每次你得到一个新的蜡烛,意味着我会做大量的 IO 定期间隔,这不是一个 对我的星座有好的看法
  • 在 SQL 中,我所要做的可能就是编写一个触发器来计算和存储所有内容,所以只需要时不时地获取每个对的最终 WMA 值,这样效率会高得多

规定

  • 如果我必须计算每支蜡烛的 WMA 并存储它,我会在 Python 上做
  • 但是因为我只需要最后一个值 SQL 比 Python 快得多

为了给您一些鼓励,这是 Python 版本的加权移动平均值

WMA 通过代码完成

import psycopg2
import psycopg2.extras
from talib import func
import timeit
import numpy as np
with psycopg2.connect('dbname=xyz user=xyz') as conn:
with conn.cursor() as cur:
t0 = timeit.default_timer()
cur.execute('select distinct symbol from ohlc_900 order by symbol')
for symbol in cur.fetchall():
cur.execute('select c from ohlc_900 where symbol = %s order by ts', symbol)
ohlc = np.array(cur.fetchall(), dtype = ([('c', 'f8')]))
wma = func.WMA(ohlc['c'], 10)
# print(*symbol, wma[-1])
print(timeit.default_timer() - t0)
conn.close()

从 SQL 到 WMA

"""
if the period is 10
then we need 9 previous candles or 15 x 9 = 135 mins on the interval department
we also need to start counting at row number - (count in that group - 10)
For example if AAPL had 134 coins and current row number was 125
weight at that row will be weight = 125 - (134 - 10) = 1
10 period WMA calculations
Row no Weight c
125 1
126 2
127 3
128 4
129 5
130 6
131 7
132 8
133 9
134 10
"""
query2 = """
WITH
condition(sym, maxts, cnt) as (
select symbol, max(ts), count(symbol) from ohlc_900 group by symbol
),
cte as (
select symbol, ts,
case when cnt >= 10 and ts >= maxts - interval '135 mins'
then (row_number() over (partition by symbol order by ts) - (cnt - 10)) * c
else null
end as weighted_close
from ohlc_900
INNER JOIN condition
ON symbol = sym
WINDOW
w as (partition by symbol order by ts rows between 9 preceding and current row)
)
select symbol, sum(weighted_close)/55 as wma
from cte
WHERE weighted_close is NOT NULL
GROUP by symbol ORDER BY symbol
"""
with psycopg2.connect('dbname=xyz user=xyz') as conn:
with conn.cursor() as cur:
t0 = timeit.default_timer()
cur.execute(query2)
# for i in cur.fetchall():
# print(*i)
print(timeit.default_timer() - t0)
conn.close()

信不信由你,该查询运行速度比执行 WEIGHTED MOVING AVERAGE 的纯 Python 版本更快! ! !我已经一步一步地写了这个查询,所以坚持下去,你会做得很好

速度

0.42141127300055814秒 Python

0.23801879299935536秒

我有134000个假的 OHLC 记录在我的数据库中分为1000个股票,所以这是一个例子,SQL 可以胜过你的应用服务器