首页 > 解决方案 > 为什么我的查询在参数化时会中断?

问题描述

我有 2 张桌子 -SalesProduct. Sales可以将产品存储为IdnName(旧版设计),并且该Type列指定与其关联的实际类型Productetc. 是一个子表,它被连接这个表中以获取真实数据。(在本例中,Product是一个存储Idn的表来演示问题。)

Sales

|------------|--------------------|----------------|
|    Idn     |  Product Idn/Name  |     Type       |
|------------|--------------------|----------------|
|     1      |          1         |     Number     |
|------------|--------------------|----- ----------|
|     2      |       Colgate      |      Word      |
|------------|--------------------|----------------|

Product (Idn)

|------------|------------------|
|    Idn     |    Some Info     |
|------------|------------------|
|     1      |       ...        |
|------------|------------------|

通常,您不应该加入这些表,Product Idn因为它包含混合数据;但是,如果您选择 LHS 与 RHS 匹配的行,则可以正常工作(1)。例如,如果Product是存储Idn的表,则以下查询失败:

SELECT * from sales JOIN product on sales.pid = product.idn

但以下查询有效:

SELECT * from sales JOIN product on sales.pid = product.idn WHERE type = 'Number'

这也可以在 Python 2 + SQLAlchemy + PyODBC 中按预期工作。但是,当我在 Python 3 + SQLAlchemy + PyODBC 中尝试此操作时,它给了我一个数据类型转换错误,并且仅在查询参数化时发生!

现在,如果我u'number'在 Python 2 中实现它,它也会在那里中断;并b'number'在 Python 3 中工作!我猜Unicode转换存在一些问题。它是否试图猜测编码并做错了什么?我可以通过更明确来解决这个问题吗?

收到的错误是:

Traceback (most recent call last):
  File "reproduce.py", line 59, in <module>
    print(cursor.execute(select_parametrized, ('number', 1)).fetchall())
pyodbc.ProgrammingError: ('42000', '[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Error converting data type varchar to numeric. (8114) (SQLFetch)

这里可能是什么问题,有没有什么好的方法可以绕过这个问题而不做类似的事情convert(因为它在以前的版本中有效)?

这是一个查询,可用于重现此问题而没有副作用(需要SQLAlchemyPyODBC):

import sqlalchemy
import sqlalchemy.orm

create_tables = """
CREATE TABLE products(
    idn NUMERIC(9) PRIMARY KEY
);
CREATE TABLE sales(
    idn NUMERIC(9) PRIMARY KEY,
    pid VARCHAR(50) NOT NULL,
    type VARCHAR(10) NOT NULL
);
"""

check_tables_exist = """   
SELECT * FROM products;
SELECT * FROM sales;
"""

insert_values = """
INSERT INTO products (idn) values (1);
INSERT INTO sales (idn, pid, type) values (1, 1, 'number');
INSERT INTO sales (idn, pid, type) values (2, 'Colgate', 'word');
"""

select_adhoc = """
SELECT * FROM products
JOIN sales ON products.idn = sales.pid
AND sales.type = 'number'
WHERE products.idn in (1);
"""

select_parametrized = """
SELECT * FROM products
JOIN sales ON products.idn = sales.pid
AND sales.type = ?
WHERE products.idn in (?);
"""

delete_tables = """
DROP TABLE products;
DROP TABLE sales;
"""

engine = sqlalchemy.create_engine('mssql+pyodbc://user:password@dsn')
connection = engine.connect()
cursor = engine.raw_connection().cursor()

Session = sqlalchemy.orm.sessionmaker(bind=connection)
session = Session()

session.execute(create_tables)

try:
    session.execute(check_tables_exist)
    session.execute(insert_values)
    session.commit()
    print(cursor.execute(select_adhoc).fetchall())
    print(cursor.execute(select_parametrized, ('number', 1)).fetchall())
finally:
    session.execute(delete_tables)
    session.commit()

1.这是一个错误的假设。它偶然起作用 - SQL 的执行计划优先考虑这种情况,如此所述。当它变成NVARCHAR.

标签: pythonsqlsql-server

解决方案


SQLAlchemy 使用您的非参数化查询 ( select_adhoc) 生成此 SQL 脚本:

SELECT * FROM products
JOIN sales ON products.idn = sales.pid
AND sales.type = 'number'
WHERE products.idn in (1);

但是使用参数化查询 ( select_parametrized),它会生成以下内容:(我从 SQL Server Profiler 中检查过。)

declare @p1 int
set @p1=NULL
exec sp_prepexec @p1 output,N'@P1 nvarchar(12),@P2 int',N'
SELECT * FROM products
INNER JOIN sales ON products.idn = sales.pid 
    AND sales.type = @P1
WHERE products.idn in (@P2);
',N'number',1
select @p1

如果您在 SQL Server 上尝试此操作,您将得到相同的异常:

消息 8114,级别 16,状态 5,第 32 行将数据类型 varchar 转换为数字时出错。

问题出在参数声明上——它对(的类型)@P1进行了隐式转换,这导致了这个问题。可能 Python 2 会生成 varchar?varcharsales.type

如果您像这样更改查询,它将正常工作;或者您需要将类型更改sales.typenvarchar.

select_parametrized = """
SELECT * FROM products
INNER JOIN sales ON products.idn = sales.pid 
    AND sales.type = CAST(? AS VARCHAR(50))
WHERE products.idn in (?);
"""

推荐阅读