Pregunta

¿Por qué las funciones con valor escalar parecen hacer que las consultas se ejecuten de forma acumulativamente más lenta a medida que se utilizan?

Tengo esta tabla que fue construida con datos comprados a un tercero.

He recortado algunas cosas para acortar esta publicación ... pero solo para que tengas una idea de cómo están configuradas las cosas.

CREATE TABLE [dbo].[GIS_Location](
        [ID] [int] IDENTITY(1,1) NOT NULL, --PK
        [Lat] [int] NOT NULL,
        [Lon] [int] NOT NULL,
        [Postal_Code] [varchar](7) NOT NULL,
        [State] [char](2) NOT NULL,
        [City] [varchar](30) NOT NULL,
        [Country] [char](3) NOT NULL,

CREATE TABLE [dbo].[Address_Location](
    [ID] [int] IDENTITY(1,1) NOT NULL, --PK
    [Address_Type_ID] [int] NULL,
    [Location] [varchar](100) NOT NULL,
    [State] [char](2) NOT NULL,
    [City] [varchar](30) NOT NULL,
    [Postal_Code] [varchar](10) NOT NULL,
    [Postal_Extension] [varchar](10) NULL,
    [Country_Code] [varchar](10) NULL,

Luego tengo dos funciones que buscan LAT y LON.

CREATE FUNCTION [dbo].[usf_GIS_GET_LAT]
(
    @City VARCHAR(30),
    @State CHAR(2)
)
RETURNS INT 
WITH EXECUTE AS CALLER
AS
BEGIN
    DECLARE @LAT INT

    SET @LAT = (SELECT TOP 1 LAT FROM GIS_Location WITH(NOLOCK) WHERE [State] = @State AND [City] = @City)

RETURN @LAT
END


CREATE FUNCTION [dbo].[usf_GIS_GET_LON]
(
    @City VARCHAR(30),
    @State CHAR(2)
)
RETURNS INT 
WITH EXECUTE AS CALLER
AS
BEGIN
    DECLARE @LON INT

    SET @LON = (SELECT TOP 1 LON FROM GIS_Location WITH(NOLOCK) WHERE [State] = @State AND [City] = @City)

RETURN @LON
END

Cuando ejecuto lo siguiente ...

SET STATISTICS TIME ON

SELECT
    dbo.usf_GIS_GET_LAT(City,[State]) AS Lat,
    dbo.usf_GIS_GET_LON(City,[State]) AS Lon
FROM
    Address_Location WITH(NOLOCK)
WHERE
    ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC)

SET STATISTICS TIME OFF

100 ~ = 8 ms, 200 ~ = 32 ms, 400 ~ = 876 ms

--Editar Lo siento, debería haber sido más claro. No estoy buscando para ajustar la consulta enumerada anteriormente. Esto es solo una muestra para mostrar que el tiempo de ejecución se hace más lento a medida que más registros se procesan. En la aplicación del mundo real, las funciones se utilizan como parte de una cláusula where para construir un radio alrededor de una ciudad y estado para incluir todos los registros en esa región.

¿Fue útil?

Solución

En la mayoría de los casos, es mejor evitar las funciones de valor escalar que hacen referencia a las tablas porque (como dicen otros) son básicamente cajas negras que deben ejecutarse una vez por cada fila y no pueden ser optimizadas por el motor del plan de consultas. Por lo tanto, tienden a escalarse linealmente incluso si las tablas asociadas tienen índices.

Es posible que desee considerar el uso de una función con valores de tabla en línea, ya que se evalúan en línea con la consulta y se pueden optimizar. Obtiene la encapsulación que desea, pero el rendimiento de pegar las expresiones directamente en la declaración de selección.

Como efecto secundario de estar en línea, no pueden contener ningún código de procedimiento (no declarar @variable; set @variable = ..; return). Sin embargo, pueden devolver varias filas y columnas.

Podrías reescribir tus funciones algo como esto:

create function usf_GIS_GET_LAT(
    @City varchar (30),
    @State char (2)
)
returns table
as return (
  select top 1 lat
  from GIS_Location with (nolock) 
  where [State] = @State
    and [City] = @City
);

GO

create function usf_GIS_GET_LON (
    @City varchar (30),
    @State char (2)
)
returns table
as return (
  select top 1 LON
  from GIS_Location with (nolock)
  where [State] = @State
    and [City] = @City
);

La sintaxis para usarlos también es un poco diferente:

select
    Lat.Lat,
    Lon.Lon
from
    Address_Location with (nolock)
    cross apply dbo.usf_GIS_GET_LAT(City,[State]) AS Lat
    cross apply dbo.usf_GIS_GET_LON(City,[State]) AS Lon
WHERE
    ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC)

