惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
Forbes - Security
Forbes - Security
The Last Watchdog
The Last Watchdog
TaoSecurity Blog
TaoSecurity Blog
Schneier on Security
Schneier on Security
SecWiki News
SecWiki News
V
Vulnerabilities – Threatpost
Project Zero
Project Zero
O
OpenAI News
W
WeLiveSecurity
Security Archives - TechRepublic
Security Archives - TechRepublic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
H
Hacker News: Front Page
Cisco Talos Blog
Cisco Talos Blog
Spread Privacy
Spread Privacy
Help Net Security
Help Net Security
P
Privacy & Cybersecurity Law Blog
K
Kaspersky official blog
S
Security @ Cisco Blogs
Latest news
Latest news
AWS News Blog
AWS News Blog
U
Unit 42
Martin Fowler
Martin Fowler
阮一峰的网络日志
阮一峰的网络日志
S
Secure Thoughts
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Know Your Adversary
Know Your Adversary
Scott Helme
Scott Helme
博客园 - 司徒正美
B
Blog RSS Feed
C
Check Point Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
D
Docker
Google Online Security Blog
Google Online Security Blog
Jina AI
Jina AI
aimingoo的专栏
aimingoo的专栏
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Last Week in AI
Last Week in AI
月光博客
月光博客
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
SegmentFault 最新的问题
NISL@THU
NISL@THU
T
The Blog of Author Tim Ferriss
C
Cisco Blogs
Attack and Defense Labs
Attack and Defense Labs
小众软件
小众软件

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
第9章:数据读写——数据库 IO
我才是银古 · 2026-06-22 · via 博客园 - 我才是银古

第9章:数据读写——数据库 IO

在企业级 GIS 应用和大规模空间数据管理中,空间数据库是不可或缺的基础设施。GeoPandas 提供了与 PostGIS、Spatialite 等空间数据库的无缝集成,支持直接从数据库读取空间数据,也支持将处理结果写回数据库。本章将详细介绍 GeoPandas 的数据库 IO 功能。


9.1 空间数据库概述

9.1.1 什么是空间数据库

空间数据库是在传统关系型数据库的基础上,添加了空间数据类型和空间查询功能的数据库系统。它能够存储、索引和查询地理空间数据。

9.1.2 常见空间数据库

数据库 说明 空间扩展 开源
PostgreSQL + PostGIS 最强大的开源空间数据库 PostGIS
SQLite + Spatialite 轻量级嵌入式空间数据库 SpatiaLite
MySQL 内置基本空间功能 内置
Oracle Spatial 企业级空间数据库 Oracle Spatial
SQL Server 微软空间数据库 内置
DuckDB + Spatial 新兴的分析型空间数据库 Spatial 扩展

9.1.3 PostGIS 简介

PostGIS 是 PostgreSQL 数据库的空间扩展,是目前最功能完善、使用最广泛的开源空间数据库。

PostGIS 的核心能力:

  • 存储和索引各种几何类型(点、线、面、多几何等)
  • 空间关系查询(包含、相交、邻近等)
  • 空间分析函数(缓冲区、叠加、最近邻等)
  • 坐标系转换
  • 拓扑分析
  • 栅格数据支持
  • 3D 和 4D 几何支持
-- PostGIS 基本操作示例
-- 创建空间表
CREATE TABLE cities (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    population INTEGER,
    geom GEOMETRY(Point, 4326)
);

-- 创建空间索引
CREATE INDEX idx_cities_geom ON cities USING GIST (geom);

-- 插入数据
INSERT INTO cities (name, population, geom)
VALUES ('北京', 21710000, ST_SetSRID(ST_MakePoint(116.407, 39.904), 4326));

-- 空间查询
SELECT name, population
FROM cities
WHERE ST_DWithin(
    geom,
    ST_SetSRID(ST_MakePoint(116.4, 39.9), 4326),
    0.1
);

9.1.4 Spatialite 简介

