【问题标题】:MySQL query to get "intersection" of numerous queries with limitsMySQL查询以获取具有限制的众多查询的“交集”
【发布时间】:2015-01-25 10:17:10
【问题描述】:

假设我有一个包含以下字段的 mySQL 表(用户):

userid  
gender  
region  
age  
ethnicity  
income

我希望能够根据用户输入的数字返回总记录数。此外,他们还将提供额外的标准。

在最简单的示例中,他们可能会要求 1,000 条记录,其中 600 条记录的性别 = '男性',而 400 条记录的性别 = '女性'。这很简单。

现在,更进一步。假设他们现在想要指定区域:

GENDER  
    Male:   600 records  
    Female: 400 records  

REGION  
    North:  100 records  
    South:  200 records  
    East:   300 records  
    West:   400 records

同样,应该只返回 1000 条记录,但最终必须有 600 条男性、400 条女性、100 条北方人、200 条南方人、300 条东方人和 400 条西方人。

我知道这不是有效的语法,但使用伪 mySQL 代码,它有望说明我正在尝试做的事情:

(SELECT * FROM users WHERE gender = 'Male' LIMIT 600  
UNION  
SELECT * FROM users WHERE gender = 'Female' LIMIT 400)

INTERSECT

(SELECT * FROM users WHERE region = 'North' LIMIT 100  
UNION  
SELECT * FROM users WHERE region = 'South' LIMIT 200  
UNION  
SELECT * FROM users WHERE region = 'East' LIMIT 300  
UNION  
SELECT * FROM users WHERE region = 'West' LIMIT 400)

请注意,我不是在寻找一次性查询。每个标准内的记录总数和记录数将根据用户的输入不断变化。因此,我试图提出一个可以反复使用的通用解决方案,而不是硬编码的解决方案。

为了让事情变得更复杂,现在添加更多条件。还可能有年龄、种族和收入,每个组都有自己设定的记录数,附加代码附加到上面:

INTERSECT

(SELECT * FROM users WHERE age >= 18 and age <= 24 LIMIT 300  
UNION  
SELECT * FROM users WHERE age >= 25 and age <= 36 LIMIT 200  
UNION  
SELECT * FROM users WHERE age >= 37 and age <= 54 LIMIT 200  
UNION  
SELECT * FROM users WHERE age >= 55 LIMIT 300)  

INTERSECT

etc.

我不确定这是否可以写在一个查询中,或者这是否需要多个语句和迭代。

【问题讨论】:

  • 我的问题是什么 mySQL 查询会起作用?我在 mySQL 中没有看到“相交”语句,所以我不确定哪种类型的语句/函数/语法会起作用。我正在寻找有关如何编写此类查询的建议。
  • 嗯,老实说,我不知道如何在 SQL 级别上进行操作。我可能会一个接一个地选择随机记录,一旦一个桶“满”,我就会开始将该特定属性值列入黑名单。话又说回来,这可能会导致一个不工作的集合(如果所有其他人都是男性,但我对来自另一个地方的男性充满了......这是一个错误)。
  • 我不认为你的问题是如何在 MySQL 中表达这些查询。我认为您的问题是当用户提供多个独立维度的计数时如何处理分层抽样。
  • 嗯,我的编辑有点扼杀了 BOLD 位 - 但它通常更具可读性:-$
  • 尝试以某种方式为此构建查询绝对是错误的方法,因为该查询将携带应用程序逻辑。需要解决的问题是 - 在应用程序级别创建过滤器/条件/任何内容,然后将其转换为查询(或查询)以进行存储(可能不仅是 MySQL,还可能是一些缓存层等)。否则,您将被硬编码到 SQL 中的一个“实现”所困,当扩展/修改它的时间到来时,这将是一件痛苦的事。哦,天哪,这将是一种痛苦..

标签: mysql sql database select inner-join


【解决方案1】:

扁平化你的标准


您可以将多维标准扁平化为单级标准

现在这个条件可以在一个查询中实现,如下所示

(SELECT * FROM users WHERE gender = 'Male' AND region = 'North' LIMIT 40) UNION ALL
(SELECT * FROM users WHERE gender = 'Male' AND region = 'South' LIMIT 80) UNION ALL
(SELECT * FROM users WHERE gender = 'Male' AND region = 'East' LIMIT 120) UNION ALL
(SELECT * FROM users WHERE gender = 'Male' AND region = 'West' LIMIT 160) UNION ALL
(SELECT * FROM users WHERE gender = 'Female' AND region = 'North' LIMIT 60) UNION ALL
(SELECT * FROM users WHERE gender = 'Female' AND region = 'South' LIMIT 120) UNION ALL
(SELECT * FROM users WHERE gender = 'Female' AND region = 'East' LIMIT 180) UNION ALL
(SELECT * FROM users WHERE gender = 'Female' AND region = 'West' LIMIT 240)

问题

  • 它并不总是返回正确的结果。例如,如果来自北方的男性用户少于 40 人,则查询将返回少于 1,000 条记录。

调整您的标准


假设有少于 40 个男性和北方用户。然后,您需要调整其他标准数量以覆盖“男性”和“北方”中缺失的数量。我相信用裸 SQL 是不可能做到的。这是我想到的伪代码。为了简单起见,我想我们只会查询男性、女性、北方和南方

conditions.add({ gender: 'Male',   region: 'North', limit: 40  })
conditions.add({ gender: 'Male',   region: 'South', limit: 80  })
conditions.add({ gender: 'Female', region: 'North', limit: 60  })
conditions.add({ gender: 'Female', region: 'South', limit: 120  })

foreach(conditions as condition) {
    temp = getResultFromDatabaseByCondition(condition)
    conditions.remove(condition)

    // there is not enough result for this condition,
    // increase other condition quantity
    if (temp.length < condition.limit) {
        adjust(...);
    }
}

假设只有30个北方男性。所以我们需要调整+10男性和+10北方人。

To Adjust
---------------------------------------------------
Male        +10
North       +10

Remain Conditions
----------------------------------------------------
{ gender: 'Male',   region: 'South', limit: 80 }
{ gender: 'Female', region: 'North', limit: 60  }
{ gender: 'Female', region: 'South', limit: 120  }

'Male' + 'South' 是匹配 'Male' 调整条件的第一个条件。将其增加+10,并将其从“剩余条件”列表中删除。因为,我们增加了南方,我们需要在其他条件下减少它。所以将“南”条件添加到“调整”列表中

To Adjust
---------------------------------------------------
South       -10
North       +10

Remain Conditions
----------------------------------------------------
{ gender: 'Female', region: 'North', limit: 60  }
{ gender: 'Female', region: 'South', limit: 120  }

Final Conditions
----------------------------------------------------
{ gender: 'Male',   region: 'South', limit: 90 }

找到匹配“南”的条件并重复相同的过程。

To Adjust
---------------------------------------------------
Female      +10
North       +10

Remain Conditions
----------------------------------------------------
{ gender: 'Female', region: 'North', limit: 60  }

Final Conditions
----------------------------------------------------
{ gender: 'Female', region: 'South', limit: 110  }
{ gender: 'Male',   region: 'South', limit: 90 }

最后

{ gender: 'Female', region: 'North', limit: 70  }
{ gender: 'Female', region: 'South', limit: 110  }
{ gender: 'Male',   region: 'South', limit: 90 }

我还没有想出调整的确切实现。这比我预期的要困难。一旦我弄清楚如何实现它,我会更新。

