【问题标题】:Complex User Matching Algorithm Performance复杂的用户匹配算法性能
【发布时间】:2018-01-31 20:17:20
【问题描述】:

我正在为一个拥有超过 400,000 个用户的应用程序编写算法。

表格

User <- holds users and their latitude, longitude, and the last time they were active. Swipes <- when a user has already seen someone, a record is inserted in here.

我的匹配算法应该获取与请求用户有一定距离的用户,他们以前没有见过,还应该获取活跃用户和一段时间没有活跃用户的组合。

我已尽最大努力尽可能地记录实现,以便于理解。这是我当前的实现:

SELECT id,
       distance
FROM
  ( 

  -- This is done so that the users that returned can be numbered 1 through x partitioned by the buckets and ordered by the distance away.
  SELECT id,
         distance,
         row_number() OVER (PARTITION BY buckets
                              ORDER BY distance) AS bucket_interval

   FROM
     (

     -- This inner query will fetch all of the users and create a column called "buckets" that will separate users depending on how active they are
      SELECT id,


            -- This is the "buckets" column
             CASE
                 WHEN now() - last_active_at < interval '1' DAY THEN 1
                 WHEN now() - last_active_at < interval '2' DAY THEN 2
                 WHEN now() - last_active_at < interval '5' DAY THEN 3
                 WHEN now() - last_active_at < interval '10' DAY THEN 4
                 ELSE 5
             END AS buckets,


             -- This column gets the distance of the current user to the other users
             3958.755864232 * 2 * ASIN(SQRT(POWER(SIN((51.6900092 - users.latitude) * PI() / 180 / 2), 2) + COS(51.6900092 * PI() / 180) * COS(users.latitude * PI() / 180) * POWER(SIN((-8.14594 - users.longitude) * PI() / 180 / 2), 2))) AS distance,

      FROM "users"


      -- This first condition will make sure people are within the desired distance (0 to 100 miles away in this case)

      WHERE (users.latitude BETWEEN -31.15177391077796 AND 134.31179231344798
             AND users.longitude BETWEEN -252.66535853758625 AND 163.78387854758624
             AND (3958.755864232 * 2 * ASIN(SQRT(POWER(SIN((51.6900092 - users.latitude) * PI() / 180 / 2), 2) + COS(51.6900092 * PI() / 180) * COS(users.latitude * PI() / 180) * POWER(SIN((-8.14594 - users.longitude) * PI() / 180 / 2), 2)))) BETWEEN 0.0 AND 100)


        -- This second condition will make sure the current user hasn't swiped through this person already

        AND users.id NOT IN
               (SELECT "swipes"."connection_id"
                FROM "swipes"
                WHERE user_id = currentUserId)
    ) x 
  ) xx

-- This is done because I only want to fetch 50 matches at a time. Since there are "5" buckets... each of them will have 10 people ordered by distance
WHERE bucket_interval <= 10
ORDER BY distance ASC
LIMIT 50

这是 ruby​​ on rails 中的实现,我正在使用一个名为 geocoder 的 gem 来获取实际距离和所有这些。

然而,ruby 实现并不像应该如何编写此查询以使其尽可能优化那么重要。

它跑得不快。

谢谢