Spatialite 是 SQLite 的空间扩展,是一个轻量级的嵌入式空间数据库,无需安装服务器即可使用。

# Spatialite 的优势:
# - 零配置,单文件数据库
# - 无需安装数据库服务器
# - 适合桌面应用和小型项目
# - 可用于测试和原型开发

9.2 数据库连接配置

9.2.1 SQLAlchemy Engine

GeoPandas 使用 SQLAlchemy 作为数据库连接的抽象层。首先需要创建一个数据库引擎(Engine)。

from sqlalchemy import create_engine

# PostgreSQL/PostGIS 连接
engine = create_engine(
    "postgresql://username:password@hostname:5432/database_name"
)

# 连接字符串格式:
# postgresql://用户名:密码@主机:端口/数据库名

9.2.2 连接字符串格式

from sqlalchemy import create_engine

# 1. 基本连接
engine = create_engine("postgresql://user:pass@localhost:5432/mydb")

# 2. 使用 psycopg2 驱动(明确指定)
engine = create_engine("postgresql+psycopg2://user:pass@localhost:5432/mydb")

# 3. 使用 psycopg(v3)驱动
engine = create_engine("postgresql+psycopg://user:pass@localhost:5432/mydb")

# 4. 带额外参数的连接
engine = create_engine(
    "postgresql://user:pass@localhost:5432/mydb",
    echo=False,                # 不打印 SQL 语句
    pool_size=5,               # 连接池大小
    max_overflow=10,           # 最大溢出连接数
    pool_timeout=30,           # 连接超时
    pool_recycle=1800          # 连接回收时间(秒)
)

9.2.3 使用环境变量管理凭据

import os
from sqlalchemy import create_engine

# 从环境变量读取数据库配置
DB_USER = os.environ.get("DB_USER", "postgres")
DB_PASS = os.environ.get("DB_PASS", "password")
DB_HOST = os.environ.get("DB_HOST", "localhost")
DB_PORT = os.environ.get("DB_PORT", "5432")
DB_NAME = os.environ.get("DB_NAME", "gis_db")

engine = create_engine(
    f"postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
)

9.2.4 使用 psycopg 直接连接

import psycopg2

# 直接使用 psycopg2 连接
conn = psycopg2.connect(
    host="localhost",
    port=5432,
    dbname="gis_db",
    user="postgres",
    password="password"
)

# 注意:read_postgis() 也支持直接传入 psycopg2 连接
# 但推荐使用 SQLAlchemy Engine

9.2.5 SQLite/Spatialite 连接

from sqlalchemy import create_engine

# SQLite 连接(无 Spatialite 扩展)
engine = create_engine("sqlite:///my_database.db")

# Spatialite 连接需要特殊处理
import sqlite3

def load_spatialite(dbapi_conn, connection_record):
    """加载 Spatialite 扩展"""
    dbapi_conn.enable_load_extension(True)
    dbapi_conn.load_extension("mod_spatialite")

from sqlalchemy import event
engine = create_engine("sqlite:///my_spatial_db.db")
event.listen(engine, "connect", load_spatialite)

9.3 read_postgis() 详解

9.3.1 基本语法

geopandas.read_postgis(
    sql,                    # SQL 查询语句或表名
    con,                    # 数据库连接(SQLAlchemy Engine 或连接字符串)
    geom_col="geom",        # 几何列名称
    crs=None,               # 坐标参考系统
    index_col=None,         # 用作索引的列
    coerce_float=True,      # 是否将数值转为浮点数
    parse_dates=None,       # 日期列解析
    params=None,            # SQL 参数
    chunksize=None          # 分块读取大小
)

9.3.2 基本读取

import geopandas as gpd
from sqlalchemy import create_engine

# 创建连接
engine = create_engine("postgresql://user:pass@localhost:5432/gis_db")

# 方法1:读取整张表
gdf = gpd.read_postgis(
    "SELECT * FROM provinces",
    con=engine,
    geom_col="geom"
)
print(f"读取 {len(gdf)} 条记录")