【讨论】:

  • 看起来很有希望!也许使用 GROUP BY 计数创建一个临时表可以更容易地阐明哪些标准受到限制,从而找出哪些标准/限制可以轻松弥补数字。问题变成了:这应该是一个完整的分组,还是一个结合了 N 个标准中的 2 个的矩阵。
  • 看看stackoverflow.com/a/27468858/4350148。在那里我证明了解决这个问题等同于找到线性方程组的整数解。
  • 下面我在 stackoverflow.com/a/27468858/4350148 中提出的解决方案展示了如何针对任意数量的维度解决此问题。您指出了一种方法,当只有 2 个维度时,当分裂精确出现时。例如,如果分裂为 399 名女性和 601 名男性,您将如何解决问题。那么还有更多的维度呢。正如我所展示的,无论你做什么,你都必须解决双音阶方程组。
  • 如果您查看 stackoverflow.com/a/27468858/4350148 并对其发表评论,那就太好了。在那里,我提出了一个完整的解决方案,但您却获得了赏金(即使这不能解决完整问题,甚至不能解决特定拆分为 600、400 的二维特定问题)。我关心的不是赏金,而是这个问题的优雅解决方案会被忽视。
  • @danb,(1) 此解决方案不限于二维。我只是展示二维的例子。 (2)如果有399个女性,你可以照常划分并向下取整。最后一个标准将是剩余值。比如399 - (39 + 79 + 119) = 163,所以是39、79、119、163
【解决方案2】:

您描述的问题是多维建模问题。特别是,您正试图同时获得沿多个维度的分层样本。这样做的关键是深入到最小的粒度级别并从那里建立样本。

我进一步假设您希望样本在所有级别都具有代表性。也就是说,您不希望“North”的所有用户都是女性。或者所有“男性”都来自“西方”,即使这确实符合最终标准。

首先考虑记录总数、维度和每个维度的分配。例如,对于第一个样本,将其视为:

  • 1000 条记录
  • 2 个维度:性别、地区
  • 性别划分:60%、40%
  • 区域划分:10%、20%、30%、40%

然后,您希望将这些数字分配给每个性别/地区组合。数字是:

  • 北,男:60
  • 北,女:40
  • 南,男:120
  • 南,女:80
  • 东,男:180
  • 东,女:120
  • 西,男:240
  • 西,女:160

您会看到这些沿着维度相加。

计算每个单元格中的数字非常简单。它是百分比乘以总数的乘积。所以,“东方,女性”是 30%*40% * 1000 。 . .瞧!值为 120。

解决办法如下:

  1. 将每个维度的输入作为总数的百分比。并确保它们在每个维度上的总和达到 100%。
  2. 为每个单元格创建一个预期百分比表。这是每个维度的百分比乘积。
  3. 将预期百分比乘以总总数。
  4. 最终查询概述如下。

假设您有一个表 cells,其中包含预期计数和原始数据 (users)。

select enumerated.*
from (select u.*,
             (@rn := if(@dims = concat_ws(':', dim1, dim2, dim3), @rn + 1,
                        if(@dims := concat_ws(':', dim1, dim2, dim3), 1, 1)
                       )
             ) as seqnum
      from users u cross join
           (select @dims = '', @rn := '') vars
      order by dim1, dim2, dim3, rand()
     ) enumerated join
     cells
     on enumerated.dims = cells.dims
where enuemrated.seqnum <= cells.expectedcount;

请注意,这是解决方案的草图。您必须填写有关尺寸的详细信息。

只要您有足够的数据用于所有单元格,这将起作用。

在实践中,在进行这种类型的多维分层抽样时,确实存在单元格为空或太小的风险。发生这种情况时,您通常可以在之后通过额外的通道来解决此问题。尽可能从足够大的细胞中获取。这些通常占所需数据的大部分。然后添加记录以满足最终计数。要添加的记录是那些其值与最需要的维度相匹配的记录。但是,此解决方案只是假设有足够的数据来满足您的条件。

【讨论】:

  • 有趣的行号方法,需要对大数字进行全表扫描和重排序,但是由于我们希望随机化未给出的维度,如果所需的维度完全覆盖表格,这将是不可避免的(即:例如,每个人都来自给定区域,没有“过滤”/“忽略”区域)。但是,对于他们确实过滤掉重要部分的情况(例如:我想要北和南,但不想要东或西),您会如何考虑将标准移动到内部选择? (当然,我们仍然需要临时表来获得所需的计数)。
  • @Wrikken 。 . .如果您的维度是所有可用数据的子集,那么对内部选择进行过滤可以通过减少考虑的单元格数量来提高性能。同样,如果您确信对于任何给定维度而言,单元格中的行数不会超过 10%,则包含 where rand() &lt; XX 可以减少集合。
  • 如果百分比不准确怎么办。例如,如果拆分为 601 名男性和 399 名女性。那么你的计算就不会是整数了
  • @Danb 。 . .使用这种方法可能会与绝对计数有小的偏差。在进行分层抽样时总是如此,这正是您提到的原因。结果应尽可能接近所需比例,同时保持变量间的分布一致。
  • 问题是要求一个精确的解决方案。为此,必须求解一个线性方程组,正如我在stackoverflow.com/a/27468858/4350148 中所展示的那样。对于一个近似答案,人们可​​以简单地找到方程组的任何解,而不是基于整数的解。基于整数的解决方案更难,但仍然可行。
【解决方案3】:

您的请求存在的问题是有大量选项可用于实现建议的数字:

       Male    Female    Sum
-----------------------------
North:  100         0    100      
South:  200         0    200
East:   300         0    300 
West:     0       400    400 
Sum:    600       400
-----------------------------
North:   99         1    100      
South:  200         0    200
East:   300         0    300 
West:     1       399    400 
Sum:    600       400
-----------------------------
....
-----------------------------
North:    0       100    100      
South:  200         0    200
East:     0       300    300 
West:   400         0    400 
Sum:    600       400

只需结合北、东和西(南总是男性:200),您将获得 400 种实现建议数字的可能性。如果每个“class”(Male/North = “class”)的记录数量有限,情况会变得更加复杂。

对于上表中的每个单元格,您可能需要最多 MIN(COUNT(gender), COUNT(location)) 记录(如果对应的单元格为零)。

这取决于:

       Male    Female    
---------------------
North:  100       100      
South:  200       200
East:   300       300 
West:   400       400 

因此您需要计算每个性别/位置对AVAILABLE(gender, location) 的可用记录。

找到特别合适的似乎接近于半幻方[1][2]

math.stackexchange.com 上有几个关于这个[3][4] 的问题。

我最终阅读了 some paper 关于如何构建这些的内容,但我怀疑是否可以通过一次选择来做到这一点。

如果你有足够的记录并且不会出现这样的情况:

       Male    Female    
---------------------
North:  100         0      
South:  200       200
East:   300         0 
West:   200       200 

我会迭代波谷位置,并在每个步骤中添加成比例的男性/女性:

  1. 男:100 (16%); F:0 (0%)
  2. 男:100 (16%);女:200 (50%)
  3. 男:400 (66%);女:200 (50%)
  4. 男:600(100%); F:400 (100%)

但这只会为您提供近似结果,并且在验证这些结果之后,您可能希望迭代几次低谷结果并将每个类别中的计数调整为“足够好”。

