sql - 提高 Postgres 在多级自连接的类图查询上的性能(与 Neo4j 比较)

标签 sql postgresql neo4j

claims Neo4j makes in their marketing 之一是关系数据库不擅长做多级自连接查询:

enter image description here

我找到了 code repository对应于声称的书,以及translated it to Postgres :

CREATE TABLE t_user (
  id bigserial PRIMARY KEY,
  name text NOT NULL
);

CREATE TABLE t_user_friend (
  id bigserial PRIMARY KEY,
  user_1 bigint NOT NULL REFERENCES t_user,
  user_2 bigint NOT NULL REFERENCES t_user
);

CREATE INDEX idx_user_friend_user_1 ON t_user_friend (user_1);
CREATE INDEX idx_user_friend_user_2 ON t_user_friend (user_2);

/* Create 1M users, each getting a random 10-character name */
INSERT INTO t_user (id, name)
  SELECT x.id, substr(md5(random()::text), 0, 10)
  FROM generate_series(1,1000000) AS x(id);

/* For each user, create 50 random friendships for a total of 50M friendship records */
INSERT INTO t_user_friend (user_1, user_2)
  SELECT g1.x AS user_1, (1 + (random() * 999999)) :: int AS user_2
  FROM generate_series(1, 1000000) as g1(x), generate_series(1, 50) as g2(y);

这些是 Neo4j 正在比较的不同深度的查询:

/* Depth 2 */

SELECT
  COUNT(DISTINCT f2.user_2) AS cnt 
FROM
  t_user_friend f1 
  INNER JOIN
    t_user_friend f2 
    ON f1.user_2 = f2.user_1 
WHERE
  f1.user_1 = 1;

/* Depth 3 */

SELECT
  COUNT(DISTINCT f3.user_2) AS cnt 
FROM
  t_user_friend f1 
  INNER JOIN
    t_user_friend f2 
    ON f1.user_2 = f2.user_1 
  INNER JOIN
    t_user_friend f3 
    ON f2.user_2 = f3.user_1 
WHERE
  f1.user_1 = 1;

/* Depth 4 */

SELECT
  COUNT(DISTINCT f4.user_2) AS cnt 
FROM
  t_user_friend f1 
  INNER JOIN
    t_user_friend f2 
    ON f1.user_2 = f2.user_1 
  INNER JOIN
    t_user_friend f3 
    ON f2.user_2 = f3.user_1 
  INNER JOIN
    t_user_friend f4 
    ON f3.user_2 = f4.user_1 
WHERE
  f1.user_1 = 1;

/* Depth 5 */

SELECT
  COUNT(DISTINCT f5.user_2) AS cnt 
FROM
  t_user_friend f1 
  INNER JOIN
    t_user_friend f2 
    ON f1.user_2 = f2.user_1 
  INNER JOIN
    t_user_friend f3 
    ON f2.user_2 = f3.user_1 
  INNER JOIN
    t_user_friend f4 
    ON f3.user_2 = f4.user_1 
  INNER JOIN
    t_user_friend f5 
    ON f4.user_2 = f5.user_1 
WHERE
  f1.user_1 = 1;

我大致能够重现书中声称的结果,得到针对 100 万用户、5000 万 friend 的这些执行时间:

| Depth | Count(*) | Time (s) |
|-------|----------|----------|
| 2     | 2497     | 0.067    |
| 3     | 117301   | 0.118    |
| 4     | 997246   | 8.409    |
| 5     | 999999   | 214.56   |

(这是一个 EXPLAIN ANALYZE of a depth 5 query )

我的问题是,有没有办法提高这些查询的性能,以达到或超过 Neo4j 在深度级别 5 时约 2 秒的执行时间?

我试过这个递归 CTE:

WITH RECURSIVE chain(user_2, depth) AS (
  SELECT t.user_2, 1 as depth
  FROM t_user_friend t
  WHERE t.user_1 = 1
UNION
  SELECT t.user_2, c.depth + 1 as depth
  FROM t_user_friend t, chain c
  WHERE t.user_1 = c.user_2
  AND depth < 4
)
SELECT COUNT(*)
FROM (SELECT DISTINCT user_2 FROM chain) AS temp;

但是它仍然很慢,深度 4 需要 5 秒,深度 5 需要 48 秒(EXPLAIN ANALYZE)

最佳答案

我想从一开始就指出,比较关系数据库和非关系数据库并不是同类比较。