# 方法2:使用表名(自动生成 SELECT *)
gdf = gpd.read_postgis(
    "provinces",
    con=engine,
    geom_col="geom"
)

9.3.3 指定几何列

# 当表中有多个几何列时,需要明确指定
gdf = gpd.read_postgis(
    "SELECT id, name, geom, centroid_geom FROM regions",
    con=engine,
    geom_col="geom"  # 指定使用哪个几何列
)

# 如果几何列名不是默认的 "geom"
gdf = gpd.read_postgis(
    "SELECT * FROM my_table",
    con=engine,
    geom_col="the_geom"  # 或 "geometry"、"shape" 等
)

9.3.4 指定 CRS

# 方法1:从数据库自动获取 CRS(如果表定义了 SRID)
gdf = gpd.read_postgis(
    "SELECT * FROM provinces",
    con=engine,
    geom_col="geom"
    # CRS 会从 geometry_columns 表中自动获取
)
print(gdf.crs)  # 自动识别

# 方法2:显式指定 CRS
gdf = gpd.read_postgis(
    "SELECT * FROM provinces",
    con=engine,
    geom_col="geom",
    crs="EPSG:4326"  # 显式指定
)

9.3.5 使用 SQL 查询进行过滤

# 属性过滤
gdf = gpd.read_postgis(
    """
    SELECT name, population, area_km2, geom
    FROM cities
    WHERE population > 5000000
    ORDER BY population DESC
    """,
    con=engine,
    geom_col="geom"
)

# 空间过滤(使用 PostGIS 函数)
gdf = gpd.read_postgis(
    """
    SELECT c.name, c.population, c.geom
    FROM cities c
    WHERE ST_Intersects(
        c.geom,
        ST_MakeEnvelope(115, 39, 117, 41, 4326)
    )
    """,
    con=engine,
    geom_col="geom"
)

# 空间连接查询
gdf = gpd.read_postgis(
    """
    SELECT c.name AS city_name,
           p.name AS province_name,
           c.population,
           c.geom
    FROM cities c
    JOIN provinces p ON ST_Within(c.geom, p.geom)
    WHERE p.name = '广东省'
    """,
    con=engine,
    geom_col="geom"
)

9.3.6 参数化查询

# 使用参数化查询防止 SQL 注入
from sqlalchemy import text

# 方法1:使用 params 参数
gdf = gpd.read_postgis(
    text("SELECT * FROM cities WHERE province = :prov AND population > :pop"),
    con=engine,
    geom_col="geom",
    params={"prov": "广东省", "pop": 1000000}
)

# 方法2:使用 SQLAlchemy text() 的 bindparams
from sqlalchemy import text, bindparam
sql = text(
    "SELECT * FROM cities WHERE name = :name"
).bindparams(bindparam("name", type_=str))

gdf = gpd.read_postgis(sql, con=engine, geom_col="geom", params={"name": "广州"})

9.4 to_postgis() 详解

9.4.1 基本语法

GeoDataFrame.to_postgis(
    name,                   # 目标表名
    con,                    # 数据库连接
    schema=None,            # 数据库 schema
    if_exists="fail",       # 表已存在时的行为
    index=True,             # 是否写入索引
    index_label=None,       # 索引列名
    chunksize=None,         # 分块写入大小
    dtype=None              # 列类型映射
)

9.4.2 基本写入

import geopandas as gpd
from sqlalchemy import create_engine

engine = create_engine("postgresql://user:pass@localhost:5432/gis_db")

# 写入新表
gdf.to_postgis("cities", con=engine)

# 写入时会自动:
# 1. 创建表结构
# 2. 设置几何列类型和 SRID
# 3. 写入数据

9.4.3 if_exists 参数

# 表已存在时的处理策略

# "fail"(默认):如果表存在则报错
gdf.to_postgis("cities", con=engine, if_exists="fail")

# "replace":删除旧表,创建新表
gdf.to_postgis("cities", con=engine, if_exists="replace")

