如何加快 SQLAlchemy 查询?

Posted

技术标签:

【中文标题】如何加快 SQLAlchemy 查询?【英文标题】:How to speed up a SQLAlchemy Query? 【发布时间】:2018-08-12 08:01:12 【问题描述】:

我有一个超过 1000 万行的表。大约有 50 多列。该表存储传感器数据/参数。假设我需要查询一整天或 86,400 秒的数据。完成此查询大约需要 20 秒或更长时间。

我在一些列上添加了单独的索引,例如 recordTimestamp(存储数据何时被捕获)、deviceId(传感器的标识)、positionValid(GPS 地理位置是否有效)。然后我添加了一个包含所有三列的复合索引。

以下是我的查询:

t1 = time.time()
conn = engine.connect()
select_statement = select([Datatable]).where(and_(
    Datatable.recordTimestamp >= start_date,
    Datatable.recordTimestamp <= end_date,
    Datatable.deviceId == device_id,
    Datatable.positionValid != None,
    Datatable.recordTimestamp % query_interval == 0))
lol_data = conn.execute(select_statement).fetchall()    
conn.close() 
t2 = time.time()
time_taken = t2 - t1
print('Select: ' + time_taken)

以下是我的解释分析声明:

EXPLAIN ANALYZE SELECT datatable.id, datatable."createdAt", datatable."analogInput01", datatable."analogInput02", datatable."analogInput03", datatable."analogInput04", datatable."analogInput05", datatable."analogInput06", datatable."analogInput07", datatable."canEngineRpm", datatable."canEngineTemperature", datatable."canFuelConsumedLiters", datatable."canFuelLevel", datatable."canVehicleMileage", datatable."deviceId", datatable."deviceTemperature", datatable."deviceInternalVoltage", datatable."deviceExternalVoltage", datatable."deviceAntennaCut", datatable."deviceEnum", datatable."deviceVehicleMileage", datatable."deviceSimSignal", datatable."deviceSimStatus", datatable."iButton01", datatable."iButton02", datatable."recordSequence", datatable."recordTimestamp", datatable."accelerationAbsolute", datatable."accelerationBrake", datatable."accelerationBump", datatable."accelerationTurn", datatable."accelerationX", datatable."accelerationY", datatable."accelerationZ", datatable."positionAltitude", datatable."positionDirection", datatable."positionSatellites", datatable."positionSpeed", datatable."positionLatitude", datatable."positionLongitude", datatable."positionHdop", datatable."positionMovement", datatable."positionValid", datatable."positionEngine" FROM datatable WHERE datatable."recordTimestamp" >= 1519744521 AND datatable."recordTimestamp" <= 1519745181 AND datatable."deviceId" = '864495033990901' AND datatable."positionValid" IS NOT NULL AND datatable."recordTimestamp" % 1 = 0;

以下是 SELECT 的 EXPLAIN ANALYZE 的结果:

Index Scan using "ix_dataTable_recordTimestamp" on dataTable (cost=0.44..599.35 rows=5 width=301) (actual time=0.070..10.487 rows=661 loops=1)
Index Cond: (("recordTimestamp" >= 1519744521) AND ("recordTimestamp" <= 1519745181))
Filter: (("positionValid" IS NOT NULL) AND (("deviceId")::text = '864495033990901'::text) AND (("recordTimestamp" % 1) = 0))
Rows Removed by Filter: 6970
Planning time: 0.347 ms
Execution time: 10.658 ms

以下是 Python 计算的时间结果:

Select:  47.98712515830994 
JSON:  0.19731807708740234

下面是我的代码分析:

10302 function calls (10235 primitive calls) in 12.612 seconds

Ordered by: cumulative time

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    1    0.000    0.000   12.595   12.595 /Users/afeezaziz/Projects/Bursa/backend/env/lib/python3.6/site-packages/sqlalchemy/engine/base.py:882(execute)
    1    0.000    0.000   12.595   12.595 /Users/afeezaziz/Projects/Bursa/backend/env/lib/python3.6/site-packages/sqlalchemy/sql/elements.py:267(_execute_on_connection)
    1    0.000    0.000   12.595   12.595 /Users/afeezaziz/Projects/Bursa/backend/env/lib/python3.6/site-packages/sqlalchemy/engine/base.py:1016(_execute_clauseelement)
    1    0.000    0.000   12.592   12.592 /Users/afeezaziz/Projects/Bursa/backend/env/lib/python3.6/site-packages/sqlalchemy/engine/base.py:1111(_execute_context)
    1    0.000    0.000   12.590   12.590 /Users/afeezaziz/Projects/Bursa/backend/env/lib/python3.6/site-packages/sqlalchemy/engine/default.py:506(do_execute)
    1   12.590   12.590   12.590   12.590 method 'execute' of 'psycopg2.extensions.cursor' objects
    1    0.000    0.000    0.017    0.017 /Users/afeezaziz/Projects/Bursa/backend/env/lib/python3.6/site-packages/sqlalchemy/engine/result.py:1113(fetchall)
    1    0.000    0.000    0.017    0.017 /Users/afeezaziz/Projects/Bursa/backend/env/lib/python3.6/site-packages/sqlalchemy/engine/result.py:1080(_fetchall_impl)
    1    0.008    0.008    0.017    0.017 method 'fetchall' of 'psycopg2.extensions.cursor' objects