当数据更新时,非关系数据库很可能会维护一些额外的预先计算的结构。这会使更新速度变慢并且需要更多磁盘空间。您使用的纯关系模式没有任何额外的东西,这使得更新尽可能快并将磁盘使用率保持在最低水平。

我将重点介绍可以使用给定架构完成的操作。


一开始我会做一个复合索引

CREATE INDEX idx_user_friend_user_12 ON t_user_friend (user_1, user_2);

一个这样的索引就足够了。

那么,我们知道总共只有100万用户,所以最终结果不会超过100万。

5 级查询最终生成 312.5M 行 (50*50*50*50*50)。这远远超过最大可能结果,这意味着有很多重复项。

因此,我会尝试具体化中间结果并在过程的早期消除重复项。

我们知道 Postgres 实现了 CTE,所以我会尝试使用它。

像这样:

WITH
CTE12
AS
(
    SELECT
        DISTINCT f2.user_2
    FROM
        t_user_friend f1 
        INNER JOIN t_user_friend f2 ON f1.user_2 = f2.user_1
    WHERE
        f1.user_1 = 1
)
,CTE3
AS
(
    SELECT
        DISTINCT f3.user_2
    FROM
        CTE12
        INNER JOIN t_user_friend f3 ON CTE12.user_2 = f3.user_1
)
,CTE4
AS
(
    SELECT
        DISTINCT f4.user_2
    FROM
        CTE3
        INNER JOIN t_user_friend f4 ON CTE3.user_2 = f4.user_1
)
SELECT
    COUNT(DISTINCT f5.user_2) AS cnt
FROM
    CTE4
    INNER JOIN t_user_friend f5 ON CTE4.user_2 = f5.user_1
;

SELECT DISTINCT 很可能需要排序,这将允许使用合并联接。


据我从上述查询的执行计划中理解 https://explain.depesz.com/s/Sjov , Postgres 不够聪明,做了一些不必要的排序。此外,它对某些 SELECT DISTINCT 使用哈希聚合,这需要额外的排序。

因此,下一步的尝试是明确地为每个步骤使用具有适当索引的临时表。

另外,我将 idx_user_friend_user_12 索引定义为唯一的。它可能会为优化器提供额外的提示。

看看下面的表现会很有趣。

CREATE TABLE temp12
(
    user_2 bigint NOT NULL PRIMARY KEY
);
CREATE TABLE temp3
(
    user_2 bigint NOT NULL PRIMARY KEY
);
CREATE TABLE temp4
(
    user_2 bigint NOT NULL PRIMARY KEY
);

INSERT INTO temp12(user_2)
SELECT
    DISTINCT f2.user_2
FROM
    t_user_friend f1 
    INNER JOIN t_user_friend f2 ON f1.user_2 = f2.user_1
WHERE
    f1.user_1 = 1
;

INSERT INTO temp3(user_2)
SELECT
    DISTINCT f3.user_2
FROM
    temp12
    INNER JOIN t_user_friend f3 ON temp12.user_2 = f3.user_1
;

INSERT INTO temp4(user_2)
SELECT
    DISTINCT f4.user_2
FROM
    temp3
    INNER JOIN t_user_friend f4 ON temp3.user_2 = f4.user_1
;

SELECT
    COUNT(DISTINCT f5.user_2) AS cnt
FROM
    temp4
    INNER JOIN t_user_friend f5 ON temp4.user_2 = f5.user_1
;

DROP TABLE temp12;
DROP TABLE temp3;
DROP TABLE temp4;

作为显式临时表的额外好处,您可以测量每个额外级别花费的时间。

关于sql - 提高 Postgres 在多级自连接的类图查询上的性能(与 Neo4j 比较),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52674380/

相关文章:

postgresql - 如何在 PostgreSQL 查询中声明变量

Neo4J - 将元素存储为用户属性或节点和关系哪个更好?

sql - 根据同一表中的两行检索一行

nosql - 图形数据库和网络数据库有什么区别?

database - 使用 Neo4j 执行任意查询

mysql - SQL索引优化WHERE查询

sql - 如何在数据库中查找最后一次修改

mysql - SQL 创建新表并从另一个表插入数据

sql - 将 sas 变量传递给 sql pass through 语句

sql-server - 如何在 Node.JS 应用程序中同时支持 PostgreSQL 和 SQL Server?