【问题标题】:Parallel Select's to Postgresql in Python在 Python 中并行选择到 Postgresql
【发布时间】:2021-09-18 20:09:35
【问题描述】:

您好,我们试图通过将一个巨大的选择分割成更小的选择来并行化它。数据集有一个“segment”列,因此我们使用它作为分区选择的一种方式。我们的目标是一个 PosgreSQL 数据库。不幸的是,我们没有观察到性能优势,换句话说,性能提升与我们使用的线程成线性关系。

我们能够将我们的观察结果隔离到一个综合测试用例中。 我们将多次提取 (11) 模拟为从 generate_series 查询中提取的每一个。

我们使用 1 个连接,每个连接按顺序运行或 11 个连接并行运行。

我们没有观察到性能优势。

相反,如果我们只是将提取模拟为阻塞 5 秒 (QUERY1) 的 1 行提取,我们将获得预期的性能优势。

我们用来并行化的主要代码。

def pandas_per_segment(the_conn_pool, segment)-> List[Tuple]:
    print(f"TASK is {segment}")
    sql_query = config.QUERY2
    with the_conn_pool.getconn() as conn:
        conn.set_session(readonly=True, autocommit=True)
        start = default_timer()
        with conn.cursor() as curs:
            curs.execute(sql_query)
            data = curs.fetchall()
        end = default_timer()
        print(f'DB to retrieve {segment} took : {end - start:.5f}')
    the_conn_pool.putconn(conn)
    return data

def get_sales(the_conn_pool) -> pd.DataFrame:
    tasks : Dict = {}
    start = default_timer()
    with futures.ThreadPoolExecutor(max_workers=config.TASKS) as executor:
        for segment in range(0, config.SEGMENTS_NO):
            task = executor.submit(pandas_per_segment,
                            the_conn_pool = the_conn_pool,
                            segment=segment)
            tasks[task] = segment
    end = default_timer()
    print(f'Consumed : {end-start:.5f}')
    start = default_timer()
    master_list = [task.result() or task in tasks]
    result = pd.DataFrame(itertools.chain(*master_list), columns=['item_id', 'brand_name', 'is_exclusive', 'units', 'revenue', 'abs_price', 'segment', 'matches_filter'])
    end = default_timer()
    print(f'Chained : {end - start:.5f}')
    return result

通过直接从 CSV 提取,我们也看到了相同的性能优势。

理论上说,Python 中的套接字/线程/大数据获取效果不佳。

这是正确的吗?我们是不是做错了什么。

在 Big Sur x64、Python 3.9.6、Postgresql 13 上进行测试,附上其余代码

我们的 docker-compose 文件

version: '2'

services:

  database:
    container_name:
      posgres
    image: 'docker.io/bitnami/postgresql:latest'
    ports:
      - '5432:5432'
    volumes:
      - 'postgresql_data:/bitnami/postgresql'
    environment:
      - POSTGRESQL_USERNAME=my_user
      - POSTGRESQL_PASSWORD=password123
      - POSTGRESQL_DATABASE=mn_dataset
    networks:
      - pgtapper

volumes:
  postgresql_data:
    driver: local

networks:
  pgtapper:
    driver: bridge

config.py 文件

TASKS = 1
SEGMENTS_NO = 11
HOST='localhost'

PORT=5432
DBNAME='mn_dataset'
USER='my_user'
PASSWORD='password123'


# PORT=15433
# DBNAME='newron'
# USER='flyway'
# PASSWORD='8P87PE8HKuvjQaAP'


CONNECT_TIMEOUT=600

QUERY1 = '''
select 

    123456789 as item_id,
    'm$$$' as brand_name,
    true as is_exclusive,
    0.409 as units,
    0.567 as revenue,
    0.999 as abs_price,
    'aaaa' as segment,
    TRUE as matches_filter

from (select pg_sleep(5)) xxx
'''

QUERY3 = '''
 select * from t1 LIMIT 10000
'''

QUERY2 = '''
 select 

    123456789 as item_id,
    'm$$$' as brand_name,
    true as is_exclusive,
    0.409 as units,
    0.567 as revenue,
    0.999 as abs_price,
    'aaaa' as segment,
    TRUE as matches_filter

from generate_series(1, 10000)
'''


MYSQL_QUERY = '''
select 

    123456789 as item_id,
    'm$$$' as brand_name,
    true as is_exclusive,
    0.409 as units,
    0.567 as revenue,
    0.999 as abs_price,
    'aaaa' as segment,
    TRUE as matches_filter

from t1
limit 10000
'''

以及我们的完整示例


# This is a sample Python script.

# Press ⌃R to execute it or replace it with your code.
# Press Double ⇧ to search everywhere for classes, files, tool windows, actions, and settings.
import itertools