编辑:这是查询计划

    [
  {
    "Plan": {
      "Startup Cost": 80238.35,
      "Plans": [
        {
          "Startup Cost": 80238.35,
          "Plans": [
            {
              "Startup Cost": 80238.24,
              "Plans": [
                {
                  "Startup Cost": 80238.24,
                  "Plans": [
                    {
                      "Startup Cost": 80238.24,
                      "Plans": [
                        {
                          "Filter": "(((NOT archived) OR (archived IS NULL)) AND (NOT is_suspended) AND (image_urls IS NOT NULL) AND (latitude >= (-90.151773910848)::double precision) AND (latitude <= 199.311792310848::double precision) AND (longitude >= (-255.665358277586)::double precision) AND (longitude <= 243.783878277586::double precision) AND (NOT (hashed SubPlan 1)) AND (NOT (hashed SubPlan 2)) AND ((lower((gender)::text) = 'f'::text) OR (lower((gender)::text) = 'female'::text)) AND ((7917.511728464::double precision * asin(sqrt((power(sin(((((54.5800092::double precision - latitude) * 3.14159265358979::double precision) / 180::double precision) / 2::double precision)), 2::double precision) + ((0.579565539469435::double precision * cos(((latitude * 3.14159265358979::double precision) / 180::double precision))) * power(sin((((((-44.9774)::double precision - longitude) * 3.14159265358979::double precision) / 180::double precision) / 2::double precision)), 2::double precision)))))) >= 0::double precision) AND ((7917.511728464::double precision * asin(sqrt((power(sin(((((54.5800092::double precision - latitude) * 3.14159265358979::double precision) / 180::double precision) / 2::double precision)), 2::double precision) + ((0.579565539469435::double precision * cos(((latitude * 3.14159265358979::double precision) / 180::double precision))) * power(sin((((((-44.94074)::double precision - longitude) * 3.14159265358979::double precision) / 180::double precision) / 2::double precision)), 2::double precision)))))) <= 10000::double precision))",
                          "Startup Cost": 1722.06,
                          "Plans": [
                            {
                              "Startup Cost": 0.11,
                              "Scan Direction": "Forward",
                              "Plan Width": 4,
                              "Node Type": "Index Scan",
                              "Index Cond": "(user_id = 231415)",
                              "Plan Rows": 1955,
                              "Relation Name": "meets",
                              "Alias": "meets",
                              "Parent Relationship": "SubPlan",
                              "Total Cost": 1713.44,
                              "Subplan Name": "SubPlan 2",
                              "Index Name": "index_meets_on_user_id"
                            }
                          ],
                          "Node Type": "Seq Scan",
                          "Plan Rows": 4,
                          "Relation Name": "users",
                          "Alias": "users",
                          "Parent Relationship": "Outer",
                          "Plan Width": 28,
                          "Total Cost": 80238.23
                        }
                      ],
                      "Sort Key": [
                        "(CASE WHEN ((now() - (users.last_active_at)::timestamp with time zone) < '1 day'::interval day) THEN 1 WHEN ((now() - (users.last_active_at)::timestamp with time zone) < '2 days'::interval day) THEN 2 WHEN ((now() - (users.last_active_at)::timestamp with time zone) < '5 days'::interval day) THEN 3 WHEN ((now() - (users.last_active_at)::timestamp with time zone) < '10 days'::interval day) THEN 4 ELSE 5 END)",
                        "((7917.511728464::double precision * asin(sqrt((power(sin(((((54.5800092::double precision - users.latitude) * 3.14159265358979::double precision) / 180::double precision) / 2::double precision)), 2::double precision) + ((0.579565539469435::double precision * cos(((users.latitude * 3.14159265358979::double precision) / 180::double precision))) * power(sin((((((-5.94074)::double precision - users.longitude) * 3.14159265358979::double precision) / 180::double precision) / 2::double precision)), 2::double precision)))))))"
                      ],
                      "Plan Rows": 4,
                      "Node Type": "Sort",
                      "Parent Relationship": "Outer",
                      "Plan Width": 28,
                      "Total Cost": 80238.24
                    }
                  ],
                  "Plan Rows": 4,
                  "Node Type": "WindowAgg",
                  "Parent Relationship": "Subquery",
                  "Plan Width": 28,
                  "Total Cost": 80238.33
                }
              ],
              "Node Type": "Subquery Scan",
              "Plan Rows": 1,
              "Filter": "(xx.bucket_interval <= 10)",
              "Alias": "xx",
              "Parent Relationship": "Outer",
              "Plan Width": 12,
              "Total Cost": 80238.35
            }
          ],
          "Sort Key": [
            "xx.distance"
          ],
          "Plan Rows": 1,
          "Node Type": "Sort",
          "Parent Relationship": "Outer",
          "Plan Width": 12,
          "Total Cost": 80238.35
        }
      ],
      "Plan Rows": 1,
      "Node Type": "Limit",
      "Plan Width": 12,
      "Total Cost": 80238.35
    }
  }
]

【问题讨论】:

  • 距离是否足够短,可以用毕达哥拉斯定理来近似距离?性能会好很多。
  • 代替 NOT IN,在许多 SQL 实现中,实现左连接和使用 IS NULL 是一个更快的想法。
  • @theMayer 你能告诉我更多吗?你有处理非常大的数据集的经验吗? swipe 表有超过 1.25 亿条记录。我之前读过一篇文章说完全相反,这就是我选择 NOT IN 的原因。很想听听您对此的想法
  • 这么说吧 - 125 M 正在接近我认为的“大”:)
  • @theMayer ooooooooo haha​​ha

标签: sql ruby-on-rails postgresql performance


【解决方案1】:

要尝试的一件事是替换您的“NOT IN”语句,因为这些语句被称为extremely slow

代替

FROM "users"
...
AND users.id NOT IN
               (SELECT "swipes"."connection_id"
                FROM "swipes"
                WHERE user_id = currentUserId)

使用

FROM "users" u
LEFT JOIN "swipes" s
        ON s.user_id = currentUserId

... 

WHERE   s.user_id IS NULL

【讨论】:

  • explainextended.com/2009/09/15/… 这篇文章表明 LEFT JOIN / NULL 比 NOT IN 花费的时间长 3 倍
  • 我不是要挑战你的问题。我只是想了解两者之间的推理,而不是这比这更好。感谢您的宝贵时间
  • 试一试,看看会发生什么。每个服务器引擎都是不同的。关于 RDBMS,您必须了解的是,从根本上说,它是一堆代码,试图自动找出获取 SELECT 描述的数据的最快方法。在这方面,猴子通常可以胜过 RDBMS 引擎。
  • 我会说,您链接的那篇文章使用的是 MSSQL。我在查询优化中的第一课是用 LEFT JOIN/IS NULL 替换一个非常大的表(带时间戳的数据)的 NOT IN。如果我没记错的话,MS 围绕 SQL 2008 调整了他们的查询优化器,以使用更有效的 NOT IN 实现。不过,这仍然不会自动利用索引。并注意您的计划中没有像文章那样包含反半联接。
  • 查看包含总成本的更新计划。我有点难以理解。您知道优化工作应该在哪里吗?通过查看它,我确实意识到左连接的潜在好处可能无关紧要,因为没有很多成本?如我错了请纠正我。谢谢
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-07-04
  • 1970-01-01
  • 2015-10-20
  • 2012-10-12
  • 2017-09-20
  • 2015-04-25
相关资源
最近更新 更多