SQLAlchemy Float类型列filter/filter_by等值查询不生效问题
问题背景
对存储站点经纬度的表做等值过滤查询拦截重复数据时,数据库层==匹配浮点类型字段返回空结果,但将全量数据加载到内存中做等值判断可以正常匹配到目标记录。
复现代码、模型定义如下:
# 查询逻辑 data_dict = {'lat':33.7838, 'lng':-117.225} filter_test = session.query(Sites.lat, Sites.lng).filter( and_( Sites.lat == data_dict['lat'], Sites.lng == data_dict['lng'] ) ).all() print(f'Filter test: {filter_test}') no_filter = session.query(Sites).all() for rec in no_filter: if rec.lat == data_dict['lat']: print(rec.country, rec.lat, rec.lng) print(rec.lat == data_dict['lat']) # 输出结果 # 数据库层过滤输出:Filter test: [] # 内存遍历输出: # United States 33.7838 -117.225 # True # United States 33.7838 -117.225 # True # 表模型定义 class Sites(Base): __tablename__ = "sites" get_table_name = get_table_name country = Column(String(15)) zipcode = Column(Integer) lat = Column(Float) lng = Column(Float)
根本原因
问题出在浮点数精度不匹配:
- SQLAlchemy中默认的
Float类型映射到数据库的是单精度浮点数(FLOAT),仅占4字节,有效数字精度仅6~7位,存储时会对超出精度的数值做截断。 - Python原生的
float是双精度浮点数,占8字节,有效数字精度为15~17位。 - 数据库层做
==等值匹配时,传入的双精度值和库中存储的截断后的单精度值存在微小的精度差,判断为不相等,因此返回空结果。 - 全量数据拉到内存后,SQLAlchemy会将数据库读出的单精度值转换为Python双精度float类型,截断后的数值和定义的常量值在Python的浮点比较逻辑下判定为相等,因此内存遍历可以匹配到结果。
修复方案
- 优先使用定点数类型存储经纬度:将字段类型改为
Numeric/DECIMAL定点类型,指定固定精度(经纬度保留6位小数即可达到米级定位精度,完全满足站点去重需求),从存储层面消除浮点精度误差,支持精确等值查询。from sqlalchemy import Numeric class Sites(Base): __tablename__ = "sites" get_table_name = get_table_name country = Column(String(15)) zipcode = Column(Integer) # 总长度10位,小数占6位 lat = Column(Numeric(precision=10, scale=6)) lng = Column(Numeric(precision=10, scale=6)) - 保留浮点类型时改用范围查询:如果不想修改字段类型,不要对浮点字段做严格等值判断,增加一个极小的误差容忍区间做范围匹配即可,经纬度场景取1e-6的容忍度(对应地表约10厘米误差)足够覆盖精度偏差。
error_tolerance = 1e-6 filter_test = session.query(Sites.lat, Sites.lng).filter( and_( Sites.lat.between(data_dict['lat'] - error_tolerance, data_dict['lat'] + error_tolerance), Sites.lng.between(data_dict['lng'] - error_tolerance, data_dict['lng'] + error_tolerance) ) ).all() - 对齐浮点精度:将字段类型改为
Double双精度浮点类型,和Python原生float精度对齐,可以减少绝大多数精度不匹配问题,但依然不建议对任何浮点类型做严格等值判断,存在出现偶发精度偏差的可能。from sqlalchemy import Double class Sites(Base): __tablename__ = "sites" get_table_name = get_table_name country = Column(String(15)) zipcode = Column(Integer) lat = Column(Double) lng = Column(Double)
内容的提问来源于stack exchange,提问作者Ian Murray
相关产品推荐
相关产品推荐