# "append":追加到已有表
gdf.to_postgis("cities", con=engine, if_exists="append")

9.4.4 指定 schema

# 写入到特定 schema
gdf.to_postgis("cities", con=engine, schema="public")
gdf.to_postgis("cities", con=engine, schema="gis_data")

# 确保 schema 存在
from sqlalchemy import text
with engine.connect() as conn:
    conn.execute(text("CREATE SCHEMA IF NOT EXISTS gis_data"))
    conn.commit()

9.4.5 索引控制

# 写入索引(默认)
gdf.to_postgis("cities", con=engine, index=True)

# 不写入索引
gdf.to_postgis("cities", con=engine, index=False)

# 自定义索引列名
gdf.to_postgis("cities", con=engine, index=True, index_label="gid")

9.4.6 dtype 参数 — 自定义列类型

from sqlalchemy import Integer, String, Float
from geoalchemy2 import Geometry

# 显式指定列类型
gdf.to_postgis(
    "cities",
    con=engine,
    dtype={
        "name": String(100),
        "population": Integer,
        "area_km2": Float,
        "geometry": Geometry("POINT", srid=4326)
    }
)

9.4.7 分块写入

# 对于大数据集,使用分块写入以控制内存和事务大小
gdf.to_postgis(
    "large_table",
    con=engine,
    chunksize=10000,  # 每次写入 10000 行
    if_exists="replace"
)

9.4.8 写入后创建空间索引

from sqlalchemy import text

# 写入数据
gdf.to_postgis("cities", con=engine, if_exists="replace")

# 手动创建空间索引(如果没有自动创建)
with engine.connect() as conn:
    conn.execute(text(
        "CREATE INDEX IF NOT EXISTS idx_cities_geometry "
        "ON cities USING GIST (geometry)"
    ))
    conn.commit()

# 分析表以更新统计信息
with engine.connect() as conn:
    conn.execute(text("ANALYZE cities"))
    conn.commit()

9.5 Spatialite 读写

9.5.1 创建 Spatialite 数据库

import sqlite3

# 创建数据库并加载 Spatialite 扩展
conn = sqlite3.connect("my_spatial.db")
conn.enable_load_extension(True)

try:
    conn.load_extension("mod_spatialite")
except Exception as e:
    print(f"加载 Spatialite 扩展失败: {e}")
    print("请确保已安装 libspatialite")

# 初始化空间元数据表
conn.execute("SELECT InitSpatialMetaData(1)")
conn.commit()

9.5.2 使用 GeoPandas 读取 Spatialite

import geopandas as gpd
from sqlalchemy import create_engine, event
import sqlite3

# 创建引擎并加载 Spatialite 扩展
engine = create_engine("sqlite:///my_spatial.db")

@event.listens_for(engine, "connect")
def connect(dbapi_connection, connection_record):
    dbapi_connection.enable_load_extension(True)
    dbapi_connection.load_extension("mod_spatialite")

# 读取数据
gdf = gpd.read_postgis(
    "SELECT *, AsText(geometry) FROM cities",
    con=engine,
    geom_col="geometry"
)

# 或者直接使用 read_file() 读取 Spatialite
gdf = gpd.read_file("my_spatial.db", layer="cities")

9.5.3 使用 GeoPandas 写入 Spatialite

# 方法1:使用 to_file()(推荐,更简单)
gdf.to_file("output.db", driver="SQLite", layer="cities")

# 方法2:通过 GeoPackage 格式(GeoPackage 本身就是 SQLite)
gdf.to_file("output.gpkg", layer="cities")

# 方法3:使用 SQLAlchemy(需要更多配置)
# 需要 GeoAlchemy2 支持

9.5.4 Spatialite vs GeoPackage

特性 Spatialite GeoPackage
基础 SQLite + SpatiaLite 扩展 SQLite(OGC 标准)
需要扩展 是(mod_spatialite)
空间函数 丰富(类似 PostGIS) 有限
互操作性 一般 优秀(OGC 标准)
GeoPandas 支持 通过 SQLAlchemy 原生支持
推荐场景 需要复杂空间查询 数据存储和交换

