我应该存储空的 tsvector 值还是 NULL 值?

Should I store empty tsvector values or NULL values?

在列中存储 tsvector 值时,对于没有搜索词的记录,我应该存储空 tsvector 还是 NULL 值?

重要吗?

在性能或存储空向量的存储开销方面是否存在差异?

换句话说,当基于可为 null 的 title 列的值更新向量时,我是否需要始终将其计算为 to_tsvector(coalesce(title,''))(因为 to_tsvector returns NULL 当给定一个 NULL 参数时)还是足以做 to_tsvector(title)?

你问题的逻辑方面

首先,SQL NULL 的语义是 UNKNOWN 的语义,而某些数据类型也有一个“空”值。这些数据类型包括:

  • TEXT''NULL::TEXT 不同)
  • JSONJSONB[]{}NULL::JSONNULL:JSONB 不同)
  • X[]ARRAY[]::X[]NULL::X[] 不同)

还有很多,包括 TSVECTOR。空集合的语义总是与 NULL 值的语义略有不同,NULL 值是 UNKNOWN 集合(尽管通常只是用作缺席集合)。当涉及到使用运算符时,这种区别特别明显,例如

  • '' || 'abc' = 'abc'NULL || 'abc' IS NULL
  • to_tsvector('cats ate rats') @@ to_tsquery('cat & rat') = trueNULL @@ to_tsquery('cat & rat') IS NULL

从这个意义上说,决定首先应该是逻辑决定,而不是存储决定,基于这个问题:即使记录不存在,您仍会使用记录的 TSVECTOR 值吗?没有任何搜索词(临空 TSVECTOR)?还是该功能根本不适用于该特定记录(pro NULL 值)?对于 @@ 运算符,它可能不是那么相关,但它肯定与 || 运算符和其他运算符相关。

答案并不明显,一般也没有明确的正确/错误方法。

你的问题的性能方面

如果这是您应用程序中对性能高度敏感的情况(例如,您有很多空 TSVECTOR 值),那么也许这个基准可以帮助您做出决定?

我 运行 在 Docker 中对 PostgreSQL 14.1 进行以下基准测试以获得此结果:

RUN 1, Statement 1: 2.91145
RUN 1, Statement 2: 1.00000 -- The fastest run is 1. The others are multiples of 1
RUN 2, Statement 1: 2.80509
RUN 2, Statement 2: 1.05232
RUN 3, Statement 1: 2.78001
RUN 3, Statement 2: 1.00202
RUN 4, Statement 1: 2.74319
RUN 4, Statement 2: 1.00524
RUN 5, Statement 1: 2.75808
RUN 5, Statement 2: 1.00045
  • 语句 1 是 SELECT v @@ to_tsquery('cat & rat')v tsvector = to_tsvector('');
  • 语句 2 是 SELECT NULL @@ to_tsquery('cat & rat')

涉及 NULL 的事实可能导致 @@ 运算符算法的捷径,与在基准测试中查询空 TSVECTOR 相比,该算法的性能提高了 2.7 倍。因此,就性能而言,使用 NULL 似乎确实有好处。

显然,这只是一个基准,不一定反映 real-world use-cases,但它应该会给您一些潜在差异的提示。

基准代码

对于复制或改编,here's a benchmark, based on this technique

DO $$
DECLARE
  v_ts TIMESTAMP;
  v_repeat CONSTANT INT := 10000;
  rec RECORD;
  run INT[];
  stmt INT[];
  elapsed DECIMAL[];
  min_elapsed DECIMAL;
  i INT := 1;

  -- Store the vector in a local variable to avoid re-computing it in the benchmark
  v tsvector = to_tsvector('');
BEGIN

  -- Repeat the whole benchmark several times to avoid warmup penalty
  FOR r IN 1..5 LOOP
    v_ts := clock_timestamp();

    FOR i IN 1..v_repeat LOOP
      FOR rec IN (
        -- Statement 1
        SELECT v @@ to_tsquery('cat & rat')
      ) LOOP
        NULL;
      END LOOP;
    END LOOP;

    run[i] := r;
    stmt[i] := 1;
    elapsed[i] := (EXTRACT(EPOCH FROM CAST(clock_timestamp() AS TIMESTAMP)) 
                 - EXTRACT(EPOCH FROM v_ts));
    i := i + 1;
    v_ts := clock_timestamp();

    FOR i IN 1..v_repeat LOOP
      FOR rec IN (
        -- Statement 2
        SELECT NULL @@ to_tsquery('cat & rat') 
      ) LOOP
        NULL;
      END LOOP;
    END LOOP;

    run[i] := r;
    stmt[i] := 2;
    elapsed[i] := (EXTRACT(EPOCH FROM CAST(clock_timestamp() AS TIMESTAMP))
                 - EXTRACT(EPOCH FROM v_ts));
    i := i + 1;
  END LOOP;

  SELECT min(t.elapsed)
  INTO min_elapsed
  FROM unnest(elapsed) AS t(elapsed);

  FOR i IN 1..array_length(run, 1) LOOP
    RAISE INFO 'RUN %, Statement %: %', run[i], stmt[i], 
      CAST(elapsed[i] / min_elapsed AS DECIMAL(10, 5));
  END LOOP;
END$$;