web-dev-qa-db-ja.com

大きなINSERTが遅くなり、ディスク使用量が爆発する原因は何ですか?

次の定義とインデックスを含む約310万行のテーブルがあります。

_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");
_

860万行の別のテーブルには、最初のテーブルの行の属性が含まれています。これらのテーブルは、_k_elem_id_ AND 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);
_

最初のテーブルの行を(いくつかの変更を加えて)新しいテーブルに挿入しようとしています。

_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)
);
_

単一の挿入ステートメントを実行すると時間がかかり、ステートメントがスタックしているかどうかを確認できないため、関数のループ内の小さなチャンクで実行することにしました。

関数は次のようになります。

_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;
_

問題は、ループを非常に速く通過することから始まりますが、ある時点で速度が低下してクロールになることです。速度が低下すると、同時にWindows 8タスクマネージャーのディスク使用率が最大99%に上昇するので、これが何らかの理由で問題に関係していると思います。

INSERTステートメントをiのランダムな値を指定して単独で実行すると、非常に高速に実行されるため、問題は関数内のループで実行した場合にのみ発生するようです。以下は、そのような単一の実行からのEXPLAIN (ANALYZE,BUFFERS)です。

_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
_

私のシステムでは、8GbのRAMを搭載したWindows 8でPostgreSQL 9.3.5を実行しています。

さまざまなバッチサイズを試し、さまざまな方法でクエリを実行し、Postgres構成でメモリ変数を増やしましたが、実際に問題を解決したようには見えません。

デフォルト値から変更された構成変数:

_shared_buffers = 2048MB
work_mem = 64MB
effective_cache_size = 6000MB
_

これを引き起こしている原因とそれに対して何ができるかを知りたいのですが。

6
jeran

新しいテーブルを作成するとき書き込みのコストを回避Write Ahead Log(WAL)完全に_CREATE TABLE AS_
WALがこれをどのように計算するかについては、 @ Kassandryの回答 を参照してください。

_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;
_

ドキュメント:

アーカイバーまたはWAL送信者がWALデータを処理する時間を回避することは別として、_wal_level_がminimal。 (WALを記述するよりも、最後にfsyncを実行する方が、より安全にクラッシュの安全性を保証できます。)これは、次のコマンドに適用されます。

  • _CREATE TABLE AS SELECT_

  • _CREATE INDEX_(および_ALTER TABLE ADD PRIMARY KEY_などのバリアント)

  • _ALTER TABLE SET TABLESPACE_

  • CLUSTER

  • _COPY FROM_、ターゲットテーブルが同じトランザクションで以前に作成または切り捨てられた場合

また重要

  • _CREATE TABLE AS_を使用すると、疑似型 serial を直接使用できなくなります。しかし、これは単なる「マクロ」なので、代わりにすべてを手動で行うことができます。シーケンスを作成し、それを使用してid値を生成します。最後に、列のデフォルトを設定し、列がシーケンスを所有するようにします。関連:

  • Plpgsql関数ラッパーはオプションで(繰り返し使用する場合に便利)、トランザクションでプレーンSQLを実行するだけです:_BEGIN; ... COMMIT;_

  • _PRIMARY KEY_を追加すると、データの挿入も速くなります。インデックスを1つにまとめると、値を段階的に追加するよりも高速です。

  • パーティショニングで論理エラーが発生しました:

    _WHERE DLE.ogc_fid >= i * batch_size
    AND   DLE.ogc_fid <= i * batch_size + batch_size
    _

    最後の行は次のパーティションと重複し、行は繰り返し挿入され、PKに一意の違反が発生します。 _<_の代わりに_<=_を使用すると修正できますが、パーティショニングを完全に削除しました。

  • これを繰り返し実行する場合、digiroad_segmentti (k_elem_id, tyyppi, dyn_tyyppi, region)マルチカラムインデックスは、データの分散に応じて支払う可能性があります。

ささいなこと

  • 言語のplpgsql名は引用しないでください。これは識別子です。
  • パラメータなしの関数をSTRICTとしてマークしても意味がありません。
  • VOLATILEはデフォルトであり、単なるノイズです。
  • NULL値のデフォルトを提供するには、COALESCEを使用します。

  • 古いテーブルにnumeric (9,0)がほとんどあったため、_double precision_(_float8_)列の一部はintegerとしてより適切に機能する可能性があります。プレーンinteger

  • region varchar(40)は正規化の候補のように見えます(領域がほとんど一意でない限り)。領域テーブルを作成し、メインテーブルのFK列として_region_id_を使用します。

7

shared_bufferswork_mem、およびeffective_cache_size構成変数のみを変更した場合は、おそらくcheckpoint_segments=3でまだ実行されています。

この場合、WALセグメントは3つしかないため、継続的にリサイクルする必要があり、毎回データファイルに書き込みを強制します。これにより、大量のI/Oアクティビティが発生し、マシンのクロールが遅くなる可能性があります。チェックポイントの動作を確認するには、ログを調べて、checkpoints are occurring too frequentlyというフレーズを検索します。 postgresql.confでlog_checkpoints=onを有効にすることで、彼らが何をしているかを確認することもできます。

checkpoint_segmentsを40などの大きな値に変更し、checkpoint_completion_targetを0.9に変更して、説明している動作をスムーズにすることをお勧めします。

設定については、9.3のPostgreSQLドキュメントの Write Ahead Log セクションで詳しく説明しています。 =)

5
Kassandry