9.6 GeoAlchemy2 集成

9.6.1 GeoAlchemy2 简介

GeoAlchemy2 是 SQLAlchemy 的地理空间扩展,提供了 ORM 模式下的空间数据操作能力。

# 安装
# pip install geoalchemy2

from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base
from geoalchemy2 import Geometry

Base = declarative_base()

class City(Base):
    __tablename__ = "cities"

    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    population = Column(Integer)
    geom = Column(Geometry("POINT", srid=4326))

9.6.2 使用 ORM 创建表

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

engine = create_engine("postgresql://user:pass@localhost:5432/gis_db")

# 创建所有表
Base.metadata.create_all(engine)

# 插入数据
with Session(engine) as session:
    city = City(
        name="北京",
        population=21710000,
        geom="SRID=4326;POINT(116.407 39.904)"
    )
    session.add(city)
    session.commit()

9.6.3 ORM 空间查询

from sqlalchemy.orm import Session
from geoalchemy2.functions import ST_DWithin, ST_GeomFromText
from geoalchemy2 import WKTElement

with Session(engine) as session:
    # 查找距离某点 0.1 度范围内的城市
    center = WKTElement("POINT(116.4 39.9)", srid=4326)

    cities = session.query(City).filter(
        ST_DWithin(City.geom, center, 0.1)
    ).all()

    for city in cities:
        print(f"{city.name}: 人口 {city.population}")

9.6.4 将 ORM 查询结果转为 GeoDataFrame

import geopandas as gpd
from sqlalchemy import select

# 使用 SQLAlchemy Core 查询
stmt = select(City).where(City.population > 5000000)

# 通过 read_postgis 执行查询
gdf = gpd.read_postgis(
    stmt,
    con=engine,
    geom_col="geom"
)
print(gdf)

9.7 高级 SQL 空间查询

9.7.1 空间关系函数

PostGIS 提供了丰富的空间关系函数,可以在 SQL 中直接使用:

import geopandas as gpd

# ST_Intersects — 相交
gdf = gpd.read_postgis(
    """
    SELECT a.name, a.geom
    FROM buildings a, flood_zones b
    WHERE ST_Intersects(a.geom, b.geom)
    AND b.risk_level = 'high'
    """,
    con=engine,
    geom_col="geom"
)

# ST_Within — 在…之内
gdf = gpd.read_postgis(
    """
    SELECT p.name, p.geom
    FROM pois p, districts d
    WHERE ST_Within(p.geom, d.geom)
    AND d.name = '海淀区'
    """,
    con=engine,
    geom_col="geom"
)

# ST_Contains — 包含
gdf = gpd.read_postgis(
    """
    SELECT d.name, d.geom, COUNT(p.id) as poi_count
    FROM districts d
    LEFT JOIN pois p ON ST_Contains(d.geom, p.geom)
    GROUP BY d.name, d.geom
    """,
    con=engine,
    geom_col="geom"
)

9.7.2 空间分析函数

# ST_Buffer — 缓冲区分析
gdf = gpd.read_postgis(
    """
    SELECT name,
           ST_Buffer(geom::geography, 1000)::geometry as geom
    FROM schools
    """,
    con=engine,
    geom_col="geom"
)

# ST_Distance — 距离计算
gdf = gpd.read_postgis(
    """
    SELECT a.name AS school_name,
           b.name AS hospital_name,
           ST_Distance(a.geom::geography, b.geom::geography) as distance_m
    FROM schools a
    CROSS JOIN LATERAL (
        SELECT name, geom
        FROM hospitals
        ORDER BY a.geom <-> geom
        LIMIT 1
    ) b
    """,
    con=engine,
    geom_col="geom"  # 注意此查询可能需要调整
)

# ST_Union — 合并几何
gdf = gpd.read_postgis(
    """
    SELECT province,
           ST_Union(geom) as geom,
           SUM(population) as total_pop
    FROM cities
    GROUP BY province
    """,
    con=engine,
    geom_col="geom"
)

