优化 SQL 数据库中动态排序查询性能的设计模式

Design pattern for optimizing query performance of dynamic sorts in SQL DB

我有一个具有相当大的活​​动数据集(比方说汽车)的应用程序,其中包含大约 200 万个活动数据行。每个 "car" 都有许多属性(列),如价格、里​​程、年份、品牌、型号、燃料类型等。

现在,在我的 Web 应用程序中每辆汽车的 /show 页面上,我需要生成前 10 名最多 "similar" 汽车的列表。因为我从来没有 "know" 如果汽车是一种非常常见或非常罕见的汽车(在实际进行数据库查询之前),我设计了一种模式,我几乎不做任何过滤(WHERE-子句) "similar-cars"-查询。相反,我做了很多 ORDER BY-子句,并结合了 CASE WHEN-基于视图中当前汽车数据的语句。假设用户正在看 Ford Focus, 2010, 30.000km, Gasoline, 12490EUR from around Düsseldorf 辆汽车。然后我会做类似的事情:

SELECT "cars".*
  FROM de."cars" 
  WHERE ("cars"."id" != 24352543) 
    AND "cars"."sales_state" = 'onsale' 
    AND (cars.is_disabled IS NOT TRUE) 
    ORDER BY
      CASE WHEN ABS(cars.price - 12490) < cars.price * 0.2 THEN 1 WHEN ABS(cars.price - 12490) < cars.price * 0.4 THEN 2 WHEN ABS(cars.price - 12490) < cars.price * 0.6 THEN 3 ELSE 4 END, 
      CASE WHEN fuel_type = 'Gasoline' THEN 0 ELSE 1 END, 
      ABS(cars.price - 12490), 
      CASE WHEN ST_Distance( ST_GeographyFromText( 'SRID=4326;POINT(' || cars.longitude || ' ' || cars.latitude || ')' ), ST_GeographyFromText('SRID=4326;POINT(12.172130 48.162990)') ) <= 30000 THEN 1 WHEN ST_Distance( ST_GeographyFromText( 'SRID=4326;POINT(' || cars.longitude || ' ' || cars.latitude || ')' ), ST_GeographyFromText('SRID=4326;POINT(12.172130 48.162990)') ) <= 100000 THEN 2 ELSE 3 END, 
      ABS(cars.year - 2010), 
      ABS(cars.km - 30000)
    LIMIT 10

实际上还有更多的排序子句。

现在这很方便,因为无论如何 "easy" 找到 10 "relevant" 辆与当前汽车相似的汽车,查询总是 return 一些东西 - 问题是 - 它很慢 并且几乎不可能根据我的知识进行索引。在 200 万条记录上执行此操作,即使我有一个调整良好的超快速专用 PostgreSQL 11、300GB 内存、10 SSD RAID 10 32 核服务器,这仍然需要我大约 2-4 秒,我没有时间。我需要它小于 200 毫秒。

我一直在绞尽脑汁寻找解决这个问题的方法,但由于我没有大规模解决此类问题的经验,我不确定哪种方法能更好地解决挑战。我的一些想法:

你无法做到那么快,因为你必须对所有查询结果执行前 N 排序,即使你加速 work_mem

ORDER BY 子句不可索引。

如果您的查询更灵活一点,也许您可​​以尝试这样的事情:

第一次查询:

WITH priced_cars AS (
  SELECT SELECT cars.*
    FROM de.cars
    WHERE (cars.id != 24352543)
      AND cars.sales_state = 'onsale'
      AND (cars.is_disabled IS NOT TRUE)
      AND cars.price BETWEEN 12490*5/6 AND 12490*5/4
)
SELECT * FROM priced_cars
ORDER BY
  CASE WHEN fuel_type = 'Gasoline' THEN 0 ELSE 1 END, 
  ABS(price - 12490), 
  CASE
    WHEN ST_Distance( ST_GeographyFromText( 'SRID=4326;POINT(' || longitude || ' ' || latitude || ')' ), ST_GeographyFromText('SRID=4326;POINT(12.172130 48.162990)') ) <= 30000
    THEN 1
    WHEN ST_Distance( ST_GeographyFromText( 'SRID=4326;POINT(' || longitude || ' ' || latitude || ')' ), ST_GeographyFromText('SRID=4326;POINT(12.172130 48.162990)') ) <= 100000
    THEN 2
    ELSE 3
  END, 
  ABS(year - 2010), 
  ABS(km - 30000)
LIMIT 10;

此查询可以使用这样的索引:

CREATE INDEX ON de.cars (price)
  WHERE sales_state = 'onsale' AND is_disabled IS NOT TRUE;

这将仅对应于第一个 ORDER BY 列为 1 的汽车,但它可以很快,因为它可以使用索引。

如果你这样找到 10 辆车,你就完成了。

Else 运行 第二个查询 priceWHERE 条件将对应于 price 上的下一个最佳标准,它再次可以使用相同的索引, 但会慢一些。

继续这样做,直到你有 10 辆车(最后一个查询将没有条件 price,并且会像以前一样慢)。

如果你必须 运行 四个这样的查询,这将是一个损失,因为在前三个查询中你找不到 10 辆车,但在其他情况下可能会更快。

对于可能的 sql 复杂性和转移(许多不同的模式)和你提到的时间(250 毫秒),我应该强制 sql 遵循 'plan' 简单有效一次分解一个过滤器。

我在一个循环中执行我的(每次)随机过滤器集,从我认为更重要的过滤器中选择 PK,然后在每个其他循环中加入 Pk。

这样你就有机会在所有随机过滤器集上获得最佳时间,而且你可能很快就知道 0 个结果。

更多详细信息-示例: 首先,您将注意力集中在您搜索的项目上,我相信这是 car.id。所以你需要一组 Car.id 随机过滤器的值。假设您有 20 个可能的过滤器。每个过滤器都会产生一组 car.id 个值。某些过滤器可能直接在 car.id 所在的 table 处工作。其他一些可能需要连接到 1-2 或 3 tables。然而,所有过滤器一起可能需要 10-15 个连接。 table 加入的人越少,获得好的计划的机会就越大。

假设您有 3 个过滤器,过滤器 2、7 和 14。加入例如12 tables 和使用这 3 个过滤器的过滤器可能有效也可能无效。如果是,另一个组合将不会。所以我建议的是(伪代码):

procedure/table function get carids as
for each optional filter 1 to 20
 if filter is set
  select car.id from car (possible joins) where filter=filter.value and car.id 
  in (previous car.id found)
  if count(car.id)=0 end and return no results
 end if
end for
return car.id collected

您可以选择指定过滤器的处理顺序。 如果您知道一组 5-6 个过滤器中至少有一个在 99% 的搜索中使用,那么首先对它们进行排序将导致在前 5 个选择中将 car.id 值缩小到 0-少数最大值