Почему запросы к pandas DataFrame медленнее после операции соединения SQL?

Я работаю с pandas DataFrame, созданным из SQL-запроса, включающего операцию соединения трех таблиц с использованием pd.read_sql(). DataFrame имеет около 1 миллиона строк. Когда я пытаюсь запросить строки на основе значения одного столбца (doi), на каждый запрос уходит от 0,1 до 0,2 секунды.

Интересно, что если я экспортирую DataFrame в файл CSV, а затем загружаю его обратно в память, та же операция запроса выполняется в 5–10 раз быстрее.

Что еще интереснее, если я запрашиваю только основную таблицу (результирующий DataFrame имеет такое же количество строк и на два столбца с короткими строками меньше), результирующий DataFrame будет таким же быстрым, как и CSV. Даже сохранение только одного столбца (doi) в фрейме данных, полученном в результате операции объединения, не улучшает производительность, и он все еще медленнее, чем фрейм данных с одной таблицей с таким же количеством строк и большим количеством столбцов.

Не могли бы вы помочь мне понять это явление и показать, как решить проблему с производительностью без экспорта в CSV?

Некоторые уточнения:

Я имею в виду разницу в производительности запросов DataFrames, сгенерированных SQL-запросом с/без объединения таблиц, а не производительность присоединения DataFrames.

Мои коды для чтения данных из базы данных SQL Server и запроса полученного DataFrame:

conn = pyodbc.connect(driver='{ODBC Driver 17 for SQL Server}', server='XXX', database='XXX', uid='XXX', trusted_connection='yes')

query_string = """
    SELECT 
        t1.*,
        t2.short_string_column1,
        t3.short_string_column2
    FROM 
        t1
    LEFT JOIN 
        t2
    ON 
        t1.doi = t2.doi
    LEFT JOIN 
        t3
    ON 
        t1.doi = t3.doi
    """
# ~1M rows
df = pd.read_sql(query_string, conn)

# ~20K dois
doi_list = {some interested dois to query}

# this is very slow
for doi in doi_list:
    # I achieved satisfying performance on the single-table DataFrame 
    # but I needed more fields to do my work, so I have to join tables.
    required_intermediate_results = df[df.doi.values == doi]
    ......

# this is 5 to 10 times faster
df.to_csv('df.csv', index=False)
df2 = pd.read_csv('df.csv')

for doi in doi_list:
    # I achieved satisfying performance on the single-table DataFrame 
    # but I needed more fields to do my work, so I have to join tables.
    required_intermediate_results = df2[df2.doi.values == doi]
    ......
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
1
109
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

TLDR о явлении; из приведенного ниже эксперимента мы видим, что pd.read_sql() требуется пропорционально больше памяти, чем pd.read_csv, где разница в использовании памяти увеличивается с размером набора данных. Поскольку разница легко переходит в гигабайты для наборов данных с> 1 миллионом строк, эта разница может быть достаточно большой, чтобы повлиять на производительность последующих операций при использовании pd.read_sql.

Поскольку оба DataFrames используют одинаковый объем памяти после обеспечения четности dtype, и учитывая, что накладные расходы памяти при создании экземпляра DB engine не зависят от размера загружаемого набора данных, результаты подтверждают гипотезу о том, что pd.read_sql() пропорционально больше memory-intensive, чем pd.read_csv .

Рекомендация для OP: учитывая выявленное поведение pd.read_sql(), вероятно, самым простым решением будет сохранить данные как .csv и загрузить их впоследствии. Возможно, вы можете разделить свою программу на два отдельных сценария: первая часть загружает и сохраняет данные, а вторая выполняет запросы.

Подход:

Я использовал ответ SO здесь, чтобы выяснить, сколько памяти использует Python, запустив следующие два фрагмента кода в блокноте Jupyter.

import os
import psutil
import sys

import psycopg2
import pandas as pd
import numpy as np

dbname = "postgres"
user = "postgres"
password = "root"
host = "localhost"
port = "5432"

connection_string = f"dbname = {dbname} user = {user} password = {password} host = {host} port = {port}"
conn = psycopg2.connect(connection_string)

df = pd.read_sql("SELECT * FROM staging_spread", con=conn)

process = psutil.Process()
print(df.shape)  # (195976, 6)
print(process.memory_info().rss/(1024*1024))  # 246.49 MB
print(sys.getsizeof(df)/(1024*1024))  # 30.1 MB

И после перезапуска ядра:

