如何在插入SQL表(Postgres)时高效地规范化数据

7 投票
1 回答
5377 浏览
提问于 2025-04-18 06:50

我想把一个很大的日志文件导入到(Postgres)SQL里。

有些字符串列的内容重复得很厉害,比如'event_type'这一列就只有10种不同的字符串值。

我对数据规范化有个大概念。

首先,我想确认一下:把'event_type'存到一个单独的表里(可能还要加个外键关系),这样对存储空间、索引和查询速度都有好处,对吗?

为了规范化,我需要先检查原始日志中'event_type'的不同值,然后把这些值插入到'event_types'表里。

像'event_types'这样的字段类型还有很多。

所以第二个问题是:有没有办法让数据库在插入数据时自动创建和维护这样的表?

还有其他的策略可以实现这个吗?我现在在用pandas。

1 个回答

7

当你开始建立一个数据库,而之前的数据是存储在其他地方,比如日志文件中,这种情况是很常见的。虽然有解决办法,但通常速度不快。你可以写一个日志消息处理程序,来处理接收到的消息;如果每秒钟的消息量不是特别大,你可能不会感觉到处理的延迟,尤其是当你不需要把消息写入一个普通文本文件时。

首先,关于数据规范化的问题。是的,你应该始终进行规范化,最好达到所谓的第三范式(3NF)。这基本上意味着任何现实世界的数据(比如你的事件类型)只需要存储一次。虽然在某些情况下,你可以稍微放宽要求,使用第二范式(2NF),通常只有在现实世界的数据占用空间非常小的情况下,比如一个国家的ISO代码,或者性别选择(男/女)等,但在大多数情况下,第三范式会更好。

以你的具体情况为例,假设你的事件类型是 char(20) 类型。十个这样的事件及其对应的 int 代码可以轻松放在一个数据库页面上,通常是4kB的磁盘空间。如果你有1000条日志消息,每条消息的事件类型是 char(20),那么你需要20kB来存储这些信息,也就是五个数据库页面。如果你的日志消息中还有其他项目,那么存储空间的节省会更大。像 datetimestamp 这样的其他项目可以以其原生格式(分别为4和8字节)存储,这样可以节省空间,提高性能,并增加功能(比如比较日期或查看范围)。

其次,你不能让数据库自动创建这些表,你需要自己来做。但一旦创建了表,就可以用存储过程来解析你的日志消息,并把数据放到正确的表中。

在处理日志消息时,你可以这样做(假设你想在数据库中进行解析,而不是在Python中):

CREATE FUNCTION ingest_log_message(mess text) RETURNS int AS $$
DECLARE
  parts  text[];
  et_id  int;
  log_id int;
BEGIN
  parts := regexp_split_to_array(mess, ','); -- Whatever your delimiter is

  -- Assuming:
  --   parts[1] is a timestamp
  --   parts[2] is your event_type
  --   parts[3] is the actual message

  -- Get the event_type identifier. If event_type is new, INSERT it, else just get the id.
  -- Do likewise with other log message parts whose unique text is located in a separate table.
  SELECT id INTO et_id
  FROM event_type
  WHERE type_text = quote_literal(parts[2]);
  IF NOT FOUND THEN
    INSERT INTO event_type (type_text)
    VALUES (quote_literal(parts[2]))
    RETURNING id INTO et_id;
  END IF;

  -- Now insert the log message
  INSERT INTO log_message (dt, et, msg)
  VALUES (parts[1]::timestamp, et_id, quote_literal(parts[3]))
  RETURNING id INTO log_id;

  RETURN log_id;
END; $$ LANGUAGE plpgsql STRICT;

你需要的表有:

CREATE TABLE event_type (
  id        serial PRIMARY KEY,
  type_text char(20)
);

CREATE TABLE log_message (
  id        serial PRIMARY KEY,
  dt        timestamp,
  et        integer REFERENCES event_type
  msg       text
);

然后你可以像简单的 SELECT 语句一样调用这个函数,它会返回新插入的日志消息的 id

SELECT * FROM ingest_log_message(the_message);

注意在函数体中使用了 quote_literal() 函数。这个函数有两个重要作用:(1)字符串中的引号会被正确转义(这样像 "isn't" 这样的词不会搞乱命令);(2)它可以防止恶意生成日志消息的SQL注入攻击。

以上所有内容显然需要根据你的具体情况进行调整。

撰写回答