Show HN: Csql – Python lib for composeable SQL queries
github.com
github.com
- write a giant unmaintainable SQL query, or
- pull everything to my PC and use pandas, to take advantage of its ability to build up results piece by piece.
I wrote this library to try and enable that piece-by-piece development approach with SQL queries, without resorting to the mental overhead of full on query builders like SQLAlchemy or Linq.
Seeking feedback - is this useful for you? is it at the right level of abstraction?
I second the recommendation for dbt. Especially if you're following an ELT architecture where you load your data into a data warehouse and then want to transform it. However dbt is more useful for transforming existing data, or aggregating data in batches. It's not a tool for generating SQL expressions on-the-fly like your library would allow you to do.
https://github.com/machow/siuba
One thing I was wondering about the gnarly ast stuff you mention, what about operator overloading? E.g. Q("Select a from" + subquery + "where a < 1")
I created something similar recently: https://docs.racket-lang.org/plisqin/index.html. At its core, it is also a library for composing fragments of SQL, but it has some novel (as far as I know) ideas. The most notable is that joins are values (or "expressions", if you prefer) that can be returned from a procedure like any other value. I was hoping that the world would realize "that's obviously how query builders should work" and copy the approach when starting new projects, but that hasn't happened.
Most difficulties in data processing arise from the need to process multiple tables. It is highly difficult in SQL but it can be also difficult in pandas especially for complex analytical queries. Indeed, in pandas you still have to use the same join and groupby operations as in SQL.
An alternative to SQL, join-groupby, and map-reduce is developed in the Prosto library:
- https://github.com/prostodata/prosto Functions matter! No join-groupby, No map-reduce.
It is a layer over pandas and its main distinguishing feature is that it relies on functions and function operations for data processing as opposed to using only sets and set operations in SQL and other set-oriented approaches
[1] https://www.postgresql.org/docs/12/queries-with.html
[2] https://en.wikipedia.org/wiki/Hierarchical_and_recursive_que...
edit: formatting
They're incredibly handy for a variety of reasons, but can also have unexpected performance impacts. For example, here are functionally equivalent queries written with a CTE vs an inlined/derived table:
WITH cte_foo AS
(SELECT * FROM bar LIMIT 1000000)
SELECT * FROM cte_foo LIMIT 1
and SELECT * FROM
(SELECT * FROM bar LIMIT 1000000)
as inlined_foo LIMIT 1
Depending on the database you're using, those two could have wildly different performance due to a concept called an optimization fence[1]. In Postgres versions 11 and below, the CTE would have truly returned/materialized 1 million rows, then the outer query would execute and ultimately return 1 row for the resultset. Whereas the second version would have been optimized such that the outer LIMIT 1 would have been pushed into the subquery and not materialized those extraneous 999,999 rows to begin with.As mentioned in [1], Postgres 12 (and 13) have started to tackle that optimization fence within Postgres. But it's still a concern/concept to be aware of, since many databases that support CTEs have varying levels of optimization fences, and you'll want to be sure you understand what optimization/performance impacts exist for your particular database before you go down the CTE path.
f"""... WHERE created_on > {p['created_on']}} ..."""
to f"""... WHERE created_on > {p.created_on} ..."""
The latter is a lot more readable and typeable, especially when it's meant to be used in f-strings which place additional restrictions on the usage of quotes. Q("""select 1 from {otherQuery}""")
What does the following code enable that the code above cannot do? Q(lambda: f"""select 1 from {otherQuery}""") function Q(stringBits: string[]): Query {
for (bit in stringBits) {
if (bit is string) {
add to sql
} else if (bit is Query) {
add bit to dependencies
add bit.name to sql
}
}
}
But this cannot be done currently in Python (see PEP-501), so I'm forcing the user to pass a lambda, which I can get the AST of, with which I can implement the machinery to do the above.Suggestions for improvements are welcome!
I also like that the unit of composition is the CTE.
I though SQLAlchemy core goal was exactly that. When does it fails, compared to Csql?
SQLAlchemy can't give me that, instead I'd need to learn a special SQLAlchemy-specific query builder syntax that probably won't support all the analytical functions I want to use anyway. It's really a whole different beast to csql.
What kind of limitations did you encounter in the past?
If you're dealing with largeish tables and several linked CTEs this might get too slow, and you're still stuck on optimizing your queries manually.
AFAIK subqueries do allow predicate pushdown, etc. Maybe for postgres, composability can be achieved through subqueries.
Nevertheless very cool stuff!
Apparently "not all the time with PG12".
https://www.depesz.com/2019/02/19/waiting-for-postgresql-12-...
> SELECT * FROM (SELECT users.* FROM users ORDER BY users.id asc) ORDER BY users.id desc
A sort should never (for some value of never) appear in a subquery because it's meaningless; it can't affect the result. In tsql it's explicitly checked for and reported.
Not having a go, just fyi.
Edit: now I'm more confused by that. The bracketed subquery doesn't have a name but it's apparnetly called 'users' because the last clause is 'ORDER BY users.id desc' - but that's illegal. Again tsql correctly rejects that (once I comment out the illegal inner order by).
Edit 2: sorry about this but FYI, I'd expect the inner sort order to be disregarded anyway, and assuming this is mysql it explicitly is.
"If ORDER BY occurs within a parenthesized query expression and also is applied in the outer query, the results are undefined and may change in a future MySQL version"
https://dev.mysql.com/doc/refman/5.7/en/select.html
(Actually, what the heck is it saying? An outer order by in the presence of an inner order by is undefined overall??)
I'd go to far to say that unless a query has an ORDER BY it is likely a bug to use LIMIT or OFFSET. In the absence of an ORDER BY the database is free to sort the rows any way it likes, and this could change between versions or based on any number of implementation details. If it appears to sort determinsitically with no ORDER BY it should not be relied upon.
Oh, quite true! However the output of that subquery, despite having an order by, will not have a guaranteed order. Ordering is lost as the result set leaves the subquery. This is in the sql standard and in most if not all implementations.
> I'd go to far to say that unless a query has an ORDER BY it is likely a bug to use LIMIT or OFFSET
Wholly agreed.
Interesting.
What about SQLAlchemy makes it unfriendly for analytics code? The last time I looked at it, SQLAlchemy Core had a pretty good fluent interface for writing SQL queries
There's some serious potential in combining Pandas and DuckDB[1], which has an ability to efficiently transfer query results into DataFrames.