

# Construcción para la eficiencia con funciones
<a name="limitless-performance-functions"></a>

Las funciones definidas por el usuario no están optimizadas para una sola partición de forma predeterminada, pero se pueden configurar para que se ejecuten como operaciones de una sola partición. Las funciones pueden encapsular la lógica y garantizar que se ejecute de forma optimizada en una sola partición.

## Por qué son importantes las operaciones con una sola partición
<a name="limitless-functions-importance"></a>

La utilización de los recursos es importante para el rendimiento y la rentabilidad. Las operaciones de una sola partición utilizan muchos menos recursos en comparación con las operaciones de particiones cruzadas. Por ejemplo, al ejecutar una función para insertar un millón de filas, la ejecución de una sola partición utiliza aproximadamente 90,5 ACU, en comparación con las 126,5 ACU de la ejecución entre particiones, lo que supone una mejora del 35 % en la eficiencia de los recursos.

La ejecución de una sola partición también proporciona:
+ Un 35 % más de rendimiento que las operaciones con particiones cruzadas
+ Tiempos de respuesta más predecibles
+ Mejor escalabilidad a medida que crecen los datos

## Operaciones y funciones de una sola partición
<a name="limitless-functions-sso"></a>

Las funciones se ejecutan en particiones cuando se cumple cualquiera de estos requisitos previos:
+ La función se crea como inmutable y se incluye en una consulta optimizada de una sola partición.
+ La función la distribuye un usuario.

Las funciones que se ejecutan en particiones funcionan y se escalan mejor porque se ejecutan donde se encuentran los datos.

## Funciones y volatilidad
<a name="limitless-functions-volatility"></a>

Para comprobar la volatilidad de una función, utilice esta consulta en las tablas del sistema de PostgreSQL:

```
SELECT DISTINCT nspname, proname, provolatile 
FROM pg_proc PRO 
JOIN pg_namespace NSP ON PRO.pronamespace = NSP.oid 
WHERE proname IN ('random', 'md5');
```

Ejemplo de código de salida:

```
  nspname   | proname | provolatile 
------------+---------+-------------
 pg_catalog | md5     | i
 pg_catalog | random  | v
(2 rows)
```

En este ejemplo, `md5()` es inmutable y `random()` es volátil. Esto significa que una instrucción optimizada para una sola partición que incluye `md5()` sigue estando optimizada para una sola partición, mientras que una instrucción que incluye `random()` no lo está.

Ejemplo con una función inmutable:

```
EXPLAIN ANALYZE 
SELECT pg_catalog.md5('123') 
FROM s1.t1 
WHERE col_a = 776586194 
  AND col_b = 654849524 
  AND col_c = '3ac2f2affb02987159ccd6ebd23e1ae5';
```

```
                          QUERY PLAN 
----------------------------------------------------
 Foreign Scan  (cost=100.00..101.00 rows=100 width=0) 
               (actual time=3.409..3.409 rows=1 loops=1)
 Single Shard Optimized
 Planning Time: 0.313 ms
 Execution Time: 4.253 ms
(4 rows)
```

Ejemplo con función volátil:

```
EXPLAIN ANALYZE 
SELECT pg_catalog.random() 
FROM s1.t1 
WHERE col_a = 776586194 
  AND col_b = 654849524 
  AND col_c = '3ac2f2affb02987159ccd6ebd23e1ae5';
```

```
                          QUERY PLAN 
------------------------------------------------------
 Foreign Scan on t1_fs00001 t1  
   (cost=100.00..15905.15 rows=1 width=8) 
   (actual time=0.658..0.658 rows=1 loops=1)
 Planning Time: 0.263 ms
 Execution Time: 2.892 ms
(3 rows)
```

La salida muestra que `md5()` se envía y se ejecuta como optimizado para una sola partición, mientras que `random()` no.

## Funciones de distribución
<a name="limitless-functions-distributing"></a>

