有时得到不正确的喜欢和不喜欢的总数

Getting incorrect total number of likes and dislikes sometimes

我在 PostgresQL 中有一个 table feed_item_likes_dislikes(feed_item_id、user_id、投票),其中

我有另一个 table feed_item_likes_dislikes_aggregate(feed_item_id, likes, dislikes) 我想保持每个 post[=15= 喜欢不喜欢的总数]

当用户在 feed_item_likes_dislikes table 和

添加了一个新的赞
INSERT INTO feed_item_likes_dislikes VALUES('54d67b62-9b71-a6bc-d934-451c1eaae3bc', 1, TRUE);

我想更新合计中的点赞总数 table。类似的情况需要处理不喜欢以及当用户通过将投票设置为 null

来取消投票时

用户也可以将他们的喜欢更新为不喜欢,反之亦然,在每种情况下,需要维护 post 的喜欢和不喜欢的总数

我写了下面的触发函数来完成这个

CREATE OR REPLACE FUNCTION update_votes() RETURNS trigger AS $$
    DECLARE
        feed_item_id_val uuid;
        likes_val integer;
        dislikes_val integer;
    BEGIN
        IF (TG_OP = 'DELETE') THEN
            -- when a row is deleted, store feed_item_id of the deleted row so that we can update its likes and dislikes count
            feed_item_id_val:=OLD.feed_item_id;
        ELSIF (TG_OP = 'UPDATE') OR (TG_OP='INSERT') THEN
            feed_item_id_val:=NEW.feed_item_id;
        END IF;
        -- get total number of likes and dislikes for the given feed_item_id
        SELECT COUNT(*) FILTER(WHERE vote=TRUE) AS likes, COUNT(*) FILTER(WHERE vote=FALSE) AS dislikes INTO likes_val, dislikes_val FROM feed_item_likes_dislikes WHERE feed_item_id=feed_item_id_val;
        -- update the aggregate count for only this feed_item_id
        INSERT INTO feed_item_likes_dislikes_aggregate (feed_item_id, likes, dislikes) VALUES (feed_item_id_val, likes_val, dislikes_val) ON CONFLICT(feed_item_id) DO UPDATE SET likes=likes_val, dislikes=dislikes_val;
        RETURN NULL; -- result is ignored since this is an AFTER trigger
    END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_votes_trigger AFTER INSERT OR UPDATE OR DELETE ON feed_item_likes_dislikes FOR EACH ROW EXECUTE PROCEDURE update_votes();

但是当我批量插入 feed_item_likes_dislikes 时,有时喜欢和不喜欢的总数不正确。

有人可以告诉我如何解决这个问题吗?

更新 1

我尝试创建一个视图,但它在我的生产数据集上花费了很多时间,这里是数据库 fiddle https://www.db-fiddle.com/f/2ZAkjQhUydMaV9o5xvLgMT/17

查询#1

EXPLAIN ANALYZE SELECT f.feed_item_id,pubdate,link,guid,title,summary,author,feed_id,COALESCE(likes, 0) AS likes,COALESCE(dislikes, 0) AS dislikes,COALESCE(bullish, 0) AS bullish,COALESCE(bearish, 0) AS bearish FROM feed_items f LEFT JOIN likes_dislikes_aggregate l ON f.feed_item_id = l.feed_item_id LEFT JOIN bullish_bearish_aggregate b ON f.feed_item_id = b.feed_item_id  ORDER BY pubdate DESC, f.feed_item_id DESC LIMIT 10;
查询计划
限制(cost=112.18..112.21 rows=10 width=238) (实际时间=0.257..0.260 rows=10 loops=1)
->排序(cost=112.18..112.93 rows=300 width=238) (actual time=0.257..0.257 rows=10 loops=1)
排序键:f.pubdate DESC,f.feed_item_id DESC
排序方式:top-N堆排序内存:27kB
-> Hash Left Join (cost=91.10..105.70 rows=300 width=238) (actual time=0.162..0.222 rows=100 loops=1)
哈希条件:(f.feed_item_id = b.feed_item_id)
-> Hash Left Join (cost=45.55..59.35 rows=300 width=222) (actual time=0.080..0.114 rows=100 loops=1)
哈希条件:(f.feed_item_id = l.feed_item_id)
-> feed_items f 上的序列扫描(成本=0.00..13.00 行=300 宽度=206)(实际时间=0.004..0.011 行=100 循环=1)
-> Hash (cost=43.05..43.05 rows=200 width=32) (actual time=0.069..0.069 rows=59 loops=1)
桶数:1024 批次:1 内存使用量:12kB
-> l 上的子查询扫描(成本=39.05..43.05 行=200 宽度=32)(实际时间=0.037..0.052 行=59 循环=1)
-> HashAggregate (cost=39.05..41.05 rows=200 width=32) (actual time=0.036..0.046 rows=59 loops=1)
组键:feed_item_likes_dislikes.feed_item_id
-> feed_item_likes_dislikes 上的序列扫描(成本=0.00..26.60 行=1660 宽度=17)(实际时间=0.003..0.008 行=95 循环=1)
-> Hash (cost=43.05..43.05 rows=200 width=32) (actual time=0.064..0.064 rows=63 loops=1)
桶数:1024 批次:1 内存使用量:12kB
-> b 上的子查询扫描(成本=39.05..43.05 行=200 宽度=32)(实际时间=0.029..0.044 行=63 循环=1)
-> HashAggregate (cost=39.05..41.05 rows=200 width=32) (actual time=0.028..0.038 rows=63 loops=1)
组键:feed_item_bullish_bearish.feed_item_id
-> feed_item_bullish_bearish 上的序列扫描(成本=0.00..26.60 行=1660 宽度=17)(实际时间=0.002..0.007 行=93 循环=1)
计划时间:0.140 毫秒
执行时间:0.328 毫秒

View on DB Fiddle

保持 运行 聚合的尝试总是充满陷阱,几乎总是 不值得付出努力。解决方案是 而不是 尝试存储聚合,而是根据需要导出它们。为此,您可以创建一个 VIEW 而不是 table。然后这会删除所有额外的处理,尤其是在这种情况下,因为您的触发器基本上包含生成视图所需的查询。 (参见 Demo here

create or replace VIEW likes_dislikes_aggregate as 
   select id
        , count(*) filter(where vote) as likes
        , count(*) filter(where not vote) as dislikes   
        , count(*) filter(where vote is null) as no_vote
     from likes_dislikes
    group by id;

没有触发器,没有额外的代码,一切都通过标准 DML 进行查看。请注意,实体视图基本上只是您的计数查询,没有触发器开销和维护。

SELECT COUNT(*) FILTER(WHERE vote=TRUE) AS likes, COUNT(*) FILTER(WHERE vote=FALSE) AS dislikes INTO likes_val, dislikes_val FROM feed_item_likes_dislikes WHERE feed_item_id=feed_item_id_val;