Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.
Construire pour l'efficacité grâce aux fonctions
Les fonctions définies par l'utilisateur ne sont pas optimisées par défaut pour une seule partition, mais elles peuvent être configurées pour s'exécuter en tant qu'opérations sur une seule partition. Les fonctions peuvent encapsuler la logique et garantir son exécution de manière optimisée pour une seule partition.
Pourquoi les opérations sur une seule partition sont importantes
L'utilisation des ressources est importante pour les performances et la rentabilité. Les opérations sur une seule partition utilisent nettement moins de ressources que les opérations entre partitions. Par exemple, lors de l'exécution d'une fonction pour insérer un million de lignes, l'exécution d'une partition unique en utilise environ 90,5 ACUs contre 126,5 ACUs pour l'exécution entre partitions, soit une amélioration de 35 % de l'efficacité des ressources.
L'exécution d'une partition unique fournit également :
-
Débit 35 % supérieur à celui des opérations cross-shard
-
Des temps de réponse plus prévisibles
-
Meilleure évolutivité à mesure que les données augmentent
Opérations et fonctions d'une seule partition
Les fonctions s'exécutent sur des partitions lorsque l'une de ces conditions préalables est remplie :
-
La fonction est créée de manière immuable et incluse dans une requête optimisée pour une seule partition
-
La fonction est distribuée par un utilisateur
Les fonctions qui s'exécutent sur des partitions sont plus performantes et évolutives car elles s'exécutent là où se trouvent les données.
Fonctions et volatilité
Pour vérifier la volatilité d'une fonction, utilisez cette requête sur les tables système 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');
Exemple de sortie :
nspname | proname | provolatile ------------+---------+------------- pg_catalog | md5 | i pg_catalog | random | v (2 rows)
Dans cet exemple, md5() est immuable et random() volatil. Cela signifie qu'une instruction optimisée pour une partition unique qui inclut md5() reste optimisée pour une seule partition, contrairement à une instruction qui inclut ne l'est pas. random()
Exemple avec fonction immuable :
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)
Exemple avec fonction volatile :
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 sortie indique que cela md5() est poussé vers le bas et exécuté en tant qu'optimisation pour une seule partition, alors que ce random() n'est pas le cas.
Fonctions de distribution
Une fonction qui accède aux données d'une seule partition doit s'exécuter sur cette partition pour améliorer les performances. La fonction doit être distribuée et la signature de la fonction doit inclure la clé de partition complète : toutes les colonnes de la clé de partition doivent être transmises en tant que paramètres à la fonction.
Exemple de fonction :
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;
Avant la distribution, la fonction n'est pas optimisée pour un seul fragment :
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)
Pour distribuer la fonction :
SELECT rds_aurora.limitless_distribute_function( 's1.func1(bigint,bigint,character)', ARRAY['param_a','param_b','param_c'], 's1.t1' );
Après distribution, la fonction est optimisée pour une partition unique :
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)
Vous pouvez confirmer l'optimisation d'une partition unique en cochant la sso_calls colonne dans : 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)
Fonctions et modèles d'efficacité
L'exécution de la logique à proximité des données est plus efficace, et les fonctions jouent un rôle clé à cet égard. Il existe deux principaux cas d'utilisation pour améliorer l'efficacité grâce aux fonctions :
-
Extraction d'une clé de partition à partir de données complexes pour invoquer une fonction distincte optimisée pour une partition unique
-
Transformation des charges de travail entre partitions en partitions optimisées pour une seule partition en séparant la logique entre partitions des instructions optimisées pour une seule partition
Extraction d'une clé de partition à partir de données complexes
Prenons l'exemple d'une fonction avec signature s3.func3(p_json_doc json) qui effectue plusieurs opérations de base de données. Ces opérations s'exécuteront sur toutes les partitions au sein d'une transaction qui couvre toutes les partitions. Si le document JSON contient la clé de partition, vous pouvez créer une fonction optimisée pour une partition unique pour effectuer les opérations de base de données.
Motif original :
s3.func3(p_json_doc json) database operation 1; database operation 2; database operation 3;
Motif optimisé :
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;
Où la fonction interne effectue :
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;
Dans ce modèle, la clé de partition est encapsulée dans un type de données complexe ou déductible à partir d'autres paramètres. La logique, l'accès aux données et les fonctions peuvent déterminer, extraire ou construire la clé de partition, puis invoquer une fonction optimisée pour une seule partition qui exécute des opérations concernant une seule partition. Comme l'interface de l'application ne change pas, l'optimisation est relativement facile à tester.
Interférer la clé de partition à d'autres fonctions ou données
Un autre modèle de conception s'applique lorsque la logique ou l'accès aux données calcule ou détermine la clé de partition. Cela est utile lorsqu'une fonction peut être exécutée sur une seule partition pour la plupart des invocations, mais qu'elle nécessite parfois une exécution entre partitions.
Motif 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;
Modèle optimisé avec fonctions distinctes :
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';
Demandez ensuite à la fonction principale d'appeler la version optimisée pour une partition unique ou la version pour une partition croisée :
IF all_whid_local THEN SELECT NEWORD_sso(…) INTO no_s_quantity; ELSE SELECT NEWORD_crosshard(…) INTO no_s_quantity; END IF;
Cette approche permet à la majorité des invocations de bénéficier de l'optimisation d'une partition unique tout en conservant un comportement correct dans les cas nécessitant une exécution entre partitions.
Vérification des opérations sur une seule partition
EXPLAINÀ utiliser pour vérifier si une instruction est optimisée pour une partition unique. La sortie indique explicitement « Single Shard Optimized » pour des opérations optimisées.
Invocation croisée avant distribution :
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
Invocation d'une partition unique après distribution :
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 différence entre les temps d'exécution démontre les avantages en termes de performances de l'optimisation d'une partition unique.