这个特定的INSERT可以进行优化吗?

时间:2017-08-04 09:51:10

标签: postgresql csv foreign-data-wrapper

我正在将csv导入postgresql 9.5.7数据库。但问题是,csv部分格式错误(某些行缺少逗号,因此整个列,或者某些可能包含太多,或某些值无效)。

所以要么,我会在导入之前使用外部工具清理csv,或者让数据库本身进行过滤。

我更喜欢第二种方法,因为在我看来,它更少依赖于外部csv清理脚本,因为所有数据验证都是直接在持久性级别进行的。

虽然在进行csv-import时通常不可能处理错误的行,但我仍然找到了解决这个问题的方法:

  1. 将csv作为外表包含在数据库中,但仅限文本仅包含一个文本列,其中包含逗号的整行(包括逗号)

  2. 通过将单个文本列分别用逗号分隔,从该外表中插入一个干净的目标表

  3. 但是在我的测试机器上导入一个包含3300万行的200 MB csv文件需要大约6个小时。那么Insert语句肯定可以进一步优化吗?我对postgres很新,所以这很可能。请纠正我在哪里做出可以做得更好的决定。

    现在,简要解释要建模的域:它是关于处理传感器的位置是通过它们的信号强度以特定的时间间隔记录到各个站点。通过以毫秒精度记录,这些间隔非常精确。

    因此,为实现这一目标而发布的所有命令如下:

    创建fdw服务器:

    CREATE EXTENSION file_fdw;
    CREATE SERVER csv_import_server FOREIGN DATA WRAPPER file_fdw;
    

    接下来,创建外部csv表,只有一个文本列包含所有数据。 干净的行看起来像这样:

    '1465721143588,-83,55,1361'

    其中第一个值是 unix时间戳,精度为毫秒, 那么 rssi 信号强度值, 然后是电台的ID ,信号被拾取, 那么传感器的id

    CREATE FOREIGN TABLE signals_csv ( 
        value TEXT )
    SERVER csv_import_server OPTIONS( 
    filename '<path_to_file>/signals.csv', format 'text');
    

    目标表:

    CREATE TABLE signals (
        timestamp TIMESTAMP NOT NULL,
        rssi INTEGER NOT NULL,
        stations_id INTEGER NOT NULL,
        distributed_tags_id INTEGER NOT NULL,
        PRIMARY KEY(timestamp, stations_id, distributed_tags_id),
        FOREIGN KEY(stations_id) REFERENCES stations(stations_id),
        FOREIGN KEY(distributed_tags_id) REFERENCES tags(id) );
    

    现在INSERT:

    INSERT INTO signals (timestamp, rssi, stations_id, distributed_tags_id) SELECT
    TO_TIMESTAMP( tmp.timestamp::double precision / 1000),
    tmp.rssi::INTEGER,
    tmp.stations_id::INTEGER,
    tmp.distributed_tags_id::INTEGER
    FROM ( SELECT 
        SPLIT_PART ( value, ',', 1) AS timestamp, 
        SPLIT_PART ( value, ',', 2) AS rssi, 
        SPLIT_PART ( value, ',', 3) AS stations_id,
        SPLIT_PART ( value, ',', 4) AS distributed_tags_id
            FROM signals_csv ) AS tmp WHERE (
            tmp.timestamp ~ '^[0-9]+$' AND
            tmp.rssi ~ '^-[0-9]+$' AND
            tmp.stations_id ~ '^[0-9]+$' AND
            tmp.distributed_tags_id ~ '^[0-9]+$' AND
            EXISTS ( SELECT 1 FROM tags t WHERE t.id::TEXT = tmp.distributed_tags_id ) AND
            EXISTS ( SELECT 1 FROM stations s WHERE s.stations_id::TEXT = tmp.stations_id )
            )
    ON CONFLICT (timestamp, stations_id, distributed_tags_id ) DO NOTHING;
    

    我猜大量的性能点击次数是:

    • 将unix时间戳的转换为双精度,然后是它们的划分,
    • 分裂字符串的正则表达式分析。
    • 外键查找

    正如我所看到的那样,如果我想保持数据以一致的方式建模,同时保持以人类可读的方式存储毫秒精度,则无法绕过这些限制。

    导入的数据虽然干净且一致,但对于这个维度,我对我的方法感到满意;唯一的缺点是它的糟糕表现。所以,如果有人能指出如何改进这一点,我会非常感激。

    干杯!

2 个答案:

答案 0 :(得分:0)

如果要插入大量行,可以使用COPY而不是INSERT。

性能优于INSERT。

答案 1 :(得分:0)

我以不同的方式解决了这个问题,可以将导入时间从7小时减少到只有1小时。

所以,不是在 INSERT之前验证数据(在我的初始帖子的WHERE clase中),而是让INSERT操作本身验证数据(因为我已经在CREATE TABLE中定义了列&#39;类型。

虽然INSERT在遇到意外数据类型时抛出异常,但我在循环中每行执行INSERT,以便异常只中止当前迭代而不是整个事务。

工作代码如下所示:

CREATE OR REPLACE FUNCTION import_tags_csv( path_to_csv TEXT ) RETURNS VOID AS $$

DECLARE

    cursor SCROLL CURSOR FOR SELECT 
        SPLIT_PART ( value, ',', 1) AS id, 
        SPLIT_PART ( value, ',', 2) AS active_from,
        SPLIT_PART ( value, ',', 3) AS active_to
        FROM csv_table;
    i BIGINT := 0;

BEGIN 

    -- create the whole foreign data wrapper for integrating the csv:
    CREATE EXTENSION file_fdw;
    CREATE SERVER csv_import_server FOREIGN DATA WRAPPER file_fdw;
    EXECUTE '
        CREATE FOREIGN TABLE csv_table ( value TEXT )
        SERVER csv_import_server OPTIONS( filename ''' || path_to_csv || ''', format ''text'')';

    -- Iterating through the rows, converting the text data and inserting it into table tags
    FOR csv_row IN cursor LOOP
    BEGIN

        i := i +1;
        INSERT INTO tags ( 
            id, 
            active_from, 
            active_to) 
            VALUES (
            csv_row.id::INTEGER,
            TO_TIMESTAMP( csv_row.active_from::double precision / 1000),
            TO_TIMESTAMP( csv_row.active_to::double precision / 1000) );

        --If invalid data is read, the table constraints throw an exception. The faulty line is dismissed
        EXCEPTION WHEN OTHERS THEN 
            RAISE NOTICE E'% \n\t line %: %\n', SQLERRM, i, csv_row;

    END;
    END LOOP;

    -- Dropping the foreign table which had the csv integrated
    DROP FOREIGN TABLE csv_table;
    DROP SERVER csv_import_server;
    DROP EXTENSION file_fdw;

END;
$$ LANGUAGE plpgsql;