从CSV文件加载创建Polars DataFrame比从内存列表加载更快是否符合预期?
这种情况完全是符合预期的,背后主要有这几个关键原因,咱们慢慢拆解:
Polars的CSV读取是高度优化的底层实现
Polars的read_csv基于Rust编写,从文件读取时会用批量解析、多线程处理(默认启用)的方式直接生成它原生的Arrow列式存储格式,整个过程几乎绕开了Python层面的低效循环和类型转换。而你从ClickHouse拉取的是Python元组组成的列表——每个元素都是Python对象,Polars要把这些行式的Python对象转成自己的列式结构,需要在Python层逐个处理,几百万行的话这种开销会被无限放大。内存布局的差异带来的性能差距
你存在列表里的元组是行式的Python对象集合,内存碎片化严重,缓存利用率极低。而Polars读CSV时,会直接按列读取解析,生成连续的Arrow列数据,这种紧凑的内存布局对CPU缓存非常友好,处理速度自然快很多。从行式Python数据转列式Arrow格式的额外内存拷贝和转换成本,比你想象的要大得多。磁盘IO的开销反而小于Python层处理开销
虽然你多了“写CSV再读CSV”的磁盘IO步骤,但如果你的机器用的是SSD,Windows下的磁盘读写速度其实非常快,反而比不上Python处理几百万个元组的开销。而且csv.writerows是批量写入,效率很高,Polars读CSV时又能直接用Rust层处理,跳过了Python的中间环节,整体速度反超就不奇怪了。
给你的优化建议
既然你因为Python 3.12没法用ConnectorX,其实可以试试跳过CSV环节,直接按列的方式收集数据,这样能比CSV方案更快:
def _run_query(self): # 初始化每个列对应的空列表 column_data = {col: [] for col in self.columns} with self.client.query_rows_stream(self.query) as stream: for i, row in enumerate(stream): # 把行数据按列拆分存入对应列表 for col_name, value in zip(self.columns, row): column_data[col_name].append(value) if i % 10000000 == 0: print(f'Collecting row {i}...') return column_data def get_results(self): column_data = self._run_query() # 直接用列字典创建DataFrame,避免行式转列式的开销 df = polars.DataFrame(column_data) # 后续的类型转换、重命名等逻辑保持不变 df = df.with_columns(polars.col('ts').str.strptime(dtype=polars.Datetime, strict=False)) df = df.with_columns(polars.col('ts').alias('date').cast(polars.Date)) df = df.drop('ts') df = df.rename({'imps': 'impressions', 'ssp_price': 'revenue'}) df = df.select( ['date', 'advertiser', 'inventory_ssp_id', 'placement_id', 'platform_id', 'ad_height', 'ad_width', 'country_id', 'impressions', 'revenue'] ) df = df.group_by(df.columns[:-2]).sum() return df
这种方式直接把数据按列收集,Polars可以直接将列表转换成Arrow列,省掉了磁盘IO和行式转列式的双重开销,应该能比CSV方案更快。
备注:内容来源于stack exchange,提问作者ehanson

