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

Can this particular INSERT be optimized?

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

因此,我会在导入之前使用外部工具清理 csv,或者让数据库本身完成过滤。

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

虽然在执行 csv 导入时通常无法处理变形的行,但我还是找到了解决此问题的方法:

  1. 将 csv 作为外部文件包含到数据库中table,但只有文本并且只有一个文本列 其中包含逗号的整行。

  2. 通过将单个文本列 拆分为各自的逗号,从那个外部 table 插入一个干净的目标 table

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

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

因此,为使其正常运行而发出的所有命令如下。

创建 fdw 服务器:

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

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

'1465721143588,-83,55,1361'

其中第一个值是 unix 时间戳,精度为毫秒, 然后是 rssi 信号强度值, 然后是接收信号的 站号 , 然后 传感器的 id

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

目标 table:

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 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 时间戳转换为双精度,然后进行除法,
  • 拆分字符串的正则表达式分析。
  • 外键查找检查

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

虽然导入的数据干净且一致,但对于这个维度,我对我的方法感到满意;唯一的缺点是性能不佳。所以如果有人能给我一些改进的建议,我将不胜感激。

干杯!

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

它的性能比 INSERT 好得多。

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

因此,我没有在 之前验证数据 INSERT(在我初始 post 的 WHERE 语句中),而是让 INSERT 操作 本身 验证数据(因为我已经在 CREATE TABLE 中定义了列的类型)。

虽然 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;