sql-serverssmsdjango-pyodbc-azure

Parameterized raw sql query much slower than query with actual values


I am trying to execute a raw sql query with connections[].cursor() in a Django app that connects to SQL server. The query executes much faster (<1s) when I provide the actual vlaues in the query string.

from django.db import connections
     with connections['default'].cursor() as cursor:
        cursor.execute("""                   
                        select c.column1 as c1
                        , ve.column2 as c2
                        from view_example c
                        left join view_slow_view ve on c.k1 = ve.k2
                        where c.column_condition = value_1 and c.column_cd_2 = value2
                        """)
        result = dictfetchall(cursor)

But when I provide the values as params in the cursor.execute() method, the query becomes much slower (2 minutes).

from django.db import connections
     with connections['default'].cursor() as cursor:
        cursor.execute("""                   
                        select c.column1 as c1
                        , ve.column2 as c2
                        from view_example c
                        left join view_slow_view ve on c.k1 = ve.k2
                        where c.column_condition = %s and c.column_condition_2 = %s
                        """, [value_1, value_2])
        contracts_dict_lst = dictfetchall(cursor)

I should also mention that the query is actually slow when executed on SSMS ONLY IF a condition is NOT provided:

 where c.column_condition = value_1 and c.column_cd_2 = value2

It is as if when Django sends the query, it is executed without the parameters (hence the long response time) and then the parameters are provided so the result is filtered.

The values in question are provided by the user, so they change and have to be passed as params and not directly in the query to avoid sql injection. The query is also much more complex than the example given above and doesn't map cleanly to a model so I have to use connection[].cursor()


Solution

  • This is probably parameter sniffing issue. If that's the case, there are couple of solutions. The easiest solution is using query hint.

    Option 1:

    from django.db import connections
         with connections['default'].cursor() as cursor:
            cursor.execute("""                   
                            select c.column1 as c1
                            , ve.column2 as c2
                            from view_example c
                            left join view_slow_view ve on c.k1 = ve.k2
                            where c.column_condition = %s and c.column_condition_2 = %s
                            OPTION(RECOMPILE) -- add this line to your query
                            """, [value_1, value_2])
            contracts_dict_lst = dictfetchall(cursor)
    

    Option 2:

    from django.db import connections
         with connections['default'].cursor() as cursor:
            cursor.execute(""" 
                            declare v1 varchar(100) = %s  -- declare variable and use them
                            declare v2 varchar(100) = %s
                  
                            select c.column1 as c1
                            , ve.column2 as c2
                            from view_example c
                            left join view_slow_view ve on c.k1 = ve.k2
                            where c.column_condition = v1 and c.column_condition_2 = v2
                            """, [value_1, value_2])
            contracts_dict_lst = dictfetchall(cursor)
    

    This is a good link for more reading.