9.7.3 空间索引利用

# 使用空间索引优化的查询模式
# PostGIS 会自动利用 GIST 索引

# 最近邻查询(KNN)— 使用 <-> 操作符
gdf = gpd.read_postgis(
    """
    SELECT name, population, geom
    FROM cities
    ORDER BY geom <-> ST_SetSRID(ST_MakePoint(116.4, 39.9), 4326)
    LIMIT 10
    """,
    con=engine,
    geom_col="geom"
)

# 边界框过滤 — 使用 && 操作符
gdf = gpd.read_postgis(
    """
    SELECT *
    FROM buildings
    WHERE geom && ST_MakeEnvelope(115, 39, 117, 41, 4326)
    """,
    con=engine,
    geom_col="geom"
)

9.8 大数据量流式读取

9.8.1 chunksize 参数

对于大表,一次性读取可能导致内存不足。使用 chunksize 参数可以分块读取:

import geopandas as gpd
from sqlalchemy import create_engine

engine = create_engine("postgresql://user:pass@localhost:5432/gis_db")

# 分块读取,每次 10000 行
chunks = gpd.read_postgis(
    "SELECT * FROM large_table",
    con=engine,
    geom_col="geom",
    chunksize=10000
)

# chunks 是一个迭代器
results = []
for i, chunk in enumerate(chunks):
    print(f"处理第 {i+1} 块,{len(chunk)} 行")

    # 对每块数据进行处理
    processed = process_chunk(chunk)
    results.append(processed)

# 合并结果
import pandas as pd
final_gdf = gpd.GeoDataFrame(pd.concat(results, ignore_index=True))

9.8.2 服务端游标

# 使用服务端游标减少客户端内存使用
from sqlalchemy import create_engine, text

engine = create_engine(
    "postgresql://user:pass@localhost:5432/gis_db",
    execution_options={
        "stream_results": True  # 启用服务端游标
    }
)

# 配合 chunksize 使用
chunks = gpd.read_postgis(
    "SELECT * FROM very_large_table",
    con=engine,
    geom_col="geom",
    chunksize=50000
)

for chunk in chunks:
    # 处理每个块
    pass

9.8.3 分页读取

def read_paginated(engine, table, geom_col, page_size=50000):
    """使用 OFFSET/LIMIT 分页读取"""
    offset = 0
    all_chunks = []

    while True:
        sql = f"""
            SELECT *
            FROM {table}
            ORDER BY id
            LIMIT {page_size} OFFSET {offset}
        """
        chunk = gpd.read_postgis(sql, con=engine, geom_col=geom_col)

        if len(chunk) == 0:
            break

        all_chunks.append(chunk)
        offset += page_size
        print(f"已读取 {offset} 行...")

    import pandas as pd
    return gpd.GeoDataFrame(pd.concat(all_chunks, ignore_index=True))

# 使用
gdf = read_paginated(engine, "large_table", "geom")

9.9 连接池与性能优化

9.9.1 连接池配置

from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool

# 配置连接池
engine = create_engine(
    "postgresql://user:pass@localhost:5432/gis_db",
    poolclass=QueuePool,
    pool_size=5,           # 常驻连接数
    max_overflow=10,       # 最大溢出连接数
    pool_timeout=30,       # 获取连接的超时时间(秒)
    pool_recycle=1800,     # 连接回收时间(秒)
    pool_pre_ping=True     # 使用前检查连接是否有效
)

# 查看连接池状态
print(engine.pool.status())

9.9.2 批量写入优化

import geopandas as gpd

# 1. 使用适当的 chunksize
gdf.to_postgis("table", con=engine, chunksize=5000)

# 2. 先删除索引,写入后重建(大批量写入时)
from sqlalchemy import text

with engine.connect() as conn:
    # 删除空间索引
    conn.execute(text("DROP INDEX IF EXISTS idx_table_geom"))
    conn.commit()