【讨论】:

  • 看看下面我的解决方案。可以通过一次选择来完成,但是首先需要求解一组方程。这很令人惊讶,因为起初对我来说似乎不可能。
【解决方案4】:

我会构建一个数据库分布图并使用它来实现采样逻辑。奖励包括向用户添加快速人口统计反馈的可能性,并且不会给服务器带来额外负担。另一方面,您需要实现一种机制来保持数据库和地图同步。

使用 JSON 可能看起来像这样:

{"gender":{
  "Male":{
    "amount":35600,
    "region":{
      "North":{
        "amount":25000,
        "age":{
          "18":{
            "amount":2400,
            "ethnicity":{
              ...
              "income":{
                ...
              }
            },
            "income":{
              ...
              "ethnicity":{
                ...
              }
            }
          },
          "19":{
            ...
          },
          ...
          "120":{
            ...
          }
        },
        "ethnicity":{
          ...
        },
        "income":{
          ...
        }
      },
      "South":{
        ...
      },
      ...
    }
    "age":{
      ...
    }
    "ethnicity":{
      ...
    },
    "income":{
      ...
    }
  },
  "Female":{
    ...
  }
},
"region":{
  ...
},
"age":{
  ...
},
"ethnicity":{
  ...
},
"income":{
  ...
}}

所以用户选择了

total 1000
   600 Male
   400 Female

   100 North
   200 South
   300 East
   400 West

   300 <20 years old
   300 21-29 years old
   400 >=30 years old

计算线性分布:

male-north-u20: 1000*0.6*0.1*0.3=18
male-north-21to29: 18
male-north-o29: 24 (keep a track of rounding errors)
etc

然后我们会检查地图:

tmp.male.north.u20=getSumUnder(JSON.gender.Male.region.North.age,20) // == 10
tmp.male.north.f21to29=getSumBetween(JSON.gender.Male.region.North.age,21,29) // == 29
tmp.male.north.o29=getSumOver(JSON.gender.Male.region.north.age,29) // == 200
etc

将符合线性分布的所有内容标记为正常并跟踪剩余。如果某些东西(如male.north.u20)低于父级的第一次调整(以确保male.north例如符合标准),则u20会丢失8,而f21to29会过度使用8。首次运行后,调整其他地区的每个缺失标准。所以就像tmp.male.south.u20+=8;tmp.male.south.f21to29-=8;

把它做好是相当乏味的。

最后,你得到了正确的分布,可以用来构造一个简单的 SQL 查询。

【讨论】:

  • 不错的方法——但是,为您提供分布图的 SQL 查询怎么样?像这样的额外查询并非不合理......
  • 是的。不知何故,您需要查询分布。特别是如果表经常变化。我在想一个函数可以处理一大堆COUNT(userid)s。另一种方法是查询整个表格,并为每一行增加地图中受影响的元素,然后仅通过特殊功能添加/删除/修改,以便地图和表格同步更改。如果同步丢失,则需要重做。我不知道这对于用例是否可行。
【解决方案5】:

这可以分两步解决。对于以性别和地区为维度的示例,我将描述如何做到这一点。然后我将描述更一般的情况。在第一步中,我们求解一个包含 8 个变量的方程组,然后我们采用由第一步中找到的解限制的 8 个选择语句的不相交并集。请注意,任何行只有 8 种可能性。他们可以是男性或女性,然后该地区是北方,南方,东方或西方之一。现在让,

X1 equal the number of rows that are male and from the north, 
X2 equal the number of rows that are male and from the south,
X3 equal the number of rows that are male and from the east,
X4 equal then number that are male and from the west 
X5 equal the number of rows that are female and from the north, 
X6 equal the number of rows that are female and from the south,
X7 equal the number of rows that are female and from the east,
X8 equal then number that are female and from the west 

方程式是:

 X1+X2+X3+X4=600
 X5+X6+X7+X8=400
 X1+X5=100
 X2+X6=200
 X3+X7=300
 X4+X8=400

现在求解上面的 X1,X2,...X8。有很多解决方案(我会描述如何解决)这里有一个解决方案:

X1=60, X2=120, X3=180,X4=240,X5=40,X6=80,X7=120,X8=160.

现在我们可以通过 8 个选择的简单联合得到结果:

(select * from user where  gender='m' and region="north" limit 60)
union distinct(select * from user where  gender='m' and region='south' limit 120)
union distinct(select * from user where  gender='m' and region='east' limit 180)
union distinct(select * from user where  gender='m' and region='west' limit 240)
union distinct(select * from user where  gender='f' and region='north' limit 40)
union distinct(select * from user where  gender='f' and region='south' limit 80)
union distinct(select * from user where  gender='f' and region='east' limit 120)
union distinct(select * from user where  gender='f' and region='west' limit 160);

请注意,如果数据库中没有 60 行满足上面的第一个选择,那么给出的特定解决方案将不起作用。所以我们必须添加其他约束,LT:

0<X1 <= (select count(*) from user where  from user where  gender='m' and region="north")
0<X2 <= (select count(*) from user where  gender='m' and region='south')
0<X3 <= (select count(*) from user where  gender='m' and region='east' )
0<X4 <= (select count(*) from user where  gender='m' and region='west')
0<X5 <= (select count(*) from user where  gender='f' and region='north' )
0<X6 <= (select count(*) from user where  gender='f' and region='south')
0<X7 <= (select count(*) from user where  gender='f' and region='east' )
0<X8 <= (select count(*) from user where  gender='f' and region='west');

现在让我们概括一下这种允许任何拆分的情况。方程为 E:

 X1+X2+X3+X4=n1
 X5+X6+X7+X8=n2
 X1+X5=m1
 X2+X6=m2
 X3+X7=m3
 X4+X8=m4

Th 数 n1,n2,m1,m2,m3,m4 被给定并且满足 n1+n2=(m1+m2+m3+m4)。因此,我们将问题简化为求解上述方程 LT 和 E。这只是一个线性规划问题,可以使用单纯形法或其他方法解决。另一种可能性是将其视为线性丢番图方程组并使用方法来找到解决方案。无论如何,我已经将问题简化为找到上述方程的解。 (鉴于方程是一种特殊形式,可能有比使用单纯形法或求解线性丢番图方程组更快的方法)。一旦我们求解 Xi,最终的解是:

(select * from user where  gender='m' and region="north" limit :X1)
union distinct(select * from user where  gender='m' and region='south' limit :X2)
union distinct(select * from user where  gender='m' and region='east' limit :X3)
union distinct(select * from user where  gender='m' and region='west' limit :X4)
union distinct(select * from user where  gender='f' and region='north' limit :X5)
union distinct(select * from user where  gender='f' and region='south' limit :X6)
union distinct(select * from user where  gender='f' and region='east' limit :X7)
union distinct(select * from user where  gender='f' and region='west' limit :X8);

让我们将具有 n 个可能性的维度 D 表示为 D:n。假设您有 D1:n1、D2:n2、...DM:nM 维度。将生成 n1*n2*...nM 变量。生成的方程数为 n1+n2+...nM。而不是定义通用方法,让我们以另一种 3 维、4 维和 2 维的情况为例;让我们将 D1 的可能值称为 d11、d12、d13,D2 是 d21、d22、d23、d24,而 D3 的值是 d31、d32。我们将有 24 个变量,方程是:

 X1 + X2 + ...X8=n11
 X9 + X10 + ..X16=n12
 X17+X18 + ...X24=n13
 X1+X2+X9+x10+x17+x18=n21
 X3+X4+X11+x12+x19+x20=n22
 X5+X6+X13+x14+x21+x22=n23
 X7+X8+X15+x116+x23+x24=n24
 X1+X3+X5+...X23=n31
 X2+X4+......X24=n32

