Description
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.