# 写入数据
gdf.to_postgis("table", con=engine, if_exists="replace")

# 重建索引
with engine.connect() as conn:
    conn.execute(text(
        "CREATE INDEX idx_table_geom ON table USING GIST (geometry)"
    ))
    conn.execute(text("ANALYZE table"))
    conn.commit()

# 3. 使用 COPY 命令(最快的批量写入方式)
# 需要使用 psycopg2 的 copy_expert 功能
# 这超出了 GeoPandas 的直接支持范围

9.9.3 查询优化技巧

# 1. 只选择需要的列
gdf = gpd.read_postgis(
    "SELECT name, population, geom FROM cities",  # 而非 SELECT *
    con=engine,
    geom_col="geom"
)

# 2. 使用空间索引友好的查询
# ✅ 好:使用边界框先过滤
gdf = gpd.read_postgis(
    """
    SELECT *
    FROM buildings
    WHERE geom && ST_MakeEnvelope(115, 39, 117, 41, 4326)
    AND ST_Intersects(geom, some_complex_geometry)
    """,
    con=engine,
    geom_col="geom"
)

# 3. 使用 LIMIT 限制结果集
gdf = gpd.read_postgis(
    "SELECT * FROM pois ORDER BY geom <-> reference_point LIMIT 100",
    con=engine,
    geom_col="geom"
)

9.10 数据库空间索引

9.10.1 空间索引的重要性

空间索引是空间数据库性能的关键。没有空间索引,每次空间查询都需要扫描全表。

-- 创建 GIST 空间索引(PostGIS)
CREATE INDEX idx_cities_geom ON cities USING GIST (geom);

-- 查看索引使用情况
EXPLAIN ANALYZE
SELECT *
FROM cities
WHERE ST_Intersects(geom, ST_MakeEnvelope(115, 39, 117, 41, 4326));

9.10.2 在 Python 中管理空间索引

from sqlalchemy import text, inspect

# 检查表是否有空间索引
def check_spatial_index(engine, table_name, geom_col="geometry"):
    """检查表是否有空间索引"""
    inspector = inspect(engine)
    indexes = inspector.get_indexes(table_name)

    for idx in indexes:
        if geom_col in idx.get("column_names", []):
            print(f"✓ 找到空间索引: {idx['name']}")
            return True

    print(f"⚠️ 表 {table_name} 没有空间索引")
    return False

# 创建空间索引
def create_spatial_index(engine, table_name, geom_col="geometry"):
    """创建空间索引"""
    index_name = f"idx_{table_name}_{geom_col}"
    sql = f"CREATE INDEX IF NOT EXISTS {index_name} ON {table_name} USING GIST ({geom_col})"

    with engine.connect() as conn:
        conn.execute(text(sql))
        conn.execute(text(f"ANALYZE {table_name}"))
        conn.commit()

    print(f"✓ 已创建空间索引: {index_name}")

9.10.3 索引类型对比

索引类型 说明 适用场景
GIST 通用搜索树,PostGIS 默认 空间查询(推荐)
SP-GiST 空间分区 GiST 非均匀分布的数据
BRIN 块范围索引,体积极小 物理有序的大数据集
R-tree Spatialite 使用 Spatialite 数据库

9.11 常见问题排查

9.11.1 连接问题

# 问题1:连接被拒绝
# 解决:检查数据库是否运行、端口是否正确、防火墙设置

try:
    engine = create_engine("postgresql://user:pass@localhost:5432/gis_db")
    with engine.connect() as conn:
        conn.execute(text("SELECT 1"))
    print("✓ 连接成功")
except Exception as e:
    print(f"✗ 连接失败: {e}")

# 问题2:认证失败
# 解决:检查用户名密码、pg_hba.conf 配置

# 问题3:数据库不存在
# 解决:先创建数据库
# createdb -U postgres gis_db

9.11.2 几何列识别问题

# 问题:read_postgis() 无法识别几何列
# 原因:查询结果中的几何列名与 geom_col 参数不匹配