from psycopg2.pool import ThreadedConnectionPool

from concurrent import futures
from timeit import default_timer
from typing import Dict, List, Tuple
import config
import pandas as pd

def pandas_per_segment(the_conn_pool, segment)-> List[Tuple]:
    print(f"TASK is {segment}")
    sql_query = config.QUERY2
    with the_conn_pool.getconn() as conn:
        conn.set_session(readonly=True, autocommit=True)
        start = default_timer()
        with conn.cursor() as curs:
            curs.execute(sql_query)
            data = curs.fetchall()
        end = default_timer()
        print(f'DB to retrieve {segment} took : {end - start:.5f}')
    the_conn_pool.putconn(conn)
    return data

def get_sales(the_conn_pool) -> pd.DataFrame:
    tasks : Dict = {}
    start = default_timer()
    with futures.ThreadPoolExecutor(max_workers=config.TASKS) as executor:
        for segment in range(0, config.SEGMENTS_NO):
            task = executor.submit(pandas_per_segment,
                            the_conn_pool = the_conn_pool,
                            segment=segment)
            tasks[task] = segment
    end = default_timer()
    print(f'Consumed : {end-start:.5f}')
    start = default_timer()
    master_list = [task.result() or task in tasks]
    result = pd.DataFrame(itertools.chain(*master_list), columns=['item_id', 'brand_name', 'is_exclusive', 'units', 'revenue', 'abs_price', 'segment', 'matches_filter'])
    end = default_timer()
    print(f'Chained : {end - start:.5f}')
    return result

# Press the green button in the gutter to run the script.
if __name__ == '__main__':
    connection_pool = ThreadedConnectionPool(
        minconn=config.TASKS,
        maxconn=config.TASKS,
        host=config.HOST,
        port=config.PORT,
        dbname=config.DBNAME,
        user=config.USER,
        password=config.PASSWORD,
        connect_timeout=config.CONNECT_TIMEOUT
    )
    get_sales(connection_pool)

# See PyCharm help at https://www.jetbrains.com/help/pycharm/


【问题讨论】:

  • 尝试使用 psycopgs2.extras.execute_batch,这里的问题是分离查询不会提高性能,因为它会增加服务器往返时间。如果多次询问,数据库将不会更快地获取结果。
  • 执行批处理序列化,文档说它是 executemany 的一个很好的替代品,主要用于更新。我不更新了。我在上面取。
  • 必须从磁盘获取相同数量的数据,并通过网络发送相同数量的数据。为什么你认为线程会有所帮助? (除了,也许,在客户端)
  • 客户端是我的要求。我有一个数据集,每一行在一组 11 个可能值中都有一个段字段。不是获取整个数据集,而是生成 11 个线程来获取对应于相应段的数据集部分,并在客户端组成最终结果。我已经在下面发布了我的 github,您可以从那里克隆并运行 raw_main.py(您需要启动 docker-compose)。

标签: python pandas postgresql performance psycopg2


【解决方案1】:

使用您的 generate_series 查询,几乎所有时间都花在 python 读取和处理数据上,几乎没有时间花在 PostgreSQL 计算和发送上。

看起来像 ThreadedConnectionPool 某些东西(可能是the global interpreter lock)协调对数据库连接的访问​​(使用futex),因此只有一个可以在python中“活动”(跨所有线程)一次。因此,虽然可以同时在数据库上运行许多查询,但这对您没有帮助,因为实际上几乎没有时间花在这上面。

【讨论】:

  • 如果你的理论是正确的,那么 QUERY1 也应该序列化。但事实并非如此。请参阅此处github.com/fithisux/multithreadesqlfetch 并运行实验。你能发布解决问题的工作代码或回购吗?
  • 不,QUERY1 的所有时间都花在 PostgreSQL 中,而不是 Python 中,因此不会序列化。它正在发送查询并读取序列化的结果,并且几乎没有时间花在使用 QUERY1 做这些事情上,因此序列化所花费的时间可以忽略不计。
  • 还有另一个脚本 cc_main.py 在没有 ThreadedConnectionPool 的情况下具有完全相同的行为。此外,如果在原始版本中我用 SimpleConnectionPool 替换,我也会有相同的行为。你有解决(或几乎解决)问题的脚本吗?
  • 查看我的更正。你说你确实看到了 csv 的好处,但我在你的 git repo 中没有看到这样的例子。如果启动速度更快并且还显示出并行的好处,那么显然这就是要走的路。
  • 我投了赞成票。 CSV 的使用确实更快。可能我有这个作为评论。但不幸的是,我试图解决的问题不适用于 CSV。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-05-05
  • 1970-01-01
  • 1970-01-01
  • 2015-02-02
  • 2013-09-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多