Otros consejos

No lo hacen.

No hay ningún error en las funciones escalares que haga que su rendimiento se degrade exponencialmente dependiendo del número de filas en las que se ejecuta la función escalar. Intente sus pruebas nuevamente y eche un vistazo al analizador de SQL, mirando las columnas CPU y READS y DURATION. Aumente el tamaño de la prueba para incluir pruebas que duren más de un segundo, dos segundos, cinco segundos.

CREATE FUNCTION dbo.slow
(
    @ignore int
)
RETURNS INT 
AS
BEGIN
    DECLARE @slow INT
    SET @slow = (select count(*) from sysobjects a 
        cross join sysobjects b 
        cross join sysobjects c 
        cross join sysobjects d 
        cross join sysobjects e 
        cross join sysobjects f
    where a.id = @ignore) 

    RETURN @slow
END
go
SET STATISTICS TIME ON

select top 1 dbo.slow(id)
from sysobjects
go
select top 5 dbo.slow(id)
from sysobjects
go
select top 10 dbo.slow(id)
from sysobjects
go
select top 20 dbo.slow(id)
from sysobjects
go
select top 40 dbo.slow(id)
from sysobjects

SET STATISTICS TIME OFF

Salida

SQL Server Execution Times:
   CPU time = 203 ms,  elapsed time = 202 ms.


SQL Server Execution Times:
   CPU time = 889 ms,  elapsed time = 939 ms.

SQL Server Execution Times:
   CPU time = 1748 ms,  elapsed time = 1855 ms.

SQL Server Execution Times:
   CPU time = 3541 ms,  elapsed time = 3696 ms.


SQL Server Execution Times:
   CPU time = 7207 ms,  elapsed time = 7392 ms.

Tenga en cuenta que si ejecuta una función escalar contra filas en el conjunto de resultados, la función escalar se ejecutará por fila sin optimización global.

Puede envolver su funcionalidad en un TVF en línea, que será mucho más rápido:

http : //sqlblog.com/blogs/alexander_kuznetsov/archive/2008/05/23/reuse-your-code-with-cross-apply.aspx

usted llama a la función dos veces (dos hits selectos al DB) por cada fila en el conjunto de resultados.

para que su consulta sea más rápida, únase a GIS_Location y omita las funciones:

SELECT
    g.Lat,
    g.Lon
FROM
    Address_Location        l WITH(NOLOCK)
    INNER JOIN GIS_Location g WITH(NOLOCK) WHERE l.State = g.State AND l.City = g.City
WHERE
    ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC)

No estoy seguro de por qué el NOLOCK, o la cláusula de locura, acabo de copiar de la pregunta ...

En pocas palabras, porque las expresiones SQL con funciones definidas por el usuario son menos eficientes que las expresiones SQL sin ellas. La lógica de ejecución no puede ser optimizada; y la sobrecarga de la función (incluidos los protocolos de llamada) debe incurrirse en cada fila.

El consejo de KMike es bueno. DONDE .. IN (SELECCIONE algo) no es probable que sea un patrón eficiente, y en este caso puede ser reemplazado fácilmente con un JOIN.

Ver si esto funciona mejor ... ¿O quizás una unión interna distinta?

select a.*,
(select top 1 g.Lat from GIS_Location g where g.City = a.City and g.State = a.State) as Lat,
(select top 1 g.Lon from GIS_Location g where g.City = a.City and g.State = a.State) as Lon
from Address_Location a
where a.ID in (select top 100 ID from Address_Location order by ID desc)

