1. 两种读取方式到底差在哪:从一次数据拉取卡顿说起
如果你用 Python 做过数据查询,大概率写过这两种代码:一种是自己建 cursor、execute、fetchall,再手动拼 DataFrame;另一种是直接pd.read_sql_query()一行搞定。表面看只是代码长短的区别,但真正跑起来,返回结构、内存占用、参数绑定方式完全不同,选错了在几十万行数据面前会非常难受。
这篇聚焦 Python 中pd.read_sql_query()与cursor.execute()两种数据库读取方式在返回结构、内存占用、参数绑定上的差异,同时把查询通道统一到 TaoToken 的 API 通道上,让你在同一个 Key 下对比两种写法。适合已经会写基础 SQL、正在做数据清洗或报表脚本的人,也适合刚接触 pandas 想搞清楚底层发生了什么的新手。
核心检索词先明确:pd.read_sql_query()是 pandas 提供的函数,直接把 SQL 查询结果转成 DataFrame;cursor.execute()是 DB-API 标准方法,执行后需要自己fetchall()再构造 DataFrame。前者省事,后者可控。我试过在同一个查询上分别用两种方式跑,返回的行数一样,但内存峰值差了将近一倍,原因就在 fetchall 一次性把结果全塞进 Python 列表。
下面按「问题场景 → 通道准备 → 可复制配置 → 验证对比 → 报错排查 → 后续入口」的顺序展开,每一步都给可运行的代码和结果说明。
2. TaoToken 统一 Key 前置准备:让两种写法共用一条查询通道
2.1 为什么要把查询通道统一
很多人在本地用 sqlite3 测试,上了生产换成 MySQL 或 PostgreSQL,连接参数、驱动、占位符全变了,两种读取方式的差异被环境差异掩盖。把查询通道统一到 TaoToken 的 API 通道后,你只需要维护一份 Base URL 和 Key,切换数据库时改的是 SQL 方言,不是连接逻辑。
TaoToken 在这里的角色是统一入口:你通过它拿到 API Key,再配合数据库连接串完成查询。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api (不加 UTM)。注意,TaoToken 是 API 通道,不是数据库本身,数据库连接仍然由你本地的驱动负责。
2.2 拿到 Key 后要准备的三件套
无论你用pd.read_sql_query()还是cursor.execute(),都需要三样东西:Base URL、API Key、Model ID(如果你同时要调用模型做结果解释)。这三件套在 TaoToken 控制台里都能找到。
- Base URL:
https://taotoken.net/api - API Key:在控制台 API Keys 页面生成,形如
sk-开头 - Model ID:按你实际调用的模型填写,比如做数据摘要时用到的对话模型
生成 Key 的入口:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API Keys 管理页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
2.3 环境依赖安装
pip install pandas sqlalchemy pymysql python-dotenv如果你用 sqlite3,标准库自带,不用额外装。用 MySQL 就装 pymysql,用 PostgreSQL 装 psycopg2。pandas 版本建议 2.x,read_sql_query在 2.x 里对连接对象的要求更明确。
2.4 把 Key 写进环境变量而不是代码
# .env 文件 TAOTOKEN_API_KEY=sk-你的key TAOTOKEN_BASE_URL=https://taotoken.net/api DB_URL=mysql+pymysql://user:pass@127.0.0.1:3306/demoimport os from dotenv import load_dotenv load_dotenv() API_KEY = os.getenv("TAOTOKEN_API_KEY") BASE_URL = os.getenv("TAOTOKEN_BASE_URL") DB_URL = os.getenv("DB_URL")这样两种读取方式共用同一份配置,对比时才不会因为连接参数不同产生干扰。
3. 可复制配置:两种写法的完整代码与参数绑定对照
3.1 方式一:cursor.execute() + fetchall()
import sqlite3 import pandas as pd conn = sqlite3.connect("demo.db") cursor = conn.cursor() start_date = "2024-01-01" end_date = "2024-03-31" sql = """ SELECT date, city, gdp FROM table_1 WHERE date >= ? AND date <= ? """ cursor.execute(sql, (start_date, end_date)) rows = cursor.fetchall() df_cursor = pd.DataFrame(rows, columns=["date", "city", "gdp"]) cursor.close() conn.close() print(df_cursor.shape) print(df_cursor.dtypes)这里的关键点:execute的第二个参数是元组,占位符用?(sqlite3)或%s(pymysql)。返回的rows是列表,每个元素是元组,pandas 拿到后需要手动指定列名,否则列名是 0、1、2。
3.2 方式二:pd.read_sql_query()
import sqlite3 import pandas as pd conn = sqlite3.connect("demo.db") start_date = "2024-01-01" end_date = "2024-03-31" sql = """ SELECT date, city, gdp FROM table_1 WHERE date >= ? AND date <= ? """ df_pd = pd.read_sql_query(sql, conn, params=(start_date, end_date)) conn.close() print(df_pd.shape) print(df_pd.dtypes)read_sql_query的params参数同样支持元组或字典,列名自动从游标描述里取,不用手写。返回的 dtype 也会根据数据库类型做推断,比如 date 列可能直接是 datetime64。
3.3 参数绑定对照表
| 维度 | cursor.execute() | pd.read_sql_query() |
|---|---|---|
| 占位符 | ?或%s | ?或%s,同驱动 |
| 传参形式 | 元组/字典 | params=元组/字典 |
| 列名来源 | 手动指定 | 自动从 cursor.description 取 |
| 返回类型 | list[tuple] | DataFrame |
| 类型推断 | 无,需 astype | 自动推断 |
| 内存峰值 | 高(fetchall 全量列表) | 较低(分块可配) |
3.4 用 SQLAlchemy 引擎统一连接
from sqlalchemy import create_engine import pandas as pd engine = create_engine(DB_URL) sql = "SELECT date, city, gdp FROM table_1 WHERE date >= %(start)s AND date <= %(end)s" df = pd.read_sql_query( sql, engine, params={"start": "2024-01-01", "end": "2024-03-31"} )用 SQLAlchemy 引擎的好处是read_sql_query能识别命名参数,且连接池复用更稳。cursor 方式也能用引擎,但要自己engine.raw_connection()。
3.5 分块读取降低内存
chunks = pd.read_sql_query(sql, conn, params=(start_date, end_date), chunksize=10000) for chunk in chunks: process(chunk)chunksize是read_sql_query独有的,cursor 方式要实现同样效果得自己写循环加fetchmany。这是两者在内存控制上的核心差异。
4. 验证请求与成功结果:同一查询跑两种写法看差异
4.1 准备测试数据
import sqlite3 import pandas as pd import numpy as np conn = sqlite3.connect("demo.db") dates = pd.date_range("2024-01-01", "2024-03-31").strftime("%Y-%m-%d") cities = ["北京", "上海", "广州", "深圳"] data = [] for d in dates: for c in cities: data.append((d, c, np.random.randint(1000, 9999))) df_seed = pd.DataFrame(data, columns=["date", "city", "gdp"]) df_seed.to_sql("table_1", conn, if_exists="replace", index=False) conn.close()4.2 跑两种写法并打印结果
import time import tracemalloc def run_cursor(): conn = sqlite3.connect("demo.db") cur = conn.cursor() tracemalloc.start() t0 = time.time() cur.execute("SELECT date, city, gdp FROM table_1 WHERE date >= ? AND date <= ?", ("2024-01-01", "2024-03-31")) rows = cur.fetchall() df = pd.DataFrame(rows, columns=["date", "city", "gdp"]) t1 = time.time() current, peak = tracemalloc.get_traced_memory() tracemalloc.stop() conn.close() return df, t1 - t0, peak def run_pd(): conn = sqlite3.connect("demo.db") tracemalloc.start() t0 = time.time() df = pd.read_sql_query( "SELECT date, city, gdp FROM table_1 WHERE date >= ? AND date <= ?", conn, params=("2024-01-01", "2024-03-31")) t1 = time.time() current, peak = tracemalloc.get_traced_memory() tracemalloc.stop() conn.close() return df, t1 - t0, peak df1, t1, m1 = run_cursor() df2, t2, m2 = run_pd() print("cursor 行数:", df1.shape, "耗时:", round(t1, 4), "峰值内存:", m1) print("read_sql_query 行数:", df2.shape, "耗时:", round(t2, 4), "峰值内存:", m2)4.3 实测结果对照
| 指标 | cursor.execute() | pd.read_sql_query() |
|---|---|---|
| 返回行数 | 364 | 364 |
| 列名 | 手动指定 | 自动获取 |
| 耗时(秒) | 0.012 | 0.009 |
| 峰值内存(字节) | 约 1.8M | 约 1.1M |
| date 列 dtype | object | object(sqlite 无原生日期) |
在 MySQL 上跑,read_sql_query的 date 列会直接是 datetime64,cursor 方式拿到的是字符串,还得自己pd.to_datetime。这一步差异在后续做时间序列分析时影响很大。
4.4 用 TaoToken 通道做结果摘要
如果你想把查询结果交给模型做一句话摘要,可以复用同一个 Key:
import requests resp = requests.post( f"{BASE_URL}/v1/chat/completions", headers={"Authorization": f"Bearer {API_KEY}"}, json={ "model": "你的模型ID", "messages": [ {"role": "user", "content": f"用一句话概括这份数据:{df2.head(5).to_dict()}"} ] } ) print(resp.json()["choices"][0]["message"]["content"])模型对话入口:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
5.1 401 Unauthorized
报错原文:{"error": {"message": "Invalid API key", "code": 401}}
原因通常是 Key 没读到或带了多余空格。检查.env里TAOTOKEN_API_KEY是否被引号包裹导致值里含引号,或者环境变量没load_dotenv()。修复:
print(repr(API_KEY)) # 看有没有空格或引号5.2 local proxy failed
报错原文:local proxy failed: connection refused
这类报错一般出现在你本地配了代理但代理没启动。检查HTTP_PROXY、HTTPS_PROXY环境变量,临时清掉再跑:
unset HTTP_PROXY HTTPS_PROXY5.3 reading choices 相关报错
报错原文:KeyError: 'choices'或reading 'choices'
说明返回体不是标准 chat completions 结构,常见于 Base URL 写成了https://taotoken.net而漏了/api。正确写法是https://taotoken.net/api,请求路径拼/v1/chat/completions。
5.4 OAuth 相关报错
报错原文:OAuth token expired或invalid_grant
如果你用的是 Claude Code 或 Codex 这类带 OAuth 的工具,token 过期需要重新授权。检查配置文件里的 Base URL 是否指向https://taotoken.net/api,Key 是否填在正确字段。Claude Code 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
5.5 cursor 方式列名对不上
pd.DataFrame(rows, columns=[...])如果列数和 SQL 查询字段数不一致,会报Shape of passed values is (x, y), indices imply (z, w)。数一下 SELECT 了几个字段,columns 就写几个。
5.6 read_sql_query 参数不生效
用%s占位符却传了元组,在某些驱动下会被当成字符串格式化。统一用params=关键字传参,不要用%手动拼接。
6. 后续入口:把两种写法用顺手的几个建议
如果你只是做一次性取数,pd.read_sql_query()更省事,列名和类型都帮你处理了。如果你要在取数过程中做条件分支、分批处理、或者需要精确控制游标行为,cursor.execute()更灵活。两者不是替代关系,是场景关系。
长期做数据管道或 Agent 类任务,建议把查询逻辑封装成函数,连接参数从环境变量读,Key 统一走 TaoToken。Coding Plan 适合需要长期编码和 Agent 调用的场景:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
接入文档里有各语言的最小示例,遇到驱动差异可以直接对照:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API Key 在控制台随时可以轮换,别把 Key 硬编码进脚本提交到仓库。