使用 EAV 结构视图优化查询

Bru*_*uno 5 postgresql performance join view eav

应用程序正在写入遵循 EAV 结构的数据库,类似于:

CREATE TABLE item (
    id INTEGER PRIMARY KEY,
    description TEXT
);

CREATE TABLE item_attr (
    item INTEGER REFERENCES item(id),
    name TEXT,
    value INTEGER,
    PRIMARY KEY (item, name)
);

INSERT INTO item VALUES (1, 'Item 1');
INSERT INTO item_attr VALUES (1, 'height', 20);
INSERT INTO item_attr VALUES (1, 'width', 30);
INSERT INTO item_attr VALUES (1, 'weight', 40);
INSERT INTO item VALUES (2, 'Item 2');
INSERT INTO item_attr VALUES (2, 'height', 10);
INSERT INTO item_attr VALUES (2, 'weight', 35);
Run Code Online (Sandbox Code Playgroud)

(我认为 EAV 有点争议,但这个问题与 EAV 无关:无论如何都不能更改这个遗留应用程序。)

可以有多个属性,但通常每个项目最多 200 个属性(通常相似)。在这 200 个属性中,大约有 25 个属性比其他属性更常见,并且在查询中使用频率更高。

为了更容易地根据这 25 个属性中的一些属性编写新查询(需求往往会改变,我需要灵活),我编写了一个视图,将这 25 个属性的属性表连接起来。按照上面的例子,这看起来像这样:

CREATE VIEW exp_item AS SELECT
   i.id AS id,
   i.description AS description,
   ia_height.value AS height,
   ia_width.value AS width,
   ia_weight.value AS weight,
   ia_depth.value AS depth
FROM item i
  LEFT JOIN item_attr ia_height ON i.id=ia_height.item AND ia_height.name='height'
  LEFT JOIN item_attr ia_width ON i.id=ia_width.item AND ia_width.name='width'
  LEFT JOIN item_attr ia_weight ON i.id=ia_weight.item AND ia_weight.name='weight'
  LEFT JOIN item_attr ia_depth ON i.id=ia_depth.item AND ia_depth.name='depth';
Run Code Online (Sandbox Code Playgroud)

典型的报告只会使用这 25 个属性中的几个,例如:

SELECT id, description, height, width FROM exp_item;
Run Code Online (Sandbox Code Playgroud)

其中一些查询没有我希望的那么快。使用 时EXPLAIN,我注意到未使用的列上的连接仍然存在,当仅使用 3 或 4 个属性时,大约 25 个连接会导致性能不必要的下降。

当然,LEFT JOIN在视图中执行所有的s 是正常的,但我想知道是否有办法保持这个视图(或类似的东西:我主要对使用视图来简化我引用属性的方式感兴趣,或多或少好像它们是列)并避免(自动)对特定查询的未使用属性使用连接。

到目前为止,我发现的唯一解决方法是为每个查询定义一个特定的视图,该视图仅基于所使用的属性进行连接。(这确实提高了速度,正如预期的那样,但每次都需要更多的视图编程,因此灵活性稍差。)

有一个更好的方法吗?(从编写查询的角度来看,是否有更好的方法来“假装”EAV 结构是一个结构良好的表,而不必进行这些不必要的左连接?)

我正在使用 PostgreSQL 8.4。中大约有 10K 行,item.in 中大约有 500K 行item_attr。我不希望超过 80K 行item和 4M 行item_attr,我相信现代系统可以处理没有太多问题。(也欢迎对其他 RDBMS/版本发表评论。)

编辑:只是为了扩展本示例中索引的使用。

PRIMARY KEY (item, name)隐式创建一个索引(item, name),因为记录了在CREATE TABLE文档。考虑到itemname都与 中的等式约束一起使用JOIN,根据有关多列索引文档,此索引似乎合适。

以下示例显示该索引似乎按预期使用,没有任何明确的附加索引:

EXPLAIN SELECT id, description, height, width FROM exp_item WHERE width < 100;

                                                QUERY PLAN                                                 
-----------------------------------------------------------------------------------------------------------
 Nested Loop Left Join  (cost=28.50..203.28 rows=10 width=20)
   ->  Nested Loop Left Join  (cost=28.50..196.73 rows=10 width=16)
         ->  Nested Loop Left Join  (cost=28.50..190.18 rows=10 width=16)
               ->  Hash Join  (cost=28.50..183.64 rows=10 width=16)
                     Hash Cond: (ia_width.item = i.id)
                     ->  Seq Scan on item_attr ia_width  (cost=0.00..155.00 rows=10 width=8)
                           Filter: ((value < 100) AND (name = 'width'::text))
                     ->  Hash  (cost=16.00..16.00 rows=1000 width=12)
                           ->  Seq Scan on item i  (cost=0.00..16.00 rows=1000 width=12)
               ->  Index Scan using item_attr_pkey on item_attr ia_depth  (cost=0.00..0.64 rows=1 width=4)
                     Index Cond: ((i.id = ia_depth.item) AND (ia_depth.name = 'depth'::text))
         ->  Index Scan using item_attr_pkey on item_attr ia_weight  (cost=0.00..0.64 rows=1 width=4)
               Index Cond: ((i.id = ia_weight.item) AND (ia_weight.name = 'weight'::text))
   ->  Index Scan using item_attr_pkey on item_attr ia_height  (cost=0.00..0.64 rows=1 width=8)
         Index Cond: ((i.id = ia_height.item) AND (ia_height.name = 'height'::text))
Run Code Online (Sandbox Code Playgroud)

gbn*_*gbn 7

这是 EAV 设计的(众多)缺点之一。

您无法真正改进 JOIN:由于必要的复杂性,基于成本的优化器不会得到完美的计划。它发现“足够好”

建议:

  • 不要使用视图:使用聚合类型查询(例如 COUNT(*) = 2 如果我同时匹配身高和体重)
  • 使用触发器来维护真实(或稀疏)表并查询该表

第一个选项扩展性更好,因为主 EAV 事实表上的一些索引可以很好地覆盖所有查询。