import os
import psutil
import sys

import psycopg2
import pandas as pd
import numpy as np

df2 = pd.read_csv("example.csv", parse_dates=['crawl_ts', 'market_ts'])

process = psutil.Process()
print(df2.shape)  # (195976, 6)
print(process.memory_info().rss/(1024*1024))  # 112.9 MB
print(sys.getsizeof(df2)/(1024*1024))  # 30.1 MB

Загруженный мной набор данных содержит почти 200 000 записей. Я провел тот же эксперимент, используя записи 25 000, 50 000, 75 000 и 100 000. Результаты общего требуемого объема памяти показаны ниже:

|   N Rows |   Total Memory pd.read_sql() |   Total Memory pd.read_csv() |   Abs Diff |   Diff Increase |
|---------:|-----------------------------:|-----------------------------:|-----------:|----------------:|
|   100000 |                       172.77 |                      101.676 |    71.0942 |         18.3342 |
|    75000 |                       151.3  |                       98.54  |    52.76   |         17.1    |
|    50000 |                       130.26 |                       94.6   |    35.66   |         18.37   |
|    25000 |                       108.56 |                       91.27  |    17.29   |        nan      |

Из этой таблицы видно, что на каждые дополнительные 25 000 строк подход с использованием pd.read_sql() требует дополнительных 18 мегабайт. Теперь, если мы экстраполируем этот результат на 1 миллион строк, подход pd.read_sql() требует на 720 МБ больше, чем подход pd.read_csv() для этого набора данных. Набор данных, который я использовал, содержал только 6 столбцов — разница, конечно, также будет увеличиваться, когда загружаются наборы данных с большим количеством столбцов!

Дополнительная информация: df.info() для обоих наборов данных показана четность наборов данных.

df.info() для df (создано с использованием pd.read_sql())

print(df.info())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 195976 entries, 0 to 195975
Data columns (total 6 columns):
 #   Column           Non-Null Count   Dtype         
---  ------           --------------   -----         
 0   crawl_ts         195976 non-null  datetime64[ns]
 1   crawler_version  195976 non-null  object        
 2   ticker           195976 non-null  object        
 3   market_ts        195976 non-null  datetime64[ns]
 4   bid              195976 non-null  float64       
 5   ask              195976 non-null  float64       
dtypes: datetime64[ns](2), float64(2), object(2)
memory usage: 9.0+ MB
None

df.info() для df2 (создано с помощью pd.read_csv())

print(df2.info())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 195976 entries, 0 to 195975
Data columns (total 6 columns):
 #   Column           Non-Null Count   Dtype         
---  ------           --------------   -----         
 0   crawl_ts         195976 non-null  datetime64[ns]
 1   crawler_version  195976 non-null  object        
 2   ticker           195976 non-null  object        
 3   market_ts        195976 non-null  datetime64[ns]
 4   bid              195976 non-null  float64       
 5   ask              195976 non-null  float64       
dtypes: datetime64[ns](2), float64(2), object(2)
memory usage: 9.0+ MB
None

@topsail Я только что обновил свой ответ на основе ваших комментариев. Во-первых, наборы данных в обоих подходах требуют одинакового объема памяти при обеспечении четности dtype. Во-вторых, я провел один и тот же анализ для разных размеров наборов данных, и мы видим, что разница увеличивается пропорционально — это подтверждает ранее неявное предположение о том, что большее количество данных требует больше накладных расходов при использовании pd.read_sql().

Simon David 16.05.2023 07:05

@topsail Я добавил df.info() для обоих наборов данных; Короче говоря, данные вращаются вокруг типичных данных фондового рынка с информацией о спросе и предложении и некоторыми временными метками.

Simon David 17.05.2023 07:32

хм. Ну, когда у меня будет время, я тоже поищу. Это интересно. Но я чувствую, что должен указать, что экономия времени за счет 1) запроса базы данных и загрузки фрейма данных, затем 2) сохранения фрейма данных в csv, затем 3) перезагрузки из фрейма данных из csv... ну, на самом деле это не устраняет потраченное время запрос к базе данных (шаг 1 все еще должен произойти). Но это может быть полезно, если вы по какой-то причине часто повторяете шаг 1 — вы можете думать об этом как о типе кэширования. Также неясно, объясняет ли это причину «более медленного» запроса ... больше памяти не обязательно означает в 5-10 раз больше.

topsail 17.05.2023 15:00

Другие вопросы по теме