En cuanto al rendimiento de la función escalar, no estoy seguro.

Las funciones escalares típicas son mucho más lentas que las contrapartes de TVF en línea. Afortunadamente para muchos escenarios cambiará.

SQL Server 2019 presentará Scalar UDF Inlining :

  

Una función en el conjunto de funciones de procesamiento inteligente de consultas. Esta función mejora el rendimiento de las consultas que invocan UDF escalares en SQL Server (comenzando con la vista previa de SQL Server 2019)

     

Funciones definidas por el usuario del escalar T-SQL

     

Las funciones definidas por el usuario que se implementan en Transact-SQL y devuelven un solo valor de datos se conocen como funciones definidas por el usuario escalares T-SQL. Las UDF T-SQL son una forma elegante de lograr la reutilización de código y la modularidad en todas las consultas SQL. Algunos cálculos (como las reglas de negocios complejas) son más fáciles de expresar en forma imperativa de UDF. Los UDF ayudan a construir una lógica compleja sin requerir experiencia en escribir consultas SQL complejas.

     

Las UDF escalares suelen tener un rendimiento deficiente debido a las siguientes razones.

     
      
  • invocación iterativa
  •   
  • falta de costos
  •   
  • Ejecución interpretada
  •   
  • Ejecución en serie
  •   
     
     

Inclinación automática de UDF escalares

     

El objetivo de la función de incorporación de UDF Scalar es mejorar el rendimiento de las consultas que invocan UDF escalares T-SQL, donde la ejecución de UDF es el principal cuello de botella.

     

Con esta nueva característica, las UDF escalares se transforman automáticamente en expresiones escalares o subconsultas escalares que se sustituyen en la consulta de llamada en lugar del operador UDF. Estas expresiones y subconsultas son entonces optimizadas. Como resultado, el plan de consulta ya no tendrá un operador de función definido por el usuario, pero sus efectos se observarán en el plan, como las vistas o los TVF en línea.

     
     

Requisitos de UDF escalables en línea

     

Un UDF T-SQL escalar puede estar en línea si se cumplen todas las condiciones siguientes   son ciertas:

     
      
  • La UDF se escribe usando las siguientes construcciones:

         
        
    1. DECLARAR, ESTABLECER: declaración de variables y asignaciones.
    2.   
    3. SELECCIONE: consulta SQL con asignaciones de variable única / múltiple1.
    4.   
    5. IF / ELSE: Ramificación con niveles arbitrarios de anidamiento.
    6.   
    7. DEVOLUCIÓN: declaraciones de devolución únicas o múltiples.
    8.   
    9. UDF: función anidada / recursiva calls2.
    10.   
    11. Otros: operaciones relacionales como EXISTS, ISNULL.
    12.   
  •   
  • El UDF no invoca ninguna función intrínseca que sea dependiente del tiempo (como GETDATE ()) o tenga efectos secundarios3 (como   NEWSEQUENTIALID ()).

  •   
  • La UDF utiliza la cláusula EXECUTE AS CALLER (el comportamiento predeterminado si no se especifica la cláusula EXECUTE AS).
  •   
  • La UDF no hace referencia a variables de tabla o parámetros con valores de tabla.
  •   
  • La consulta que invoca una UDF escalar no hace referencia a una llamada UDF escalar en su cláusula GROUP BY.
  •   
  • La UDF no está compilada de forma nativa (se admite la interoperabilidad).
  •   
  • La UDF no se utiliza en una columna calculada o en una definición de restricción de verificación.
  •   
  • La UDF no hace referencia a tipos definidos por el usuario.
  •   
  • No se han agregado firmas a la UDF.
  •   
  • La UDF no es una función de partición.
  •   

Comprobando si la función es inlinable:

SELECT OBJECT_NAME([object_id]) AS name, is_inlineable
FROM sys.sql_modules
WHERE [object_id] = OBJECT_ID('schema.function_name')

Habilitar / deshabilitar la función en el nivel de la base de datos:

ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = ON/OFF;
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top