【问题讨论】:

查询在 10 毫秒内完成。您能否提供慢查询的解释分析? 我不知道为什么直接使用会给出这个...虽然我在连接开始和连接结束上方添加了 print(time.time()) ,结果如下:选择:10.030901908874512所以根据打印语句,大约是 10 秒。 “on CPU”分析的良好可视化是flamegraphs。试着让他们看看最繁忙的代码路径。从您展示的措施来看,我猜想“转换为 python 对象”会占用时间。以 kB 为单位的数据集有多大? 是的,需要花费太多时间的是转换成 python 对象。接收后的数据集大约是1-3MB。我有哪些选择? 除非您真的需要所有原始数据,否则您可能已经对数据库本身执行了一些分析并仅返回聚合结果。或者,如果您需要逐行处理数据,您可以在不获取所有行的情况下执行此操作,但可以即时执行。基本上,需要更多上下文才能给出更多想法。 【参考方案1】:

尝试使用 COPY 内置的 Postgres,或者如果您确实需要在 Python 中检索结果(例如,您不能通过 COPY 直接写入磁盘),您可以通过 psycopgs copy_expert 函数使用 COPY:

cur = conn.cursor()

outputquery = "COPY (0) TO STDOUT WITH CSV HEADER".format(query)

with open('resultsfile', 'w') as f:
    cur.copy_expert(outputquery, f)

conn.close()

这应该避免一起序列化。

【讨论】:

【参考方案2】: 根据您的查询和索引,我认为您已经尽力做到最好,因为您的查询涉及"recordTimestamp""deviceId""positionValid",所以,只要确保您已经从3 列。 我认为问题出在“select([Datatable])”,我猜你选择了所有列,所以,正如你的描述,有 50 多列,解析数据并将数据发送到客户端需要时间。更清楚地说,添加索引只会帮助您的“执行时间”(查找结果的时间),但不会帮助您的“获取时间”(当您运行“lol_data = conn.execute(select_statement).fetchall()”时) . 解决方案:如果你不想改变表的结构,你只需要选择你需要的列。但将表拆分为 2 个表更好。 1 个表包含参数,其他表包含 "deviceId""recordTimestamp"、值。您可以使用索引更改"deviceId"(比较和发送字符串比使用整数需要更多时间)。

【讨论】:

我用 select([Datatable.recordTimestamp, Datatable.deviceId]) 试过了,查询时间差不多。我已经尝试过整数形式的 deviceId,不幸的是相同的查询调子。 是的,因为你的数据库做同样的事情:每行获取数据并解析它,但只发送2列,所以,它可以节省你接收数据的时间而不是接收数据的时间你解析数据。 50+ 列非常大,如果您维护这种形式,您已经在查询方面做到了最好(从 3 列创建 BTREE 索引,而不是 HASH 索引,因为 HASH 索引关注的是地址而不是值)。我正在处理一个包含 2 亿条记录且只有 4 列的表:计数、设备 ID、日期时间和值。一个月内检索数据仅需8秒(约3万行)。【参考方案3】:

SQLAlchemy 只是数据库的连接器,整个查询运行在数据库的末尾。

借助过程和 SQLAlchemy 优化查询,您可以将其归档。 这是一本很好的读物,可以优化您的使用方式。 SQLAlchemy collection docs

如果您使用的是 mysql 数据库,您还应该尝试使用 MySQLdb API,它比 SQLAlchemy 快一点,因为 MySQLdb 是专门针对 MySQL 操作和迭代的面向对象的。

【讨论】:

以上是关于如何加快 SQLAlchemy 查询?的主要内容,如果未能解决你的问题,请参考以下文章

如何在与 SQLAlchemy 和 psycopg2 的 PostgreSQL 连接上设置“lock_timeout”?

在 SQLAlchemy 中以特定格式按日期分组

flask-sqlalchemy的使用

flask_sqlalchemy

SQlAlchemy的增删改查

Flask-SQLAlchemy 学习总结