2009-04-28 20 views
13

¿Por qué las funciones con valores escalares parecen hacer que las consultas se ejecuten de forma acumulativamente más lenta cuanto más veces en sucesión se utilizan?¿Por qué las funciones de valor escalar de SQL Server se vuelven más lentas?

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

He recortado algunas cosas para hacer que esta publicación sea más corta ... pero solo para que tenga la idea de cómo se configuran 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 el 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 sentimos que debería han sido más claros. No estoy buscando sintonizar la consulta mencionada anteriormente. Esto es solo una muestra para mostrar que el tiempo de ejecución se vuelve más lento cuanto más registros atraviesa. En la aplicación del mundo real, las funciones se utilizan como parte de una cláusula where para crear un radio alrededor de una ciudad y un estado para incluir todos los registros en esa región.

+3

no suba espolvorear los consejos NOLOCK en muestras que no lo necesitan en SO, las cosas de NOLOCK realmente no tienen nada que ver con esta pregunta. –

+0

si no puede deshacerse de las funciones en la "consulta real", entonces siempre será muy lento. Da un mejor ejemplo, con las funciones que se utilizan en WHERE y podemos darte ideas sobre eso ... –

Respuesta

25

En la mayoría de los casos, es mejor evitar las funciones escalares que hacen referencia a las tablas porque (como otros dijeron) son básicamente recuadros negros que deben ejecutarse una vez por cada fila y no pueden ser optimizados por el motor del plan de consultas. Por lo tanto, tienden a escalar 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 pueden optimizarse. Obtiene la encapsulación que desea, pero el rendimiento de pegar las expresiones directamente en la instrucción de selección.

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

Usted podría volver a escribir sus 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) 
+0

Si bien esta es una buena * solución * al problema de rendimiento de OP, realmente no responde a la pregunta: "¿POR QUÉ? las funciones escalares se degradan de forma no lineal? " (Incluso dijiste en tu respuesta: "tienden a escalar linealmente") Simplemente pregunté porque estoy viendo el mismo comportamiento que OP y soy extremadamente curioso en cuanto a POR QUÉ no es lineal. – tbone

+0

@tbone, la pregunta nunca los mencionó degradando de forma no lineal. Deben escalar linealmente en relación con la cantidad de filas que se devuelven, ya que se ejecutarán una vez por fila. Vea la respuesta de sam azafrán para ver un ejemplo de ellos escalando linealmente. –

+0

Las estadísticas que publicó no son lineales: 100 ~ = 8 ms, 200 ~ = 32 ms, 400 ~ = 876 ms – tbone

2

llama a la función dos veces (dos visitas seleccionadas a la base de datos) para cada fila en el conjunto de resultados.

para hacer su consulta más rápido se unen derecho a GIS_Location y evita 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 el loco cláusula where, He copiado de la cuestión ...

+0

Los datos apenas cambian, por lo que la sugerencia de la tabla nolock reduce el tiempo de ejecución ya que no tiene que emitir los bloqueos compartidos. La cláusula crazy where es solo para samplear x registros para que pueda mostrarlo cada vez más lento a medida que avanza. Esto es solo una muestra y no la aplicación del mundo real. En el real, no puedo darme el lujo de unirme a otra mesa, ya que la que estoy tratando es una tabla de legado desnormalizado de bandera. – DBAndrew

+0

@ DBAndrew, si no puede deshacerse de las funciones en la "consulta real", entonces siempre será muy lento. Da un mejor ejemplo, con las funciones que se utilizan en WHERE y podemos darte ideas sobre eso ... –

0

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 generarse para 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 reemplazarse fácilmente con un JOIN.

0

Vea si esto funciona mejor ... ¿O tal vez una combinació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.

6

No lo hacen.

No hay errores en las funciones escalares que hacen que su rendimiento se degrade exponencialmente según el número de filas en las que se ejecute la función escalar. Pruebe nuevamente sus pruebas y eche un vistazo al generador de perfiles SQL, mirando las columnas CPU y READS y DURATION. Aumente su tamaño de prueba para incluir pruebas que demoren más de un segundo, dos segundos o 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 está ejecutando una función escalar en contra de filas del conjunto de resultados, la función escalar será ejecutado por fila sin optimización global.

Cuestiones relacionadas