Una función que accede a datos de una sola partición debe ejecutarse en esa partición para obtener ventajas de rendimiento. La función debe distribuirse y la firma de la función debe incluir la clave de partición completa; todas las columnas de la clave de partición deben pasarse como parámetros a la función.

Función de ejemplo:

```
CREATE OR REPLACE FUNCTION s1.func1(
    param_a bigint, 
    param_b bigint, 
    param_c char(100)
) 
RETURNS int AS $$
DECLARE 
    res int;
BEGIN
    SELECT COUNT(*) INTO res
    FROM s1.t1
    WHERE s1.t1.col_a = param_a
      AND s1.t1.col_b = param_b
      AND s1.t1.col_c = param_c;
    
    RETURN res;
END
$$ LANGUAGE plpgsql;
```

Antes de la distribución, la función no está optimizada para una sola partición:

```
EXPLAIN ANALYZE 
SELECT * FROM s1.func1(776586194, 654849524, '3ac2f2affb02987159ccd6ebd23e1ae5');
```

```
                                              QUERY PLAN 
------------------------------------------------------------------------------------------------------
 Function Scan on func1  (cost=0.25..0.26 rows=1 width=4) 
                         (actual time=37.503..37.503 rows=1 loops=1)
 Planning Time: 0.901 ms
 Execution Time: 51.647 ms
(3 rows)
```

Para distribuir la función:

```
SELECT rds_aurora.limitless_distribute_function(
    's1.func1(bigint,bigint,character)', 
    ARRAY['param_a','param_b','param_c'], 
    's1.t1'
);
```

Tras la distribución, la función se optimiza para una sola partición.

```
EXPLAIN ANALYZE 
SELECT * FROM s1.func1(776586194, 654849524, '3ac2f2affb02987159ccd6ebd23e1ae5');
```

```
                                           QUERY PLAN 
------------------------------------------------------------------------------------------------
 Foreign Scan  (cost=100.00..101.00 rows=100 width=0) 
               (actual time=4.332..4.333 rows=1 loops=1)
 Single Shard Optimized
 Planning Time: 0.857 ms
 Execution Time: 5.116 ms
(4 rows)
```

Puede confirmar la optimización de una sola partición consultando la columna `sso_calls` en `rds_aurora.limitless_stat_statements`:

```
subcluster_id | subcluster_type | calls | sso_calls |                query 
--------------+-----------------+-------+-----------+--------------------------------------
 2            | router          |     2 |         1 | SELECT * FROM s1.func1( $1, $2, $3 )
 3            | router          |     1 |         1 | SELECT * FROM s1.func1( $1, $2, $3 )
(2 rows)
```

## Funciones y patrones de eficiencia
<a name="limitless-functions-efficiency-patterns"></a>

Ejecutar la lógica cerca de los datos es más eficiente y las funciones desempeñan un papel clave para lograrlo. Existen dos casos de uso principales para mejorar la eficiencia con funciones:

1. Extraer las claves de partición de datos complejos para invocar una función optimizada de una sola partición independiente.

1. Convertir las cargas de trabajo entre particiones en una sola partición optimizada separando la lógica entre particiones de las instrucciones optimizadas para una sola partición.

### Extracción de la clave de partición de datos complejos
<a name="limitless-functions-encapsulated-key"></a>

Considere una función con firma `s3.func3(p_json_doc json)` que realiza varias operaciones de base de datos. Estas operaciones se ejecutarán en todas las particiones dentro de una transacción que abarca todas las particiones. Si el documento JSON contiene la clave de partición, puede crear una función optimizada de una sola partición para realizar las operaciones de la base de datos.

Patrón original:

```
s3.func3(p_json_doc json)
    database operation 1;
    database operation 2;
    database operation 3;
```

Patrón optimizado:

```
s3.func3(p_json_doc json)
DECLARE 
    v_a bigint;
BEGIN
    v_a := (p_json_doc->>'field_a')::bigint;
    SELECT s3.func3_INNER(v_a, p_json_doc);
END;
```

