Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type choices in sp_executesql_sql_type cause poor query performance. #1010

Open
Philgo68 opened this issue Mar 3, 2022 · 0 comments
Open

Comments

@Philgo68
Copy link

Philgo68 commented Mar 3, 2022

Using varchar(max) as a fall back parameter type causes SQL Server to make poor execution plan choices, dramatically slowing down queries.

If the type selection in sp_executesql_sql_type was better when the :sqlserver_type was missing, the system would perform MUCH better in these odd situations. Instead the quick logic falls to nvarchar(max) pretty easily:

def sp_executesql_sql_type(attr)
  return attr.type.sqlserver_type if attr.type.respond_to?(:sqlserver_type)
  case value = attr.value_for_database
  when Numeric
    value > 2_147_483_647 ? 'bigint'.freeze : 'int'.freeze
  else
    "nvarchar(max)".freeze
  end
end

We are querying against an ERP database we have little control over and have a "through:" relationship:

class WorkOrderAR
  has_many :operations,
    :primary_key => [:TYPE, :BASE_ID, :LOT_ID, :SPLIT_ID],
    :class_name => 'OperationAR',
    :foreign_key => [:WORKORDER_TYPE, :WORKORDER_BASE_ID, :WORKORDER_LOT_ID, :WORKORDER_SPLIT_ID],
    inverse_of: :work_order

  has_many :requirements, through: :operations, source: :direct_requirements
end

class OperationAR
  has_many :direct_requirements, -> {where(SUBORD_WO_SUB_ID: nil)},
    class_name: "RequirementAR",
    primary_key: [:WORKORDER_TYPE, :WORKORDER_BASE_ID, :WORKORDER_LOT_ID, :WORKORDER_SPLIT_ID, :WORKORDER_SUB_ID, :SEQUENCE_NO],
    foreign_key: [:WORKORDER_TYPE, :WORKORDER_BASE_ID, :WORKORDER_LOT_ID, :WORKORDER_SPLIT_ID, :WORKORDER_SUB_ID, :OPERATION_SEQ_NO],
    inverse_of: :operation
end

When loading Operations:

 workorder = WorkOrderAR.where({TYPE: 'W', BASE_ID: '2021023169', LOT_ID: '1', SPLIT_ID: '0', SUB_ID: '0'}).load.first
 workorder.operations.load

the sqlserver_type is in place and the query is fine:

EXEC sp_executesql N'SELECT [OPERATION].* FROM [OPERATION] WHERE [OPERATION].[WORKORDER_TYPE] = @0 AND [OPERATION].[WORKORDER_BASE_ID] = @1 AND [OPERATION].[WORKORDER_LOT_ID] = @2 AND [OPERATION].[WORKORDER_SPLIT_ID] = @3'
, N'@0 nchar(1), @1 nvarchar(30), @2 nvarchar(3), @3 nvarchar(3)', @0 = N'W', @1 = N'2021023169', @2 = N'1', @3 = N'0'

But when loading Requirements using the :through relationship

workorder.requirements.load

the sqlserver_type is missing and then query uses nvarchar(max):

EXEC sp_executesql N'SELECT [REQUIREMENT].* FROM [REQUIREMENT] INNER JOIN [OPERATION] ON [REQUIREMENT].[WORKORDER_TYPE] = [OPERATION].[WORKORDER_TYPE] AND [REQUIREMENT].[WORKORDER_BASE_ID] = [OPERATION].[WORKORDER_BASE_ID] AND [REQUIREMENT].[WORKORDER_LOT_ID] = [OPERATION].[WORKORDER_LOT_ID] AND [REQUIREMENT].[WORKORDER_SPLIT_ID] = [OPERATION].[WORKORDER_SPLIT_ID] AND [REQUIREMENT].[WORKORDER_SUB_ID] = [OPERATION].[WORKORDER_SUB_ID] AND [REQUIREMENT].[OPERATION_SEQ_NO] = [OPERATION].[SEQUENCE_NO] WHERE [OPERATION].[WORKORDER_TYPE] = @0 AND [OPERATION].[WORKORDER_BASE_ID] = @1 AND [OPERATION].[WORKORDER_LOT_ID] = @2 AND [OPERATION].[WORKORDER_SPLIT_ID] = @3 AND [REQUIREMENT].[SUBORD_WO_SUB_ID] IS NULL'
, N'@0 nvarchar(max), @1 nvarchar(max), @2 nvarchar(max), @3 nvarchar(max)', @0 = N'W', @1 = N'2021023169', @2 = N'1', @3 = N'0'

at least in our case, sql server then picks a terrible execution plan, ignoring the obvious indexes. Changing the parameters in the query from nvarchar(max) to nvarchar(40) takes the time down from 7+seconds to 50 ms.

We have patched in
"nvarchar(#{value.to_s.length + 1})".freeze
to replace the
"nvarchar(max)".freeze
in sp_executesql_sql_type and it has made active record / sql server usable for our application.

The only other types this procedure will return are int and bigint if the value is numeric. This also seems a bit naïve if a float or real got into this area of code, but does not come up in our app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant