Hello Community!
I have not posted here for several years as I have been getting along just 
fine with the excellent SQLAlchemy toolkit and the excellent documentation. 
However, I am trying to do something a bit fancy since all the SQL Server 
dataservers I use are running versions that support OPENJSON. A number of 
our applications have used staging techniques to copy tables of data to the 
dataserver so that that can be used in JOINs (e.g. send up 10,000 
identifiers into the table rather than doing RBAR or batching into IN 
clauses). This technique is IO intensive and has quite a bit of overhead. 
An alternative approach is to serialize the data into a big blob of JSON, 
send it over as a single scalar and then expand it out to a table via a CTE 
on the dataserver. We have found the performance of this to be excellent 
from scripting language, presumably because we are avoiding so many API 
calls. However, I have not found a way to do this cleanly from SQLAlchemy 
and end up using sql.text a lot. Also, because it does not seem possible to 
re-use the same placeholder multiple times in a statement, if we want to 
expand out the JSON multiple times (e.g. if the JSON is nested and has 
multiple tables in it), we work around that using [sys].[sp_executesql]. 
Please see snippet below and a complete script (with create_engine 
redacted) attached.

What I am looking for is some guidance on how to use SQLALchemy to produce 
CTEs that reference the same variable (@blob_o_json) multiple times without 
having to pass in the same value multiple times.

thanks in advance!
pjjH



# TODO
: formulate this as a SQLAlchemy expression (is there an exec() element?)
outer_sql = r'''
EXEC [sys].[sp_executesql] @statement = :sql,
                           @params = N'@bind_value_blob_o_json varchar(max)',
                           @bind_value_blob_o_json = :json_blob
'''

# we prepare the *outer* statement that has a single command in it
statement = sql.text(outer_sql)

# now execute that passing in unicode value of the inner SQL.

logging.basicConfig(format='%(asctime)s %(message)s')
logging.getLogger().setLevel(logging.DEBUG)
nrows = [1000,10000,100000, 200000, 500000]
for n in nrows:
    
# we cons up a list of dicts to represent a single 'inline' table with lots of 
rows
    logging.debug("passing  %d items" % (n))
    blob_o_json =json.dumps([dict(rn=x,foo=x*2,bar=x-1,fruit='banana', 
flavor='yummy') for x in 
range(0,n)])
    
# Note: we named arguments in this next line correspond to the :sql and 
:json_blob placeholders in outer_sql
    result =  engine.execute(statement,sql=six.text_type(inner_sql), 
json_blob=blob_o_json)
    rs = result.fetchall()
    #logging.debug((len(rs), rs[]))
    
# drop down to DBAPI to process each result-set. The application should know 
how many result-sets will be present
    
# I think it is likely that the vast majority of queries will just have a 
single result-set

    
# I did not know how to get to anything other than the first result-set ... I 
thought there 
    
# was probably some pyodbc magic to  make it work with SQLAlchemy and -- sure 
enough! -- there
    
# was a ticket with a workaround: 
https://github.com/sqlalchemy/sqlalchemy/issues/1635#issuecomment-441907416

    #cursor = result.cursor
    #rs1 = cursor.fetchall()
    #cursor.nextset()
    #rs2 = cursor.fetchall()

    #logging.debug((len(rs1), rs1[-4:], rs2[-4:]))
    logging.debug("finished %d" % (n))

logging.debug("done")

-- 
SQLAlchemy - 
The Python SQL Toolkit and Object Relational Mapper

http://www.sqlalchemy.org/

To post example code, please provide an MCVE: Minimal, Complete, and Verifiable 
Example.  See  http://stackoverflow.com/help/mcve for a full description.
--- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/sqlalchemy/345f67ce-a4b9-4575-87bc-81653868908en%40googlegroups.com.
import six
import json
import logging

import urllib

import sqlalchemy
from sqlalchemy import create_engine, sql


# we are using the SQL Express 2017 'metatweaker' instance as that is a readily accessible
# engine that is running a recent enough revision of SQL Server ... OPENJSON is not in SQL 2014
# Also, if you want to see what the performance characteristics of this look like on the server side, you need to 
# have some roles such as VIEW SERVER STATE (e.g. to look at query plans)

# engine = create_engine("mssql+pyodbc://@dbreins.deshaw.com\DBREINS/master?driver=SQL+Server")
# engine = create_engine(r'mssql+pyodbc://@fretsaw.deshaw.com\sqlexpress/metatweaker?driver=SQL+Server')