在哪里

X1 equals number with D1=d11  and  D2=d21 and D3=d31
X2 equals number with D1=d11 and D2=d21 and D3 = d31
....
X24 equals number with D1=D13 and D2=d24, and D3=d32.

添加小于限制。然后求解 X1,X2,... X24。创建 24 个选择语句并采用不相交的联合。 我们可以对任何维度进行类似的求解。

总而言之:给定维度 D1:n1, D2:n2, ...DM:nM 我们可以解决上述针对 n1*n2*...nM 变量的相应线性规划问题,然后通过以下方式生成解决方案对 n1*n2*...nM 选择语句进行不相交的联合。所以是的,我们可以通过 select 语句生成解决方案,但首先我们必须通过获取每个 n1*n2*...nM 变量的计数来求解方程并确定限制。

即使赏金已经结束,我也会为您感兴趣的人添加更多内容。我在这里声称,如果有解决方案,我已经完全展示了如何解决这个问题。

澄清我的方法。在 3 维的情况下,假设我们将年龄分成 3 种可能性之一。然后很好地使用问题中的性别和地区。每个用户有 24 种不同的可能性,对应于他们在这些类别中的位置。设 Xi 为最终结果中每种可能性的数量。让我写一个矩阵,其中每一行代表每种可能性中的一种。每个用户最多将贡献 1 到 m 或 f,1 到北、南、东或西,以及 1 到年龄类别。用户只有 24 种可能性。让我们展示一个矩阵:(abc) 3 个年龄,(nsew) 地区和 (mf) 男性或女性:a 为小于或等于 10 岁,b 为 11 至 30 岁,c 为 31 至 50 岁。

     abc nsew mf
X1   100 1000 10
X2   100 1000 01
X3   100 0100 10
X4   100 0100 01
X5   100 0010 10
X6   100 0010 01
X7   100 0001 10
X8   100 0001 01

X9   010 1000 10
X10  010 1000 01
X11  010 0100 10
X12  010 0100 01
X13  010 0010 10
X14  010 0010 01
X15  010 0001 10
X16  010 0001 01

X17   001 1000 10
X18   001 1000 01
X19   001 0100 10
X20   001 0100 01
X21   001 0010 10
X22   001 0010 01
X23   001 0001 10
X24   001 0001 01

每一行代表一个用户,如果它对结果有贡献,则列中有一个 1。例如,第一行显示 1 代表 a,1 代表 n,1 代表 m。这意味着用户的年龄小于或等于10,来自北方并且是男性。 Xi 表示最终结果中有多少这样的行。因此,假设 X1 为 10,这意味着我们说最终结果有 10 个结果,所有这些结果都来自北方,都是男性,小于或等于 10。好吧,现在我们只需要将它们相加。请注意,前 8 个X1+X2+X3+X4+X5+X6+X7+X8 是年龄小于或等于 10 的所有行。它们必须加起来就是我们为该类别选择的任何内容。接下来的 2 组 8 组也是如此。