Dónde lo hace la función interna:

```
s3.func3_INNER(p_a, p_json_doc)
    database operation 1 WHERE shard_key = p_a;
    database operation 2 WHERE shard_key = p_a;
    database operation 3 WHERE shard_key = p_a;
```

En este patrón, la clave de partición está encapsulada en un tipo de datos complejo o se puede deducir a partir de otros parámetros. La lógica, el acceso a los datos y las funciones pueden determinar, extraer o construir la clave de partición y, a continuación, invocar una función optimizada para una sola partición que realice operaciones relacionadas solo con una partición. Como la interfaz de la aplicación no cambia, la optimización es relativamente fácil de probar.

### Aplazamiento de la clave de partición de otras funciones o datos
<a name="limitless-functions-deferred-key"></a>

Otro patrón de diseño se aplica cuando la lógica o el acceso a los datos calculan o determinan la clave de partición. Esto resulta útil cuando una función se puede ejecutar en una sola partición para la mayoría de las invocaciones, pero ocasionalmente requiere una ejecución entre particiones.

Patrón original:

```
NEWORD(INTEGER, …) RETURNS NUMERIC
DECLARE
    all_whid_local := true;
    LOOP through the order lines
        Generate warehouse ID;
        IF generated warehouse ID == input warehouse ID
        THEN
            ol_supply_whid := input warehouse ID;
        ELSE
            all_whid_local := false;
            ol_supply_whid := generated warehouse ID;
        END IF;
        …
    END LOOP;
    …
    RETURN no_s_quantity;
```

Patrón optimizado con funciones independientes:

```
CREATE OR REPLACE FUNCTION NEWORD_sso(no_w_id INTEGER, …)
RETURNS NUMERIC
…
    RETURN no_s_quantity;
    …
END;
LANGUAGE 'plpgsql';

SELECT rds_aurora.limitless_distribute_function(
    'NEWORD_sso(int,…)', 
    ARRAY['no_w_id'], 
    'warehouse'
);

CREATE OR REPLACE FUNCTION NEWORD_crosshard(no_w_id INTEGER, …)
RETURNS NUMERIC
…
    RETURN no_s_quantity;
    …
END;
LANGUAGE 'plpgsql';
```

A continuación, haga que la función principal llame a la versión optimizada para una sola partición o a la versión entre particiones:

```
IF all_whid_local THEN
    SELECT NEWORD_sso(…) INTO no_s_quantity;
ELSE
    SELECT NEWORD_crosshard(…) INTO no_s_quantity;
END IF;
```

Este enfoque permite que la mayoría de las invocaciones se beneficien de la optimización de una sola partición, al tiempo que se mantiene el comportamiento correcto para los casos que requieren la ejecución entre particiones.

## Comprobación de las operaciones de una sola partición
<a name="limitless-functions-checking-sso"></a>

Use `EXPLAIN` para comprobar si una instrucción está optimizada para una sola partición. El resultado muestra explícitamente “Optimizado para una sola partición” para las operaciones optimizadas.

Invocación entre particiones antes de la distribución:

```
                       QUERY PLAN 
---------------------------------------------------------------------
 Function Scan on func1  (cost=0.25..0.26 rows=1 width=4) 
                         (actual time=59.622..59.623 rows=1 loops=1)
 Planning Time: 0.925 ms
 Execution Time: 60.211 ms
```

Invocación de una sola partición tras la distribución:

```
                       QUERY PLAN 
----------------------------------------------------------------------
 Foreign Scan  (cost=100.00..101.00 rows=100 width=0) 
               (actual time=4.576..4.577 rows=1 loops=1)
 Single Shard Optimized
 Planning Time: 1.483 ms
 Execution Time: 5.404 ms
```

La diferencia en los tiempos de ejecución demuestra la ventaja en cuanto al rendimiento que ofrece la optimización de una sola partición.