优化 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 毫秒。
我一直在绞尽脑汁寻找解决这个问题的方法,但由于我没有大规模解决此类问题的经验,我不确定哪种方法能更好地解决挑战。我的一些想法:
- 以迭代方式进行查询,我在某些列上过滤 (
WHERE
) 术语(例如,从限制价格子集上的数据开始)以减少数据集。然后,如果结果是 returned,那就太好了,否则再做一个更广泛的查询,依此类推。
- 使用完全不同的算法,可能会为汽车预填充某种相似性度量列
- 利用一些内部 PostgreSQL features/extensions 可以加快速度,不管是什么?
你无法做到那么快,因为你必须对所有查询结果执行前 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 运行 第二个查询 price
的 WHERE
条件将对应于 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-少数最大值
我有一个具有相当大的活动数据集(比方说汽车)的应用程序,其中包含大约 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 毫秒。
我一直在绞尽脑汁寻找解决这个问题的方法,但由于我没有大规模解决此类问题的经验,我不确定哪种方法能更好地解决挑战。我的一些想法:
- 以迭代方式进行查询,我在某些列上过滤 (
WHERE
) 术语(例如,从限制价格子集上的数据开始)以减少数据集。然后,如果结果是 returned,那就太好了,否则再做一个更广泛的查询,依此类推。 - 使用完全不同的算法,可能会为汽车预填充某种相似性度量列
- 利用一些内部 PostgreSQL features/extensions 可以加快速度,不管是什么?
你无法做到那么快,因为你必须对所有查询结果执行前 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 运行 第二个查询 price
的 WHERE
条件将对应于 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-少数最大值