#engine = create_engine(r"mssql+pyodbc://@frosthawk.deshaw.com\sqlexpress2017/metatweaker?driver=SQL+Server")
#engine = create_engine(r'mssql+pyodbc://@dbreinsdev.deshaw.com\reinsdev,1440/reins?APP=sqlalchemy')
engine = create_engine(r'mssql+pyodbc://@dbreinsdev.deshaw.com\reinsdev:1440/reins?driver=ODBC+Driver+11+for+SQL+Server&APP=sqlalchemy')


#quoted=urllib.parse.quote_plus('Driver={Easysoft ODBC-SQL Server};Server=dbreinshyd3.deshaw.com;port=1433;Application=Python;Kerberos=1;ServerSPN=MSSQLSvc/dbreinshyd3.deshaw.com:1433')

#engine = create_engine('mssql+pyodbc:///?odbc_connect={}'.format(quoted))




inner_sql = r'''
-- do we want to allow multiple blobs of JSON?
declare @blob_o_json varchar(max);

select @blob_o_json=@bind_value_blob_o_json; -- Hopefully this is not string-substituted but, rather, a prepared statement is created
                                             -- with one bind value per entry in @params

-- yay! we can put comments in here too

-- TODO: figure out how to get SQLAlchemy to combine the variable declaration and the CTEs.
WITH T(foo,give_n_a_funny_alias) as (
    SELECT foo,give_n_a_funny_alias
      FROM OPENJSON(@blob_o_json) WITH (
           give_n_a_funny_alias int 'strict $.rn',
           foo int 'strict $.foo'
          ) AS alias_for_the_selectable_from_OPENJSON
      ),T1(n,foo) as (
    SELECT n,foo
      FROM OPENJSON(@blob_o_json) WITH (
           n int 'strict $.rn',
           foo int 'lax $.no_I_am_not_here_in_the_JSON' -- this key is not present ... is we use strict rather than lax, the query would blow up
          ) AS alias_for_the_selectable_from_OPENJSON
      )
SELECT T.foo, T.give_n_a_funny_alias as bar
FROM T -- A CTE derived from @blob_o_json
JOIN dbo.Nums -- JOINing on a vanilla table in the database
ON (T.give_n_a_funny_alias=Nums.n)
JOIN T1 -- Another CTE from the JSON
ON (T1.n = Nums.n)

'''


# Note that the named arguments here match up with the :param_name placeholders in the *outer*
# statement. The @params value says that the parameters of the *inner* SQL are 'bind_value_blob_json'.
# This expansion of parameters at different stages is a little difficult to follow but one should not have to write this kind of code multiple times.
# It looks like variable declarations work in the inner SQL and each statement is executed.

# TODO: formulate this as a SQLAlchemy expression (is there an exec() element?)
outer_sql = r'''
EXEC [sys].[sp_executesql] @statement = :sql,
                           @params = N'@bind_value_blob_o_json varchar(max)',
                           @bind_value_blob_o_json = :json_blob
'''

# we prepare the *outer* statement that has a single command in it
statement = sql.text(outer_sql)

# now execute that passing in unicode value of the inner SQL.

logging.basicConfig(format='%(asctime)s %(message)s')
logging.getLogger().setLevel(logging.DEBUG)
nrows = [1000,10000,100000, 200000, 500000]
for n in nrows:
    # we cons up a list of dicts to represent a single 'inline' table with lots of rows
    logging.debug("passing  %d items" % (n))
    blob_o_json =json.dumps([dict(rn=x,foo=x*2,bar=x-1,fruit='banana', flavor='yummy') for x in range(0,n)])
    # Note: we named arguments in this next line correspond to the :sql and :json_blob placeholders in outer_sql
    result =  engine.execute(statement,sql=six.text_type(inner_sql), json_blob=blob_o_json)
    rs = result.fetchall()
    #logging.debug((len(rs), rs[]))
    # drop down to DBAPI to process each result-set. The application should know how many result-sets will be present
    # I think it is likely that the vast majority of queries will just have a single result-set

    # I did not know how to get to anything other than the first result-set ... I thought there 
    # was probably some pyodbc magic to  make it work with SQLAlchemy and -- sure enough! -- there
    # was a ticket with a workaround: https://github.com/sqlalchemy/sqlalchemy/issues/1635#issuecomment-441907416

    #cursor = result.cursor
    #rs1 = cursor.fetchall()
    #cursor.nextset()
    #rs2 = cursor.fetchall()

    #logging.debug((len(rs1), rs1[-4:], rs2[-4:]))
    logging.debug("finished %d" % (n))

logging.debug("done")

Reply via email to