Construcción para la eficiencia con funciones
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
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
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
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
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
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:
-
Extraer las claves de partición de datos complejos para invocar una función optimizada de una sola partición independiente.
-
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
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
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
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.