到目前为止,我们得到了方程式:(na 是年龄小于 10 的数字,nb 是 10 到 20 之间的年龄,nc 是年龄小于 50 的数字

X1+X2+X3+X4+X5+X6+X7+X8 =  na
X9+X10+X11 + .... X16 = nb
X17+X18+X19+...           X24=nc

这些是年龄划分。现在让我们看看区域拆分。只需将“n”列中的变量相加,

X1+X2+X9+X10+X17+X18 = nn
X3+X4+X11+X12+X19+20=ns
...

等等。 你知道我是如何通过向下看列来得到这些方程的吗? 继续 ew 和 mf。总共给出 3+4+2 个方程。所以我在这里做的很简单。我推断您选择的任何行都会对 3 个维度中的每个维度贡献一个,并且只有 24 种可能性。然后让 Xi 成为每种可能性的数字,你就得到了需要求解的方程。在我看来,无论你想出什么方法,都必须是这些方程的解。换句话说,我只是根据求解这些方程重新表述了问题。

现在我们需要一个整数解,因为我们不能有小数行。请注意,这些都是线性方程。但我们想要一个整数解。以下是描述如何解决这些问题的论文的链接:https://www.math.uwaterloo.ca/~wgilbert/Research/GilbertPathria.pdf

【讨论】:

  • 有趣的是,您将此视为丢番图方程的解。我认为这只是问题的一半。另一半是基于原始数据中值分布的每组大小的约束。具有讽刺意味的是,这将问题变成了“丢番图”线性规划问题。我不确定是否解决了线性规划问题的整数解,但连续近似可能足够接近。
  • @GordonLinoff 我认为我已经给出了问题的精确表述。如果您解决这些方程式,那么您将能够生成正确的 sql。同样,如果您生成一个解决方案,那么您将找到方程的解决方案。唯一真正的限制是上述计数的限制。也就是说,每个 Xi 的解决方案必须少于满足与 Xi 对应的查询的行数。但是一旦你解决了丢番图方程,你就可以很容易地选择一个满足这些约束的方程。你同意吗?
  • 确实很有意思,但是有两个问题。 (1) 解不保证是整数。把它变成整数会引入一组新的问题。 (2) 由于不可能的约束,方程可能无法解。
  • 如果方程没有解,那么就没有办法生成sql来生成答案。换句话说,如果有答案(而且很可能会有),那么方程组就会有答案。或者换一种说法,如果您生成一组满足约束的用户,那么这些方程将有一个解决方案。如果您只是在寻找一个近似的解决方案,那么这会更容易。只需求解线性方程,您就会得到一个接近的匹配。
  • (1) 假设Male, Northen, 15岁以下的近似解是198.6,怎么查询?限198?还是限制 199?如果向上或向下舍入,则需要修正其他标准数量。修正您的其他标准是另一组问题,因为如果您向上或向下取整,可能会破坏您的其他约束。
【解决方案6】:

在 SQL 中形成业务逻辑绝不是一个好主意,因为它会妨碍吸收即使是很小的变化的能力。

我的建议是在 ORM 中执行此操作,并将业务逻辑从 SQL 中抽象出来。

例如,如果您使用的是 Django

您的模型如下所示:

class User(models.Model):
    GENDER_CHOICES = (
      ('M', 'Male'),
      ('F','Female')
    )       
    gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
    REGION_CHOICES = (
      ('E', 'East'),
      ('W','West'),
      ('N','North'),
      ('S','South')
    )
    region = models.CharField(max_length=1, choices=REGION_CHOICES)
    age = models.IntegerField()
    ETHNICITY_CHOICES = (
      .......
    ) 
    ethnicity = models.CharField(max_length=1, choices=ETHNICITY_CHOICES)
    income = models.FloatField()

您的查询功能可能是这样的:

# gender_limits is a dict like {'M':400, 'F':600}
# region_limits is a dict like {'N':100, 'E':200, 'W':300, 'S':400}
def get_users_by_gender_and_region(gender_limits,region_limits):
    for gender in gender_limits:
        gender_queryset = gender_queryset | User.objects.filter(gender=gender)[:gender_limits[gender]]
    for region in region_limits:
        region_queryset = region_queryset | User.objects.filter(region=region)[:region_limits[region]]
    return gender_queryset & region_queryset

查询功能可以通过您计划支持的所有查询的知识进一步抽象,但这应该作为示例。

如果您使用不同的 ORM,同样的想法也可以转化为任何好的 ORM 都具有联合和交集抽象。

【讨论】:

  • 无需安装Django,这会导致哪些查询?
  • 抱歉,我没完全明白你的问题。能解释一下吗?
  • get_users_by_gender_and_region 函数完全符合您的伪 mySQL 代码的用途,如果这就是您要问的。
【解决方案7】:

我会使用编程语言来生成 SQL 语句,但下面是纯 mySQL 的解决方案。做了一个假设:一个地区总是有足够的男性/女性来拟合这些数字(例如,如果北方没有女性生活怎么办?)。

例程正在预先计算所需的行数量。不能使用变量指定限制。我更像是一个预言家,我们有分析功能。 MySQL 还通过允许变量在一定程度上提供了这一点。所以我设置了目标区域和性别并计算了细分。然后我使用计算限制我的输出。

此查询显示计数以证明该概念。

set @male=600;
set @female=400;
set @north=100;
set @south=200;
set @east=300;
set @west=400;
set @north_male=@north*(@male/(@male+@female));
set @south_male=@south*(@male/(@male+@female));
set @east_male =@east *(@male/(@male+@female));
set @west_male =@west *(@male/(@male+@female));
set @north_female=@north*(@female/(@male+@female));
set @south_female=@south*(@female/(@male+@female));
set @east_female =@east *(@female/(@male+@female));
set @west_female =@west *(@female/(@male+@female));

select gender, region, count(*) 
from (
          select * from (select @north_male  :=@north_male-1   as row, userid, gender, region from users where gender = 'Male' and region = 'North' ) mn where row>=0 
union all select * from (select @south_male  :=@south_male-1   as row, userid, gender, region from users where gender = 'Male' and region = 'South' ) ms where row>=0
union all select * from (select @east_male   :=@east_male-1    as row, userid, gender, region from users where gender = 'Male' and region = 'East'  ) me where row>=0
union all select * from (select @west_male   :=@west_male-1    as row, userid, gender, region from users where gender = 'Male' and region = 'West'  ) mw where row>=0
union all select * from (select @north_female:=@north_female-1 as row, userid, gender, region from users where gender = 'Female' and region = 'North' ) fn where row>=0 
union all select * from (select @south_female:=@south_female-1 as row, userid, gender, region from users where gender = 'Female' and region = 'South' ) fs where row>=0
union all select * from (select @east_female :=@east_female-1  as row, userid, gender, region from users where gender = 'Female' and region = 'East'  ) fe where row>=0
union all select * from (select @west_female :=@west_female-1  as row, userid, gender, region from users where gender = 'Female' and region = 'West'  ) fw where row>=0
) a
group by gender, region
order by gender, region;

输出:

Female  East   120
Female  North   40
Female  South   80
Female  West   160
Male    East   180
Male    North   60
Male    South  120
Male    West   240

去掉外面的部分得到真实的记录:

set @male=600;
set @female=400;
set @north=100;
set @south=200;
set @east=300;
set @west=400;
set @north_male=@north*(@male/(@male+@female));
set @south_male=@south*(@male/(@male+@female));
set @east_male =@east *(@male/(@male+@female));
set @west_male =@west *(@male/(@male+@female));
set @north_female=@north*(@female/(@male+@female));
set @south_female=@south*(@female/(@male+@female));
set @east_female =@east *(@female/(@male+@female));
set @west_female =@west *(@female/(@male+@female));
          select * from (select @north_male  :=@north_male-1   as row, userid, gender, region from users where gender = 'Male' and region = 'North' ) mn where row>=0 
union all select * from (select @south_male  :=@south_male-1   as row, userid, gender, region from users where gender = 'Male' and region = 'South' ) ms where row>=0
union all select * from (select @east_male   :=@east_male-1    as row, userid, gender, region from users where gender = 'Male' and region = 'East'  ) me where row>=0
union all select * from (select @west_male   :=@west_male-1    as row, userid, gender, region from users where gender = 'Male' and region = 'West'  ) mw where row>=0
union all select * from (select @north_female:=@north_female-1 as row, userid, gender, region from users where gender = 'Female' and region = 'North' ) fn where row>=0 
union all select * from (select @south_female:=@south_female-1 as row, userid, gender, region from users where gender = 'Female' and region = 'South' ) fs where row>=0
union all select * from (select @east_female :=@east_female-1  as row, userid, gender, region from users where gender = 'Female' and region = 'East'  ) fe where row>=0
union all select * from (select @west_female :=@west_female-1  as row, userid, gender, region from users where gender = 'Female' and region = 'West'  ) fw where row>=0
;

为了测试,我编写了一个程序,它确实创建了 10000 个完全随机的样本记录:

use test;
drop table if exists users;
create table users (userid int not null auto_increment, gender VARCHAR (20), region varchar(20), primary key (userid) );
drop procedure if exists load_users_table;
delimiter #
create procedure load_users_table()
begin
    declare l_max int unsigned default 10000;
    declare l_cnt int unsigned default 0;
    declare l_gender varchar(20);
    declare l_region varchar(20);
    declare l_rnd smallint;
    truncate table users;
    start transaction;
    WHILE l_cnt < l_max DO
        set l_rnd = floor( 0 + (rand()*2) );
        if l_rnd = 0 then
            set l_gender = 'Male';
        else
            set l_gender = 'Female';
        end if;
        set l_rnd=floor(0+(rand()*4));
        if l_rnd = 0 then
            set l_region = 'North';
        elseif l_rnd=1 then
            set l_region = 'South';
        elseif l_rnd=2 then
            set l_region = 'East';
        elseif l_rnd=3 then
            set l_region = 'West';
        end if;
        insert into users (gender, region) values (l_gender, l_region);
        set l_cnt=l_cnt+1;
    end while;
    commit;
end #
delimiter ;
call load_users_table();

select gender, region, count(*) 
from users
group by gender, region
order by gender, region;

希望这一切对您有所帮助。底线是:使用UNION ALL 并限制使用预先计算的变量而不是LIMIT

【讨论】:

    【解决方案8】:

    嗯,我认为问题在于随机获取记录,而不是所有地区的 60/40 比例。我已经为地区和性别做了。它可以以同样的方式推广到年龄、收入和种族等其他领域。

        Declare @Mlimit bigint
        Declare @Flimit bigint
        Declare @Northlimit bigint
        Declare @Southlimit bigint 
        Declare @Eastlimit bigint
        Declare @Westlimit bigint  
    
        Set @Mlimit= 600
        Set @Flimit=400
        Set @Northlimit= 100
        Set @Southlimit=200
        Set @Eastlimit=300
        Set @Westlimit=400
    
        CREATE TABLE #Users(
            [UserId] [int]  NOT NULL,
            [gender] [varchar](10) NULL,
            [region] [varchar](10) NULL,
            [age] [int] NULL,
            [ethnicity] [varchar](50) NULL,
            [income] [bigint] NULL
    
        )
          Declare @MnorthCnt bigint
          Declare @MsouthCnt bigint
          Declare @MeastCnt bigint
          Declare @MwestCnt bigint
    
           Declare @FnorthCnt bigint
          Declare @FsouthCnt bigint
          Declare @FeastCnt bigint
          Declare @FwestCnt bigint
    
          Select @MnorthCnt=COUNT(*) from users where gender='male' and region='north' 
          Select @FnorthCnt=COUNT(*) from users where gender='female' and region='north' 
    
          Select @MsouthCnt=COUNT(*) from users where gender='male' and region='south' 
          Select @FsouthCnt=COUNT(*) from users where gender='female' and region='south' 
    
          Select @MeastCnt=COUNT(*) from users where gender='male' and region='east' 
          Select @FeastCnt=COUNT(*) from users where gender='female' and region='east' 
          Select @MwestCnt=COUNT(*) from users where gender='male' and region='west' 
          Select @FwestCnt=COUNT(*) from users where gender='female' and region='west' 
    
        If (@Northlimit=@MnorthCnt+@FnorthCnt)
        begin
         Insert into #Users select * from Users where region='north' 
        set @Northlimit=0
        set @Mlimit-=@MnorthCnt
        set @Flimit-=@FnorthCnt
        set @MnorthCnt=0 
        set @FnorthCnt=0
        end
    
        If (@Southlimit=@MSouthCnt+@FSouthCnt)
        begin
         Insert into #Users select * from Users where region='South' 
        set @Southlimit=0
        set @Mlimit-=@MSouthCnt
        set @Flimit-=@FSouthCnt
        set @MsouthCnt=0
        set @FsouthCnt=0
        end
    
        If (@Eastlimit=@MEastCnt+@FEastCnt)
        begin
         Insert into #Users select * from Users where region='East' 
        set @Eastlimit=0
        set @Mlimit-=@MEastCnt
        set @Flimit-=@FEastCnt
        set @MeastCnt=0
        set @FeastCnt=0
        end
    
        If (@Westlimit=@MWestCnt+@FWestCnt)
        begin
         Insert into #Users select * from Users where region='West' 
        set @Westlimit=0
        set @Mlimit-=@MWestCnt
        set @Flimit-=@FWestCnt
        set @MwestCnt=0
        set @FwestCnt=0
        end 
    
    If @MnorthCnt<@Northlimit
     Begin
     insert into #Users select top (@Northlimit-@MnorthCnt) * from Users where gender='female' and region='north'
     and userid not in (select userid from #users)
     set @Flimit-=(@Northlimit-@MnorthCnt)
     set @FNorthCnt-=(@Northlimit-@MnorthCnt)
     set @Northlimit-=(@Northlimit-@MnorthCnt)
     End
    
     If @FnorthCnt<@Northlimit
     Begin
     insert into #Users select top (@Northlimit-@FnorthCnt) * from Users where gender='male' and region='north'
     and userid not in (select userid from #users)
     set @Mlimit-=(@Northlimit-@FnorthCnt)
     set @MNorthCnt-=(@Northlimit-@FnorthCnt)
     set @Northlimit-=(@Northlimit-@FnorthCnt)
     End
    
     if @MsouthCnt<@southlimit
     Begin
     insert into #Users select top (@southlimit-@MsouthCnt) * from Users where gender='female' and region='south'
     and userid not in (select userid from #users)
     set @Flimit-=(@southlimit-@MsouthCnt)
     set @FSouthCnt-=(@southlimit-@MsouthCnt)
     set @southlimit-=(@southlimit-@MsouthCnt)
     End
    
     if @FsouthCnt<@southlimit
     Begin
     insert into #Users select top (@southlimit-@FsouthCnt) * from Users where gender='male' and region='south'
     and userid not in (select userid from #users)
     set @Mlimit-=(@southlimit-@FsouthCnt)
     set @MSouthCnt-=(@southlimit-@FsouthCnt)
     set @southlimit-=(@southlimit-@FsouthCnt)
     End
    
    if @MeastCnt<@eastlimit
     Begin
     insert into #Users select top (@eastlimit-@MeastCnt) * from Users where gender='female' and region='east'
     and userid not in (select userid from #users)
     set @Flimit-=(@eastlimit-@MeastCnt)
     set @FEastCnt-=(@eastlimit-@MeastCnt)
     set @eastlimit-=(@eastlimit-@MeastCnt)
     End
    
    if @FeastCnt<@eastlimit
     Begin
     insert into #Users select top (@eastlimit-@FeastCnt) * from Users where gender='male' and region='east'
     and userid not in (select userid from #users)
     set @Mlimit-=(@eastlimit-@FeastCnt)
     set @MEastCnt-=(@eastlimit-@FeastCnt)
     set @eastlimit-=(@eastlimit-@FeastCnt)
    End
    
    if @MwestCnt<@westlimit
     Begin
     insert into #Users select top (@westlimit-@MwestCnt) * from Users where gender='female' and region='west'
     and userid not in (select userid from #users)
     set @Flimit-=(@westlimit-@MwestCnt)
     set @FWestCnt-=(@westlimit-@MwestCnt)
     set @westlimit-=(@westlimit-@MwestCnt)
     End
    
    if @FwestCnt<@westlimit
     Begin
     insert into #Users select top (@westlimit-@FwestCnt) * from Users where gender='male' and region='west'
     and userid not in (select userid from #users)
     set @Mlimit-=(@westlimit-@FwestCnt)
     set @MWestCnt-=(@westlimit-@FwestCnt)
     set @westlimit-=(@westlimit-@FwestCnt)
     End     
    
    
        IF (@MnorthCnt>=@Northlimit and @FnorthCnt>=@Northlimit and @MsouthCnt>=@southlimit and @FsouthCnt>=@southlimit and @MeastCnt>=@eastlimit and @FeastCnt>=@eastlimit and @MwestCnt>=@westlimit and @FwestCnt>=@westlimit and not(@Mlimit=0 and @Flimit=0))
        Begin
    
        ---Create Cursor
        DECLARE UC CURSOR FAST_forward
        FOR
        SELECT *
        FROM Users
        where userid not in (select userid from #users) 
    
        Declare @UserId [int]  ,
            @gender [varchar](10) ,
            @region [varchar](10) ,
            @age [int] ,
            @ethnicity [varchar](50) ,
            @income [bigint]   
        OPEN UC
    
        FETCH NEXT FROM UC
        INTO @UserId ,@gender, @region, @age, @ethnicity, @income
    
        WHILE @@FETCH_STATUS = 0 and not (@Mlimit=0 and @Flimit=0) 
        BEGIN
        If @gender='male' and @region='north' and @Northlimit>0 AND @Mlimit>0
        begin
        insert into #Users values (@UserId ,@gender, @region, @age, @ethnicity, @income)
        set @Mlimit-=1
        set @MNorthCnt-=1
        set @Northlimit-=1
        end  
        If @gender='male' and @region='south' and @southlimit>0 AND @Mlimit>0
        begin
        insert into #Users values (@UserId ,@gender, @region, @age, @ethnicity, @income)
        set @Mlimit-=1
        set @MsouthCnt-=1
        set @Southlimit-=1
        end 
        If @gender='male' and @region='east' and @eastlimit>0 AND @Mlimit>0
        begin
        insert into #Users values (@UserId ,@gender, @region, @age, @ethnicity, @income)
        set @Mlimit-=1
        set @MeastCnt-=1
        set @eastlimit-=1
        end  
        If @gender='male' and @region='west' and @westlimit>0 AND @Mlimit>0
        begin
        insert into #Users values (@UserId ,@gender, @region, @age, @ethnicity, @income)
        set @Mlimit-=1
        set @MwestCnt-=1
        set @westlimit-=1
        end 
    
        If @gender='female' and @region='north' and @Northlimit>0 AND @flimit>0
        begin
        insert into #Users values (@UserId ,@gender, @region, @age, @ethnicity, @income)
        set @Flimit-=1
        set @FNorthCnt-=1
        set @Northlimit-=1
        end  
        If @gender='female' and @region='south' and @southlimit>0 AND @flimit>0
        begin
        insert into #Users values (@UserId ,@gender, @region, @age, @ethnicity, @income)
        set @Flimit-=1
        set @FsouthCnt-=1
        set @Southlimit-=1
        end 
        If @gender='female' and @region='east' and @eastlimit>0 AND @flimit>0
        begin
        insert into #Users values (@UserId ,@gender, @region, @age, @ethnicity, @income)
        set @flimit-=1
        set @feastCnt-=1
        set @eastlimit-=1
        end  
        If @gender='female' and @region='west' and @westlimit>0 AND @flimit>0
        begin
        insert into #Users values (@UserId ,@gender, @region, @age, @ethnicity, @income)
        set @flimit-=1
        set @fwestCnt-=1
        set @westlimit-=1
        end   
        FETCH NEXT FROM UC
        INTO @UserId ,@gender, @region, @age, @ethnicity, @income
        END
    
        CLOSE UC
    
        DEALLOCATE UC
    
        end
    
        Select * from #Users
    
        SELECT GENDER, REGION, COUNT(*) AS COUNT FROM #USERS 
        GROUP BY GENDER, REGION
        DROP TABLE #Users
    

    【讨论】:

      【解决方案9】:

      我希望您希望根据所需的过滤器生成一堆查询。

      我将通过完整的代码示例解释一种可能的方法 - 但稍后请注意注意事项。
      我还将解决您无法从比例分布中满足要求的样本的问题,但您可以从调整后的分布中满足 - 并解释如何进行该调整

      基本算法是这样的:

      从一组过滤器{F1, F2, ... Fn} 开始,每个过滤器都有一组值,以及应该在这些值之间分布的百分比。例如,F1 可能是性别,有 2 个值(F1V1 = 男性:60%,F1V2 = 女性:40%)您还需要所需的总样本量(称为X)从这个起点您可以结合所有过滤器项目从每个过滤器中获取单个集合的所有组合过滤器项目,以及每个过滤器所需的数量。 代码应该能够处理任意数量的过滤器,具有任意数量的值(精确值或范围)

      EG:假设有 2 个过滤器,F1:性别,{F1V1 = 男性:60%,F1V2 = 女性:40%},F2:地区,{F2V1 = 北方:50%,F2V2 = 南方:50%} 和X = 10 人所需的总样本。
      在这个样本中,我们希望其中 6 个是男性,其中 4 个是女性,5 个来自北方,5 个来自南方。

      然后我们做

      1. 为 F1 中的每个值创建一个 sql 存根 - 与初始百分比的相关分数(即
        • WHERE gender = 'Male':0.6,
        • WHERE gender = 'Female': 0.4)
      2. 对于 F2 中的每个项目 - 从上述步骤中的每个项目创建一个新的 sql 存根 - 过滤器现在是 F1 值和 F2 值,相关的分数是 2 个分数的乘积。所以我们现在有 2 x 2 = 4 项
        • WHERE gender = 'Male' AND region = 'North':0.6 * 0.5 = 0.3,
        • WHERE gender = 'Female' AND region = 'North':0.4 * 0.5 = 0.2,
        • WHERE gender = 'Male' AND region = 'South':0.6*0.5 = 0.3,
        • WHERE gender = 'Female' AND region = 'South': 0.4*0.5 = 0.2
      3. 对每个额外的过滤器 F3 到 Fn 重复上述步骤 2。 (在我们的示例中,只有 2 个过滤器,所以我们已经完成了)
      4. 将每个 SQL 存根的限制计算为 [与存根关联的分数] * X = 所需的总样本大小(因此对于我们的示例,男性/北方为 0.3 * 10 = 3,女性/北方为 0.2 * 10 = 2等)
      5. 最后对于每一个sql stub - 把它变成一个完整的SQL语句,并加上限制

      代码示例

      我将为此提供 C# 代码,但将其翻译成其他语言应该很容易。 在纯动态 SQL 中尝试这样做会非常棘手

      请注意,这是未经测试的 - 可能充满错误 - 但它是您可以采取的方法的一个想法。

      我已经定义了一个公共方法和一个公共类 - 这将是入口点。

      // This is an example of a public class you could use to hold one of your filters
      // For example - if you wanted 60% male / 40% female, you could have an item with 
      //    item1 = {Fraction: 0.6, ValueExact: 'Male', RangeStart: null, RangeEnd: null}
      //  & item2 = {Fraction: 0.4, ValueExact: 'Female', RangeStart: null, RangeEnd: null}
      public class FilterItem{
          public decimal Fraction {get; set;}
          public string ValueExact {get; set;}
          public int? RangeStart {get; set;}
          public int? RangeEnd {get; set;}
      }
      
      // This is an example of a public method you could call to build your SQL 
      // - passing in a generic list of desired filter
      // for example the dictionary entry for the above filter would be 
      // {Key: "gender", Value: new List<FilterItem>(){item1, item2}}
      public string BuildSQL(Dictionary<string, List<FilterItem>> filters, int TotalItems)
      {
          // we want to build up a list of SQL stubs that can be unioned together.
          var sqlStubItems = new List<SqlItem>();
          foreach(var entry in filters)
          {
              AddFilter(entry.Key, entry.Value, sqlStubItems);
          }
          // ok - now just combine all of the sql stubs into one big union.
          var result = ""; // Id use a stringbuilder for this normally, 
                           // but this is probably more cross-language readable.
          int limitSum = 0;
          for(int i = 0; i < sqlStubItems.Count; i++) // string.Join() would be more succinct!
          {
             var item = sqlStubItems[i];
             if (i > 0)
             {
                 result  += " UNION ";
             }
             int limit = (int)Math.Round(TotalItems * item.Fraction, 0);
             limitSum+= limit;
             if (i == sqlStubItems.Count - 1 && limitSum != TotalItems)
             {
                //may need to adjust one of the rounded items to account 
                //for rounding errors making a total that is not the 
                //originally required total limit.
                limit += (TotalItems - limitSum);
             }
             result +=  item.Sql + " LIMIT " 
                    + Convert.ToString(limit);
      
          }
          return result;
      }
      
      // This method expands the number of SQL stubs for every filter that has been added.
      // each existing filter is split by the number of items in the newly added filter.
      private void AddFilter(string filterType, 
                             List<FilterItem> filterValues, 
                             List<SqlItem> SqlItems)
      {
         var newItems = new List<SqlItem>();
      
         foreach(var filterItem in filterValues)
         {
             string filterAddon; 
             if (filterItem.RangeStart.HasValue && filterItem.RangeEnd.HasValue){
                 filterAddon = filterType + " >= " + filterItem.RangeStart.ToString() 
                             + " AND " + filterType + " <= " + filterItem.RangeEnd.ToString();
             } else {
                 filterAddon = filterType + " = '" 
                               + filterItem.ValueExact.Replace("'","''") + "'"; 
                               //beware of SQL injection. (hence the .Replace() above)
             }
             if(SqlItems.Count() == 0)
             {
                 newItems.Add(new SqlItem(){Sql = "Select * FROM users WHERE " 
                                            + filterAddon, Fraction = filterItem.Fraction});
             } else {
                 foreach(var existingItem in SqlItems)
                 {
                     newItems.Add(new SqlItem()
                     {
                       Sql = existingItem +  " AND " + filterAddon, 
                       Fraction = existingItem.Fraction * filterItem.Fraction
                     });
                 }
             }
         }
         SqlItems.Clear();
         SqlItems.AddRange(newItems);
      }
      
      
      
      // this class is for part-built SQL strings, with the fraction
      private class SqlItem{
        public string Sql { get; set;}
        public decimal Fraction{get; set;}
      }
      

      备注(根据 Sign 的评论)

      • 四舍五入错误可能意味着您在应用大量过滤器时没有得到您想要的 600 / 400 分割 - 但应该接近。
      • 如果您的数据集不是很多样化,则可能无法始终生成所需的拆分。这种方法需要过滤器之间的均匀分布(所以如果你总共有 10 人,6 男,4 女,5 来自北方,5 来自南方,则需要 3 来自北方的男性,3 来自北方的男性南,北2女,南2女。)
      • 不会随机检索人员 - 无论默认排序是什么。您需要添加 ORDER BY RAND() 之类的内容(但不是因为它非常低效)才能获得随机选择。
      • 小心 SQL 注入。清理所有用户输入,替换单引号 ' 字符。

      样本分布不均问题

      您如何解决根据代表性拆分(上述算法给出的)在我们的一个桶中没有足够的项目来创建我们的样本的问题?或者如果你的数字不是整数怎么办?

      好吧,我不会提供代码,但我会描述一种可能的方法。您需要对上面的代码进行相当多的更改,因为 sql 存根的平面列表不会再削减它了。相反,您需要构建一个 SQL 存根的 n 维矩阵(为每个过滤器 F1 - n 添加一个维度)在完成上述步骤 4 之后(我们有我们想要的,但不一定是每个 SQL 存根项的可能数字),我期望做的是

      1. 生成 SQL 以选择所有组合 sql WHERE 存根的计数。
      2. 然后您将迭代集合 - 如果您遇到请求限制高于计数(或不是整数)的项目,
        • 将请求的限制向下调整为计数(或最接近的整数)。
        • 然后在每个轴上选择另一个至少低于其最大计数的上述调整的项目,并将其向上调整相同。如果无法找到符合条件的项目,则您请求的拆分是不可能的。
        • 然后将向上调整的所有相交项再次向下调整
        • 对于 n 的每个附加维度,对交叉点之间的交叉点重复上述步骤(但每次都在负数和正数之间切换调整)

      所以假设继续我们之前的示例 - 我们的代表性拆分是:
      男/北 = 3,女/北 = 2,男/南 = 3,女/南 = 2,但北边只有 2 个男(但我们可以选择其他组的人很多)

      • 我们将 Male/North 调整为 2 (-1)
      • 我们将女性/北方调整为 3 (+1),男性/南方调整为 4 (+1)
      • 我们将相交母/南调整为 1 (-1)。瞧! (没有额外的维度,因为我们只有 2 个标准/维度)

      在调整更高维度的相交项时,此图可能会有所帮助(仅显示最多 4 个维度,但应该有助于了解需要做什么!每个点代表我们在 n 维矩阵中的一个 SQL 存根项(并且有一个相关的限制数字)一条线代表一个共同的标准值(例如性别=男性)。目标是调整完成后沿着任何一条线的总数应该保持不变!我们从红点开始,继续每个额外的维度......在上面的例子中,我们只看 2 个维度 - 一个由红点形成的正方形,它上方和右侧的 2 个橙色点,以及 NE 的 1 个绿色点来完成正方形。

      【讨论】:

      • 这是我一直在考虑的思路,但是你留下了2个比较大的空洞。 1) 分数可能会导致舍入误差。 2)可能有一个桶不允许这种分布
      • 是的 - 两者都是真的。我在这里并不追求完美,只是为了演示一种可能的编程方法
      • 这是一个非常酷的方法。我不确定我是否完全了解所有细节。你是说你已经给出了一种可以解决任意数量维度的方法吗?如果您查看stackoverflow.com/a/27468858/4350148,我将准确地展示如何生成一组方程,这些方程在求解时可以完全回答问题。如果你有办法解决它,那么你也给出了解决特定 Diaphontine 方程组的方法,所以我想更仔细地看看你的解决方案。
      • @danb 是的 - 这种方法应该适用于任意数量的维度(显然也适用于标准拆分中的 2 个以上的项目)我同意它归结为一组线性方程 - 我读过你的回答,听起来是对的——只是其中一些我很难想象+还有我需要做的额外阅读。这种(纯粹的迭代)方法对我来说很有意义,并且不会那么难变成代码。我可能错过了一些东西!
      • 主要是我展示了如何建立方程。知道如何求解可能需要一些额外的阅读。基本上我们需要线性方程的整数解。我可以详细说明如何建立如果有兴趣,可以使用更多维度的方程。要点是问题等同于求解这些方程。如果您认为您的方法可以解决我感兴趣的任意数量的维度,因为这意味着您已经弄清楚了如何解决那里的方程。已经有这方面的方法,我想看看你的方法与那些方法相比如何。
      【解决方案10】:

      我会选择GROUP BY

      SELECT gender,region,count(*) FROM users GROUP BY gender,region

      +----------------------+
      |gender|region|count(*)|
      +----------------------+
      |f     |E     |     129|
      |f     |N     |      43|
      |f     |S     |      84|
      |f     |W     |     144|
      |m     |E     |     171|
      |m     |N     |      57|
      |m     |S     |     116|
      |m     |W     |     256|
      +----------------------+
      

      您可以验证您有 600 名男性、400 名女性、100 名北部、200 名南部、300 名东部和 400 名西部。

      您也可以包含其他字段。

      对于年龄和收入等范围字段,您可以按照以下示例进行操作:

      SELECT
        gender,
        region,
        case when age < 30 then 'Young'
             when age between 30 and 59 then 'Middle aged'
             else 'Old' end as age_range,
        count(*)
      FROM users
      GROUP BY gender,region, age_range
      

      所以,结果会是这样的:

      +----------------------------------+
      |gender|region|age        |count(*)|
      +----------------------------------+
      |f     |E     |Middle aged|      56|
      |f     |E     |Old        |      31|
      |f     |E     |Young      |      42|
      |f     |N     |Middle aged|      14|
      |f     |N     |Old        |      11|
      |f     |N     |Young      |      18|
      |f     |S     |Middle aged|      40|
      |f     |S     |Old        |      23|
      |f     |S     |Young      |      21|
      |f     |W     |Middle aged|      67|
      |f     |W     |Old        |      42|
      |f     |W     |Young      |      35|
      |m     |E     |Middle aged|      77|
      |m     |E     |Old        |      56|
      |m     |E     |Young      |      38|
      |m     |N     |Middle aged|      13|
      |m     |N     |Old        |      25|
      |m     |N     |Young      |      19|
      |m     |S     |Middle aged|      46|
      |m     |S     |Old        |      39|
      |m     |S     |Young      |      31|
      |m     |W     |Middle aged|     103|
      |m     |W     |Old        |      66|
      |m     |W     |Young      |      87|
      +----------------------------------+
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-02-01
        • 1970-01-01
        • 2015-02-21
        • 1970-01-01
        相关资源
        最近更新 更多