dynamically select columns in sql query best practices


使用 mysql 等类型的关系型数据库,最大的一个痛点之一就是写查询语句了,其中一个需求就是动态列查询,网络上的文章千百万,问题和回答也千百万,靠谱不靠谱,对于搜这个问题的人,应该都难分辨。

对于做数据分析,可能不太会有这个需求,大多数情况下,是在构建 API 接口的时候。

如果业务功能需求简单,不常变化,或是 API 接口功能设计上不用支持各种类型的动态列查询,那也罢了。

但是,如果需求百般变化,一个接口要是不支持动态列查询,又如何应对呢?

比如这个:具体要返回哪些数据列,前端要传参,后台要判断是否应该返回。

跟在 select 语句后面、table 名称前面的这段内容:列名称,就是要动态控制的地方。

然而,虽然 sql 语句是文本,初看貌似可以通过动态生成文本的方式来实现,这真的可行么?

答案自然是不行的,为啥?sql 注入(SQL injection)希望你了解下。

pymysql 支持参数化查询,%s 与 %(name)s,是否可以用参数化查询来控制呢?比如这样:

params = {'limit': 10, 'columns': ', '.join(['id', 'name'])}
sql = 'select %(columns)s from test limit %(limit)s'
cursor.execute(sql, params)

测试过,也不行的,你也可以试试看。

所以,动态列查询,到底要如何解决呢?

有人说,这样可能你只能用 ORM 工具来解决了,其实这个方向倒是没错的,毕竟要是自己真的有能力处理,可能最终写出来的,已然跟 ORM 没差多少了。

SQLAlchemy 我是知道一定可以做到的,但是,一定要用 ORM 么,一定要写 Model 么?更何况 SQLAlchemy 的 ORM 模式又要如何进行动态列查询目前也还没找到答案,以及数据序列化又是另外一个问题,文档可能太厚了,而教程又太精炼,且还分散在各处。

终于,我想到了 Pandas,这个东西支持直接就从 sql 数据库读取数据,而且还支持选择想要列,或许可以看看它是如何实现的,从 pd.read_sql() 开始往里翻,让我找到了这个:

    def read(self, coerce_float=True, parse_dates=None, columns=None,
             chunksize=None):

        if columns is not None and len(columns) > 0:
            from sqlalchemy import select
            cols = [self.table.c[n] for n in columns]
            if self.index is not None:
                [cols.insert(0, self.table.c[idx]) for idx in self.index[::-1]]
            sql_select = select(cols)
        else:
            sql_select = self.table.select()

        result = self.pd_sql.execute(sql_select)
        column_names = result.keys()

        if chunksize is not None:
            return self._query_iterator(result, chunksize, column_names,
                                        coerce_float=coerce_float,
                                        parse_dates=parse_dates)
        else:
            data = result.fetchall()
            self.frame = DataFrame.from_records(
                data, columns=column_names, coerce_float=coerce_float)

            self._harmonize_columns(parse_dates=parse_dates)

            if self.index is not None:
                self.frame.set_index(self.index, inplace=True)

            return self.frame

今天的重点在这一部分:

if columns is not None and len(columns) > 0:
    from sqlalchemy import select
    cols = [self.table.c[n] for n in columns]
    if self.index is not None:
        [cols.insert(0, self.table.c[idx]) for idx in self.index[::-1]]
    sql_select = select(cols)
else:
    sql_select = self.table.select()

result = self.pd_sql.execute(sql_select)
column_names = result.keys()

赤裸裸地用着 sqlalchemy 的 select ,这是 SQLAlchemy Core 里面的东西,号称 No ORM Required。SQLAlchemy ORM 也是基于 Core 的。

虽然 pandas 在文档中明确地指出:

'''
columns: list, default: None
    List of column names to select from sql table (only used when reading a table).
'''

意思就是只能在读取一个 table 的时候才能使用 columns 参数。

不过这个是 pandas 所建立的高层逻辑,而所使用的 select 底层其实支持更丰富的查询方法,具体可以参考 Selecting 教程。

核心看这句:cols = [self.table.c[n] for n in columns],利用 table.c[n] 这个方法从列名称构建了列查询,实现了动态列查询的功能。

为啥之前看教程时候没想到这个,因为看到的都是这么写的:table.c.name,而不是 table.c[name],根本没想到可以往字符变量上去靠,现在想想,其实把字符赋值给一个变量名称,然后再使用,也就是一样的效果。脑袋没转过来,毕竟思维定势了。

不过依然还是有个前提,表对象是要存在的,字段名也是要存在的,否则就会报错的。

中间的这个 c,其实就是 columns : An alias for the columns attribute. 它的类型是 ImmutableColumnCollection,简单认为是一个只读的字典就好了。

如果你有一个 user 表,列名称叫 ‘username’,

col = user.c['username']

知道这个方法,接下来基本上就看你自由发挥了。关于表和列的更深入的知识,请参考 SQLAlchemy 的教程说明和 API 说明,能不能挖出宝来就自己了,我是真的会容易看晕:

也尝试构建使用多个表的动态列查询:

columns_user = ['id', 'name']
columns_order = ['id', 'amount']
cols_user = [user.c[n] for n in columns_user]
cols_order = [order.c[n] for n in columns_order]
sql_select = select(cols_user + cols_order).limit(10)

又或者可以是这样:

tables = {'user': user, 'order': order}
columns = ['user.id', 'user.name', 'order.id', 'order.amount']
cols = []
for n in columns:
    t_name, c_name = n.split('.')
	cols.append(tables.get(t_name).c[c_name])
sql_select = select(cols).limit(10)

至此,还没有用到 ORM 层的东西,唯独需要预先做的事情的就是去定义表结构对象。