Qu'est-ce qui provoque le ralentissement d'un grand INSERT et l'explosion de l'utilisation du disque ?
Question
J'ai un tableau d'environ 3,1 millions de lignes avec la définition et les index suivants :
CREATE TABLE digiroad_liikenne_elementti (
ogc_fid serial NOT NULL,
wkb_geometry geometry(Geometry,4258),
tiee_tila numeric(9,0),
vaylatyypp numeric(9,0),
toiminnall numeric(9,0),
eurooppati character varying(254),
kansalline numeric(9,0),
tyyppi numeric(9,0),
liikennevi numeric(9,0),
ens_talo_o numeric(9,0),
talonumero numeric(9,0),
ens_talo_v numeric(9,0),
oik_puol_t character varying(254),
tieosan_ta numeric(9,0),
viim_talo_ numeric(9,0),
viim_tal_1 numeric(9,0),
vas_puol_t character varying(254),
laut_tyypp numeric(9,0),
lautta_lii numeric(9,0),
inv_paalu_ numeric(19,11),
inv_paal_1 numeric(19,11),
liitalue_o numeric(9,0),
ketju_oid numeric(9,0),
tietojoukk numeric(9,0),
ajoratanum numeric(4,0),
viite_guid character varying(254),
"timestamp" date,
tiee_kunta numeric(9,0),
toissij_ti character varying(254),
viite_oid numeric(9,0),
k_elem_id numeric(9,0),
region character varying(40) DEFAULT 'REGION'::character varying,
CONSTRAINT digiroad_liikenne_elementti_pkey PRIMARY KEY (ogc_fid)
);
CREATE INDEX digiroad_liikenne_elementti_wkb_geometry_geom_idx
ON digiroad_liikenne_elementti USING gist (wkb_geometry);
CREATE INDEX dle_k_elem_id_idx
ON digiroad_liikenne_elementti USING btree (k_elem_id);
CREATE INDEX dle_ogc_fid_idx
ON digiroad_liikenne_elementti USING btree (ogc_fid);
CREATE INDEX dle_region_idx
ON digiroad_liikenne_elementti USING btree (region COLLATE pg_catalog."default");
Une autre table de 8,6 millions de lignes contient les attributs des lignes de la première table, les tables peuvent être jointes avec k_elem_id
ET region
.
CREATE TABLE digiroad_segmentti (
ogc_fid serial NOT NULL,
wkb_geometry geometry(Geometry,4258),
segm_tila numeric(9,0),
tyyppi numeric(9,0),
loppupiste numeric(19,11),
alkupiste numeric(19,11),
vaikutuska numeric(9,0),
vaikutussu numeric(9,0),
vaikutusai character varying(254),
tieosanume numeric(19,11),
tienumero numeric(9,0),
dyn_arvo numeric(9,0),
dyn_tyyppi numeric(9,0),
omistaja_t numeric(9,0),
pysakki_va numeric(9,0),
pysakki_ty numeric(9,0),
pysakki_su numeric(9,0),
pysakki_ka numeric(9,0),
pysakki_yl character varying(254),
palvelu_pa numeric(9,0),
toissijain numeric(9,0),
siltataitu numeric(9,0),
rdtc_tyypp numeric(9,0),
rdtc_alaty numeric(9,0),
rdtc_paikk numeric(19,11),
rdtc_luokk numeric(9,0),
rdtc_liitt character varying(254),
palvelu_ob numeric(9,0),
ketju_oid numeric(9,0),
tietojoukk numeric(9,0),
ajoratanum numeric(4,0),
viite_guid character varying(254),
"timestamp" date,
sivusiirty numeric(19,11),
toissij_ti character varying(254),
viite_oid numeric(9,0),
k_elem_id numeric(9,0),
region character varying(40) DEFAULT 'REGION'::character varying,
CONSTRAINT digiroad_segmentti_pkey PRIMARY KEY (ogc_fid)
);
CREATE INDEX digiroad_segmentti_wkb_geometry_geom_idx
ON digiroad_segmentti USING gist (wkb_geometry);
CREATE INDEX ds_dyn_arvo_idx
ON digiroad_segmentti USING btree (dyn_arvo);
CREATE INDEX ds_dyn_tyyppi_idx
ON digiroad_segmentti USING btree (dyn_tyyppi);
CREATE INDEX ds_k_elem_id_idx
ON digiroad_segmentti USING btree (k_elem_id);
CREATE INDEX ds_ogc_fid_idx
ON digiroad_segmentti USING btree (ogc_fid);
CREATE INDEX ds_region_idx
ON digiroad_segmentti USING btree (region COLLATE pg_catalog."default");
CREATE INDEX ds_tyyppi_idx
ON digiroad_segmentti USING btree (tyyppi);
J'essaie d'insérer les lignes du premier tableau (avec quelques modifications) dans un nouveau tableau :
CREATE TABLE edge_table (
id serial NOT NULL,
geom geometry,
source integer,
target integer,
km double precision,
kmh double precision DEFAULT 60,
kmh_winter double precision DEFAULT 50,
cost double precision,
cost_winter double precision,
reverse_cost double precision,
reverse_cost_winter double precision,
x1 double precision,
y1 double precision,
x2 double precision,
y2 double precision,
k_elem_id integer,
region character varying(40),
CONSTRAINT edge_table_pkey PRIMARY KEY (id)
);
Étant donné que l'exécution d'une seule instruction d'insertion prendrait beaucoup de temps et que je ne serais pas en mesure de voir si l'instruction est bloquée ou quelque chose du genre, j'ai décidé de le faire en morceaux plus petits à l'intérieur d'une boucle dans une fonction.
La fonction ressemble à ceci :
DROP FUNCTION IF EXISTS insert_function();
CREATE OR REPLACE FUNCTION insert_function()
RETURNS VOID AS
$$
DECLARE
const_type_1 CONSTANT int := 5;
const_type_2 CONSTANT int := 11;
i int := 0;
row_count int;
BEGIN
CREATE TABLE IF NOT EXISTS edge_table (
id serial PRIMARY KEY,
geom geometry,
source integer,
target integer,
km double precision,
kmh double precision DEFAULT 60,
kmh_winter double precision DEFAULT 50,
cost double precision,
cost_winter double precision,
reverse_cost double precision,
reverse_cost_winter double precision,
x1 double precision,
y1 double precision,
x2 double precision,
y2 double precision,
k_elem_id integer,
region varchar(40)
);
batch_size := 1000;
SELECT COUNT(*) FROM digiroad_liikenne_elementti INTO row_count;
WHILE i*batch_size < row_count LOOP
RAISE NOTICE 'insert: % / %', i * batch_size, row_count;
INSERT INTO edge_table (kmh, kmh_winter, k_elem_id, region)
SELECT CASE WHEN DS.dyn_arvo IS NULL THEN 60 ELSE DS.dyn_arvo END,
CASE WHEN DS.dyn_Arvo IS NULL THEN 50 ELSE DS.dyn_arvo END,
DR.k_elem_id,
DR.region
FROM (
SELECT DLE.k_elem_id,
DLE.region,
FROM digiroad_liikenne_elementti DLE
WHERE DLE.ogc_fid >= i * batch_size
AND
DLE.ogc_fid <= i * batch_size + batch_size
) AS DR
LEFT JOIN
digiroad_segmentti DS ON
DS.k_elem_id = DR.k_elem_id
AND
DS.region = DR.region
AND
DS.tyyppi = const_type_1
AND
DS.dyn_tyyppi = const_type_2;
i := i + 1;
END LOOP;
END;
$$
LANGUAGE 'plpgsql' VOLATILE STRICT;
Le problème est qu'il commence par parcourir les boucles assez rapidement, mais ralentit ensuite à un moment donné.Lorsqu'il ralentit, l'utilisation du disque dans mon gestionnaire de tâches de Windows 8 augmente en même temps jusqu'à 99 %, je soupçonne donc que cela est lié au problème d'une manière ou d'une autre.
Exécuter le INSERT
instruction seule avec une valeur aléatoire de i
s'exécute très rapidement, le problème semble donc se poser uniquement lors de son exécution dans la boucle à l'intérieur d'une fonction.Voici la EXPLAIN (ANALYZE,BUFFERS)
à partir d'une seule de ces exécutions :
Insert on edge_table (cost=0.86..361121.68 rows=1031 width=23) (actual time=3405.101..3405.101 rows=0 loops=1)
Buffers: shared hit=36251 read=3660 dirtied=14
-> Nested Loop Left Join (cost=0.86..361121.68 rows=1031 width=23) (actual time=61.901..3377.609 rows=986 loops=1)
Buffers: shared hit=32279 read=3646
-> Index Scan using dle_ogc_fid_idx on digiroad_liikenne_elementti dle (cost=0.43..85.12 rows=1031 width=19) (actual time=31.918..57.309 rows=986 loops=1)
Index Cond: ((ogc_fid >= 200000) AND (ogc_fid < 201000))
Buffers: shared hit=27 read=58
-> Index Scan using ds_k_elem_id_idx on digiroad_segmentti ds (cost=0.44..350.16 rows=1 width=23) (actual time=2.861..3.337 rows=0 loops=986)
Index Cond: (k_elem_id = dle.k_elem_id)
Filter: ((tyyppi = 5::numeric) AND (dyn_tyyppi = 11::numeric) AND (vaikutussu = 3::numeric) AND ((region)::text = (dle.region)::text))
Rows Removed by Filter: 73
Buffers: shared hit=31266 read=3588
Total runtime: 3405.270 ms
Mon système exécute PostgreSQL 9.3.5 sur Windows 8 avec 8 Go de RAM.
J'ai expérimenté différentes tailles de lots, en effectuant la requête de différentes manières et en augmentant les variables de mémoire dans la configuration de Postgres, mais rien ne semble avoir vraiment résolu le problème.
Variables de configuration qui ont été modifiées par rapport à leurs valeurs par défaut :
shared_buffers = 2048MB
work_mem = 64MB
effective_cache_size = 6000MB
J'aimerais savoir ce qui cause cela et ce qui pourrait être fait pour y remédier.
La solution
Lors de la création d'un nouveau tableau éviter le coût de l'écriture Journal d'écriture anticipée (WAL) complètement avec CREATE TABLE AS
.
Voir @ Réponse de Kassandry pour une explication de la manière dont WAL entre en ligne de compte.
CREATE OR REPLACE FUNCTION insert_function()
RETURNS void AS
$func$
DECLARE
const_type_1 CONSTANT int := 5;
const_type_2 CONSTANT int := 11;
BEGIN
CREATE SEQUENCE edge_table_id_seq;
CREATE TABLE edge_table AS
SELECT nextval('edge_table_id_seq'::regclass)::int AS id
, NULL::geometry AS geom
, NULL::integer AS source
, target::integer AS target
, NULL::float8 AS km
, COALESCE(DS.dyn_arvo::float8, float8 '60') AS kmh
, COALESCE(DS.dyn_Arvo::float8, float8 '50') AS kmh_winter
, NULL::float8 AS cost
, NULL::float8 AS cost_winter
, NULL::float8 AS reverse_cost
, NULL::float8 AS reverse_cost_winter
, NULL::float8 AS x1
, NULL::float8 AS y1
, NULL::float8 AS x2
, NULL::float8 AS y2
, D.k_elem_id::integer AS k_elem_id
, D.region::varchar(40) AS region
FROM digiroad_liikenne_elementti D
LEFT JOIN digiroad_segmentti DS
ON DS.k_elem_id = D.k_elem_id
AND DS.region = D.region
AND DS.tyyppi = const_type_1
AND DS.dyn_tyyppi = const_type_2;
ALTER TABLE edge_table
ADD CONSTRAINT edge_table_pkey PRIMARY KEY(id)
, ALTER COLUMN id SET NOT NULL
, ALTER COLUMN id SET DEFAULT nextval('edge_table_id_seq'::regclass)
, ALTER COLUMN kmh SET DEFAULT 60
, ALTER COLUMN kmh_winter SET DEFAULT 50;
ALTER SEQUENCE edge_table_id_seq OWNED BY edge_table.id;
END
$func$ LANGUAGE plpgsql;
En plus d'éviter le temps pour que le Archiver ou le WAL expédateur traitent les données WAL, ce qui fera en fait certaines commandes plus rapidement, car ils sont conçus pour ne pas écrire du WAL du tout si
wal_level
estminimal
.(Ils peuvent garantir la sécurité en cas d'accident à moindre coût en effectuant unefsync
À la fin qu'en écrivant le Wal.) Cela s'applique aux commandes suivantes:
CREATE TABLE AS SELECT
CREATE INDEX
(et des variantes telles queALTER TABLE ADD PRIMARY KEY
)
ALTER TABLE SET TABLESPACE
CLUSTER
COPY FROM
, lorsque la table cible a été créée ou tronquée plus tôt dans la même transaction
Il est également important
CREATE TABLE AS
rend impossible l'utilisation du pseudo-typeserial
directement.Mais comme il ne s'agit que d'un "makro", vous pouvez tout faire à la main :Créez la séquence, utilisez-la pour générerid
valeurs.Enfin, définissez la colonne par défaut et faites en sorte que la colonne soit propriétaire de la séquence.En rapport:Le wrapper de fonction plpgsql est facultatif (pratique pour une utilisation répétée), vous pouvez simplement exécuter SQL simple dans une transaction:
BEGIN; ... COMMIT;
Ajout du
PRIMARY KEY
après l'insertion des données est également plus rapide car la création de l'index (sous-jacent) en un seul morceau est plus rapide que l'ajout de valeurs de manière incrémentielle.Tu avais un erreur logique dans votre partitionnement :
WHERE DLE.ogc_fid >= i * batch_size AND DLE.ogc_fid <= i * batch_size + batch_size
La dernière ligne chevaucherait la partition suivante, la ligne serait insérée à plusieurs reprises, conduisant à une violation unique du PK.L'utilisation de
<
au lieu de<=
résoudrait ce problème - mais j'ai complètement supprimé le partitionnement.Si vous exécutez ceci à plusieurs reprises, un index multicolonne sur
digiroad_segmentti (k_elem_id, tyyppi, dyn_tyyppi, region)
pourrait payer, en fonction de la distribution des données.
Des choses mineures
- Ne citez pas la langue
plpgsql
le nom, c'est un identifiant. - Il serait inutile de marquer une fonction sans paramètres comme
STRICT
. VOLATILE
est la valeur par défaut et juste du bruit.Utiliser
COALESCE
pour fournir une valeur par défaut pour les valeurs NULL.Certains de vos
double precision
(float8
) les colonnes pourraient mieux fonctionner carinteger
puisque tu avais surtoutnumeric (9,0)
dans vos anciennes tables, qui peuvent probablement être remplacées par des tables simples moins chèresinteger
.La colonne
region varchar(40)
ressemble à un candidat à la normalisation (à moins que les régions ne soient pour la plupart uniques ?) Créez une table de régions et utilisez simplementregion_id
comme colonne FK dans la table principale.
Autres conseils
Si vous aviez seulement changé le shared_buffers
,work_mem
, et effective_cache_size
variables de configuration, alors vous utilisez probablement toujours checkpoint_segments=3
.
Dans ce cas, vous ne disposez que de trois segments WAL et, en tant que tels, vous devez les recycler en permanence, forçant à chaque fois les écritures dans les fichiers de données, ce qui entraîne une énorme quantité d'activité d'E/S et peut certainement ralentir considérablement votre machine.Vous pouvez vérifier le comportement des points de contrôle en consultant le journal et en recherchant la phrase checkpoints are occurring too frequently
.Vous pouvez également voir ce qu'ils font en activant log_checkpoints=on
dans votre postgresql.conf
Je recommanderais de changer votre checkpoint_segments
à quelque chose de plus grand, comme 40, et le checkpoint_completion_target
à 0,9 pour essayer d'atténuer le comportement que vous décrivez.
Les paramètres sont décrits plus en détail ici dans la documentation PostgreSQL pour 9.3 dans le Journal d'écriture anticipée section.=)