在 MySQL 数据库中存储具有价格历史和不同特异性(全局、用户组、单用户)的价格

Dju*_*uka 5 mysql performance subquery query-performance

我需要用他们的历史来存储价格。价格可以具有不同的特异性。它可以指单个用户(最具体)、组(不太具体)和产品价格(全局 - 最不具体)。还有两种不同类型的价格(产品价格和交货价格)。现在我遇到了这个问题。我让它工作了,但是当我编写查询时我不知何故感觉不好(它们往往变得冗长而复杂)。

我设计了两个这样的表(它们是相同的):

基本价格

| id | referent_id | product_id | valid_from | valid_until | amount | pieces_per_lot |
Run Code Online (Sandbox Code Playgroud)

交货价格

| id | referent_id | product_id | valid_from | valid_until | amount | pieces_per_lot |
Run Code Online (Sandbox Code Playgroud)

对于用户特定的价格参考 id 将是一个正整数,匹配用户 id。对于特定于组的价格参考 id 将是一个负整数,匹配组 id(我将组 id 设为负整数)。对于全球产品价格,参考 ID 将为 NULL。这样,当我查询特定项目和特定用户及其组的价格时,我需要做的就是按 item_id 过滤表并按 referent_id 列按降序排列结果。这样,特定于用户的在顶部,组之后和全局在最后。

两行的日期范围 valid_from-valid_until 不能重叠。

现在,获取当前价格的查询将如下所示:

SELECT 
    * 
FROM 
    base_prices AS outer 
WHERE 
    id = (
        SELECT 
            id 
        FROM 
            base_prices AS inner 
        WHERE 
                NOW() BETWEEN valid_from AND valid_until
            AND inner.item_id = outer.item_id
            AND (
                    inner.referent_id = # HERE GOES USER ID
                OR  inner.referent_id = # HERE GOES GROUP ID
                OR  inner.referent_id IS NULL
            )
        ORDER BY
                inner.referent_id DESC # FOR SPECIFICITY
Run Code Online (Sandbox Code Playgroud)

困扰我的是我总是必须编写这种子查询来获取项目,这对我来说似乎不是一个好的设计。当数据开始建立时,此查询将花费更多。此外,我必须编写一些触发器以在插入和更新期间保持数据完整性。

我的观点是当前价格和历史价格需要分开,但我很着急,所以这就是出现的原因。我正在考虑在基于 valid_from 列的插入期间安排事件,该列会将数据从历史价格表移动到(新)当前价格表。

我没有经验,所以这对我来说似乎有点奇怪。不过,这可能没问题,但我希望看到一些建议和意见。

如果我遗漏了什么或没有足够的信息,请发表评论。

Dav*_*ett 1

有几种方法可以将当前值与历史值分开:您可以简单地包含一个对于最新价格为 true 的布尔字段并对其进行过滤,或者您可以将当前价格保留在单独的表中。这两种选择都需要一些额外的工作来保持完整性,但会使当前价格的查询更加高效。这并不能消除子查询选择与用户/组/NULL 条件匹配的第一行的需要,尽管在处理内部和外部查询时将扫描更少的行。

如果基表中只有当前价格,则以下内容作为直接查询可能会更有效(如果将历史值保留在同一个表中,请将is_latest=true支票或当前日期过滤器添加到whereand子句中):on

SELECT CASE WHEN usermatch.id IS NOT NULL THEN usermatch.field1 WHEN groupmatch.id IS NOT NULL THEN groupmatch.field1 ELSE nullmatch.field1 END AS field1
     , CASE WHEN usermatch.id IS NOT NULL THEN usermatch.field2 WHEN groupmatch.id IS NOT NULL THEN groupmatch.field2 ELSE nullmatch.field2 END AS field2
     (... and so on ...)
     , CASE WHEN usermatch.id IS NOT NULL THEN usermatch.fieldN WHEN groupmatch.id IS NOT NULL THEN groupmatch.fieldN ELSE nullmatch.fieldN END AS fieldN
FROM   base_prices AS nullmatch
LEFT OUTER JOIN 
       base_prices AS groupmatch ON groupmatch.id=nullmatch.id AND nullmatch.referent_id = @GroupIDHere
LEFT OUTER JOIN 
       base_prices AS usermatch ON usermatch.id=nullmatch.id AND nullmatch.referent_id = @UserIDHere
WHERE nullmatch.referent_id IS NULL
Run Code Online (Sandbox Code Playgroud)

仅当每个产品始终有一行referent_id 为空时,这才有效,否则您可以尝试:

SELECT CASE WHEN usermatch.id IS NOT NULL THEN usermatch.field1 WHEN groupmatch.id IS NOT NULL THEN groupmatch.field1 ELSE nullmatch.field1 END AS field1
     , CASE WHEN usermatch.id IS NOT NULL THEN usermatch.field2 WHEN groupmatch.id IS NOT NULL THEN groupmatch.field2 ELSE nullmatch.field2 END AS field2
     (... and so on ...)
     , CASE WHEN usermatch.id IS NOT NULL THEN usermatch.fieldN WHEN groupmatch.id IS NOT NULL THEN groupmatch.fieldN ELSE nullmatch.fieldN END AS fieldN
FROM   users
CROSS JOIN 
       base_prices AS nullmatch
LEFT OUTER JOIN 
       base_prices AS groupmatch ON groupmatch.id=nullmatch.id AND nullmatch.referent_id = users.groupid
LEFT OUTER JOIN 
       base_prices AS usermatch ON usermatch.id=nullmatch.id AND nullmatch.referent_id = users.id
WHERE users.id = @UserIDHere
AND   nullmatch.referent_id IS NULL
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,在视图中使用此构造时都要小心,因为通过这些 case 语句导出的字段进一步过滤将无法使用您可能定义的任何索引。

显然,每个字段的 case 语句有点麻烦,但它应该停止查询运行程序为返回的每一行搜索一次表,就像子查询安排一样。

在其他数据库中,您可能可以使用窗口函数(如ROW_NUMBER)来更方便、更高效地完成此操作,但据我所知,mySQL 不支持这些功能。