# 解决1:明确指定几何列名
gdf = gpd.read_postgis(
    "SELECT id, name, the_geom FROM my_table",
    con=engine,
    geom_col="the_geom"  # 必须与 SELECT 中的列名一致
)

# 解决2:使用别名
gdf = gpd.read_postgis(
    "SELECT id, name, shape AS geom FROM my_table",
    con=engine,
    geom_col="geom"
)

9.11.3 CRS 问题

# 问题:读取后 CRS 为 None
# 原因:几何列没有注册 SRID

# 检查 SRID
with engine.connect() as conn:
    result = conn.execute(text(
        "SELECT ST_SRID(geom) FROM my_table LIMIT 1"
    ))
    srid = result.scalar()
    print(f"SRID: {srid}")

# 如果 SRID 为 0 或 -1,需要手动设置
gdf = gpd.read_postgis(
    "SELECT * FROM my_table",
    con=engine,
    geom_col="geom",
    crs="EPSG:4326"  # 显式指定 CRS
)

# 修复数据库中的 SRID
with engine.connect() as conn:
    conn.execute(text(
        "SELECT UpdateGeometrySRID('my_table', 'geom', 4326)"
    ))
    conn.commit()

9.11.4 编码问题

# 问题:中文乱码
# 解决:确保连接使用 UTF-8 编码

engine = create_engine(
    "postgresql://user:pass@localhost:5432/gis_db"
    "?client_encoding=utf8"
)

# 或者在 PostgreSQL 配置中设置
# ALTER DATABASE gis_db SET client_encoding TO 'UTF8';

9.11.5 性能问题

# 问题:查询非常慢
# 排查步骤:

# 1. 检查是否有空间索引
with engine.connect() as conn:
    result = conn.execute(text("""
        SELECT indexname
        FROM pg_indexes
        WHERE tablename = 'my_table'
    """))
    indexes = result.fetchall()
    print("索引列表:", indexes)

# 2. 使用 EXPLAIN ANALYZE 分析查询
with engine.connect() as conn:
    result = conn.execute(text("""
        EXPLAIN ANALYZE
        SELECT * FROM my_table
        WHERE ST_Intersects(geom, ST_MakeEnvelope(115, 39, 117, 41, 4326))
    """))
    for row in result:
        print(row[0])

# 3. 检查表大小和行数
with engine.connect() as conn:
    result = conn.execute(text("""
        SELECT
            pg_size_pretty(pg_total_relation_size('my_table')) as total_size,
            (SELECT count(*) FROM my_table) as row_count
    """))
    row = result.fetchone()
    print(f"表大小: {row[0]}, 行数: {row[1]}")

9.12 本章小结

本章详细介绍了 GeoPandas 与空间数据库的交互,涵盖了以下核心内容:

主题 关键要点
空间数据库 PostGIS 是最强大的开源选择,Spatialite 适合轻量级场景
连接配置 使用 SQLAlchemy Engine,支持连接池
read_postgis() 支持 SQL 查询、几何列指定、CRS 设置、参数化查询
to_postgis() 支持表创建、追加、替换,自动处理几何类型
Spatialite 零配置嵌入式空间数据库,通过 read_file() 更简单
GeoAlchemy2 SQLAlchemy 的空间扩展,支持 ORM 模式
空间 SQL ST_Intersects、ST_Within、ST_Buffer 等函数
流式读取 chunksize 参数控制内存使用
性能优化 连接池、空间索引、查询优化

核心建议:

  • 大型项目使用 PostGIS,小型项目可用 Spatialite 或 GeoPackage
  • 始终创建空间索引
  • 使用参数化查询防止 SQL 注入
  • 大数据集使用分块读取(chunksize)
  • 批量写入前删除索引,写入后重建
  • 使用环境变量管理数据库凭据,避免硬编码

下一章我们将学习 GeoPandas 中的几何属性与度量功能,了解如何获取几何对象的各种属性信息。