Запрос MySQL для получения "пересечения" многочисленных запросов с ограничениями

Предположим, что у меня есть одна таблица mySQL (пользователи) со следующими полями:

userid  
gender  
region  
age  
ethnicity  
income

Я хочу иметь возможность возвращать число полных записей на основе числа, которое вводит пользователь. Кроме того, они также будут предоставлять дополнительные критерии.

В простейшем примере они могут запросить 1000 записей, в которых 600 записей должны иметь пол = "Мужской" и 400 записей, где gender = "Female". Это достаточно просто сделать.

Теперь сделайте еще один шаг. Предположим, что теперь они хотят указать Region:

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.

Я не уверен, что это возможно для записи в одном запросе или для этого требуется несколько операторов и итераций.

Ответ 1

Сгладить ваши критерии


Вы можете сгладить ваши многомерные критерии в критерии одного уровня

enter image description here

Теперь эти критерии могут быть достигнуты в одном запросе, как следует

(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 пользователей, которые являются мужчинами и с севера, тогда запрос вернет менее 1000 записей.

Отрегулируйте критерии


Скажем, что существует менее 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 northener.

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  }

"Мужской" + "Юг" является первым условием, которое соответствует условию настройки "Мужской" . Увеличьте его на +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 }

Я еще не придумал точной реализации настройки. Это сложнее, чем я ожидал. Я обновлю, как только смогу понять, как его реализовать.

Ответ 2

Проблема, которую вы описываете, представляет собой проблему многомерного моделирования. В частности, вы пытаетесь получить стратифицированный образец по нескольким измерениям одновременно. Ключом к этому является спуск до наименьшего уровня детализации и накопление образца оттуда.

Я также предполагаю, что вы хотите, чтобы образец был репрезентативным на всех уровнях. То есть вы не хотите, чтобы все пользователи из "Северного" были женщинами. Или все "мужчины" должны быть от "Запада", даже если это соответствует конечным критериям.

Начните с мышления с точки зрения общего количества записей, измерений и распределений вдоль каждого измерения. Например, для первого образца подумайте об этом как:

  • 1000 записей
  • 2 измерения: пол, область
  • гендерный раскол: 60%, 40%
  • разделение региона: 10%, 20%, 30%, 40%

Затем вы хотите выделить эти числа для каждой комбинации пола/региона. Цифры:

  • Север, Мужской: 60
  • Север, Женский: 40
  • Юг, Мужчина: 120
  • Юг, Женщина: 80
  • Восток, Мужчина: 180
  • Восток, Женщина: 120
  • Запад, Мужчина: 240
  • Запад, Женский: 160

Вы увидите, что они складываются по размеру.

Расчет чисел в каждой ячейке довольно прост. Это показатель процента от общего количества. Итак, "Восток, женщина" составляет 30% * 40% * 1000., Вуаля! Значение равно 120.

Вот решение:

  • Возьмите входные данные по каждому измерению в процентах от общего числа. И убедитесь, что они добавляют до 100% по каждому измерению.
  • Создайте таблицу ожидаемых процентов для каждой из ячеек. Это произведение процентов по каждому измерению.
  • Несколько ожидаемых процентов по общей сумме.
  • Окончательный запрос описан ниже.

Предположим, что у вас есть таблица 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;

Обратите внимание, что это эскиз решения. Вы должны заполнить детали о размерах.

Это будет работать до тех пор, пока у вас будет достаточно данных для всех ячеек.

На практике при выполнении этого типа многомерной стратифицированной выборки вы рискуете, что ячейки будут пустыми или слишком маленькими. Когда это произойдет, вы можете часто исправить это с последующим дополнительным пропуском. Возьмите то, что вы можете, из достаточно больших ячеек. Обычно они составляют большинство необходимых данных. Затем добавьте записи, чтобы встретить окончательный счет. Записи, которые должны быть добавлены, - это те, значения которых соответствуют тому, что необходимо по самым необходимым измерениям. Однако это решение просто предполагает, что для удовлетворения ваших критериев достаточно данных.

Ответ 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 возможностей, как достичь предложенных чисел. И это становится еще более сложным, когда у вас всего лишь ограниченное количество записей на каждый "класс" ( "Мужской/Северный =" класс ").

Вам может понадобиться до MIN(COUNT(gender), COUNT(location)) записей для каждой ячейки в таблице выше (для случая, когда она будет равна нулю).

Это зависит от:

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

Итак, вам нужно подсчитать доступные записи каждой пары gender/location AVAILABLE(gender, location).

Поиск конкретного соответствия кажется близким к полумагическим квадратам [1] [2].

И есть несколько вопросов по math.stackexchange.com об этом [3] [4].

Я закончил читать некоторые статьи о том, как их построить, и я сомневаюсь, что это можно сделать с помощью одного выбора.

Если у вас достаточно записей и не получится в такой ситуации:

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

Я бы пошел с итерационными местами корыта и добавил на каждом шаге пропорциональное количество мужчин/женщин:

  • M: 100 (16%); F: 0 (0%)
  • M: 100 (16%); F: 200 (50%)
  • M: 400 (66%); F: 200 (50%)
  • M: 600 (100%); F: 400 (100%)

Но это даст вам только приблизительные результаты и после проверки тех, которые вы можете захотеть повторить результат через несколько раз и настроить подсчеты в каждой категории, чтобы быть "достаточно хорошими".

Ответ 4

Это можно решить в два этапа. Я опишу, как это сделать для примера, где гендер и регион являются измерениями. Тогда я опишу более общий случай. На первом этапе мы решаем систему уравнений из 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

Даны числа 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);

Обозначим размерность D с n возможностями как D: n. Предположим, что у вас есть размеры D1: n1, D2: n2,... DM: nM. Он будет генерировать переменные n1 * n2 *... nM. Число генерируемых уравнений равно n1 + n2 +... nM. Вместо этого определим общий метод, чтобы взять другой случай из трех измерений, 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 оператора select и возьмите несвязный союз. Мы можем решить аналогично для любых измерений.

Итак, вкратце: учитывая размеры D1: n1, D2: n2,... DM: nM, мы можем решить соответствующую задачу линейного программирования, как описано выше для n1 * n2 *... nM переменных, а затем сгенерировать решение взяв несвязное объединение над операторами n1 * n2 *... nM select. Итак, да, мы можем генерировать решение с помощью операторов select, но сначала мы должны решить уравнения и определить пределы, получая подсчеты для каждой из переменных n1 * n2 *... nM.

Несмотря на то, что щедрость закончилась, я собираюсь добавить немного больше для тех, кого вы интересуете. Я утверждаю, что я полностью показал, как это решить, если есть решение.

Чтобы прояснить мой подход. В случае 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 уравнения в целом. Так что я здесь сделал довольно просто. Я рассуждал, что любая строка, которую вы выбираете, вносит вклад в каждый из трех измерений, и есть только 24 возможности. Тогда пусть Xi - число для каждой возможности, и вы получите уравнения, которые необходимо решить. Мне кажется, что любой метод, который вы придумали, должен быть решением этих уравнений. Другими словами, я просто переформулировал проблему с точки зрения решения этих уравнений.

Теперь нам нужно целочисленное решение, так как мы не можем иметь дробную строку. Обратите внимание, что это все линейные уравнения. Но мы хотим целочисленное решение. Вот ссылка на статью, в которой описывается, как их решить: https://www.math.uwaterloo.ca/~wgilbert/Research/GilbertPathria.pdf

Ответ 5

Я бы построил карту распределения базы данных и использовал ее для реализации логики выборки. Бонусы включают возможность добавления быстрой демографической обратной связи для пользователя и дополнительной нагрузки на сервер. На стороне консоли вам необходимо реализовать механизм, позволяющий синхронизировать базу данных и карту.

Это может выглядеть так, используя 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, например, соответствует критериям), вы получаете 8 для u20 и чрезмерно используете 8 для f21to29. После первого запуска настройте каждый недостающий критерий в других регионах. Так что tmp.male.south.u20+=8;tmp.male.south.f21to29-=8;.

Это довольно утомительно, чтобы понять это.

В итоге у вас есть правильный дистрибутив, который можно использовать для создания тривиального SQL-запроса.

Ответ 6

Я ожидаю, что вы захотите создать кучу запросов на основе необходимых фильтров.

Я объясню возможный подход, с полным образцом кода, - но обратите внимание на предостережения позже.
Я также рассмотрю проблему, когда вы не можете выполнить запрошенный образец из пропорционального распределения, но вы можете с помощью скорректированного распределения - и объясните, как сделать эту настройку

Основной алгоритм выглядит следующим образом:

Начните с набора фильтров {F1, F2, ... Fn}, каждый из которых имеет группу значений и проценты, которые должны быть распределены между этими значениями. Например, F1 может представлять собой пол, с двумя значениями (F1V1 = Мужской: 60%, F1V2 = Женский: 40%). Вы также захотите, чтобы общий размер выборки (назовем это X). С этой начальной точки вы можете объединить все элементы фильтров из каждого фильтра, чтобы получить единый набор всех объединенных фильтрующих элементов и количества, необходимые для каждого. Код должен иметь возможность обрабатывать любое количество фильтров с любым количеством значений (точных значений или диапазонов)

EG: предположим, 2 фильтра, F1: пол, {F1V1 = Мужской: 60%, F1V2 = Женский: 40%}, F2: регион, {F2V1 = Север: 50%, F2V2 = Юг: 50%} и общий образец, необходимый для X = 10 человек.
В этом примере мы хотели бы, чтобы 6 из них были мужчинами, 4 из которых были женщинами, 5 - с севера и 5 - с юга.

Тогда выполняем

  • Создайте sql-заглушку для каждого значения в F1 - с соответствующей долей начального процента (т.
    • WHERE gender = 'Male': 0,6,
    • WHERE gender = 'Female': 0.4)
  • Для каждого элемента в F2 - создайте новый sql-заглушку из каждого элемента с шага выше - с фильтром, который теперь является значением F1 и значением F2, а связанная с ним фракция является продуктом двух фракций. Итак, теперь у нас есть 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
  • Повторите шаг 2 выше для каждого дополнительного фильтра F3 - Fn. (в нашем примере было всего 2 фильтра, поэтому мы уже сделали)
  • Вычислить предел для каждого SQL-заглушки как [фракция, связанная с заглушкой] * X = общий размер требуемого образца (так что для нашего примера это 0.3 * 10 = 3 для Male/North, 0.2 * 10 = 2 для Female/North и т.д.)
  • Наконец, для каждого sql-заглушки - превратите его в полный оператор SQL и добавьте лимит

Пример кода

Я предоставил код С# для этого, но это должно быть достаточно легко перевести на другие языки. Было бы довольно сложно сделать это в чистом динамическом 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;}
}

Примечания (согласно комментарию по знаку)

  • Ошибки округления могут означать, что вы не получите ровно 600/400, на которые вы стремитесь при применении большого количества фильтров, но должны быть близки.
  • Если ваш набор данных не очень разнообразен, возможно, не всегда возможно создать необходимый раскол. Для этого метода потребуется равномерное распределение среди фильтров (поэтому, если вы делали в общей сложности 10 человек, 6 мужчин, 4 женщин, 5 с севера, 5 с юга потребовали бы 3 мужчин с севера, 3 мужчин из юг, 2 женщины с севера и 2 женщины с юга.)
  • Люди не собираются получать наугад - как раз по умолчанию. Вам нужно будет добавить что-то вроде ORDER BY RAND() (но не так, как его ОЧЕНЬ неэффективно), чтобы получить случайный выбор.
  • Остерегайтесь инъекции SQL. Санируйте все пользовательские ввод, заменяя одиночные кавычки '.

Плохо распределенная проблема с образцами

Как вы решаете проблему нехватки элементов в одном из наших ковшей для создания нашего образца в соответствии с репрезентативным разделом (что приведенный выше алгоритм)? Или что, если ваши числа не являются целыми числами?

Ну, я не пойду так, чтобы предоставить код, но я опишу возможный подход. Вам нужно будет немного изменить код, потому что плоский список заглушек sql больше не собирается его вырезать. Вместо этого вам нужно будет построить n-мерную матрицу SQL-заглушек (добавив измерение для каждого фильтра F1-n). После того, как шаг 4 выше был завершен (где у нас есть требуемые, но не обязательно возможные номера для каждого элемента SQL-заглушки), то, что я ожидал бы сделать, это

  • сгенерировать SQL, чтобы выбрать подсчеты для всех объединенных заглушек WHERE.
  • Затем вы выполните итерацию коллекции - и если вы нажмете элемент, где запрошенный лимит будет больше, чем счетчик (или не целое число)
    • отрегулируйте запрошенный предел до счетчика (или ближайшего целого числа).
    • Затем выберите другой элемент на каждой из осей, который, по крайней мере, выше приведенной настройки ниже, чем его максимальное количество, и настройте его на то же самое. Если невозможно найти подходящие предметы, то запрошенный раскол невозможен.
    • Затем отрегулируйте все пересекающиеся элементы для восходящих настроенных элементов снова.
    • Повторите шаг выше для пересечений между пересекающимися точками для каждой дополнительной размерности до n (но каждый раз переключайте настройку между отрицательным и положительным)

Предположим, что мы продолжаем наш предыдущий пример - наш представитель раскол:
Мужчины/Север = 3, Женщины/Север = 2, Мужчины/Юг = 3, Женщины/Юг = 2, но на севере всего 2 мальчика (но это больше людей из других групп, которые мы могли бы выбрать)

  • Мы корректируем Male/North до 2 (-1)
  • Мы настраиваем Female/North на 3 (+1) и Male/South на 4 (+1)
  • Корректируем Пересечение Женщины/Юг на 1 (-1). Вуаля! (дополнительных размеров нет, поскольку у нас было только 2 критерия/размеры)

Эта иллюстрация может быть полезна при настройке пересекающихся элементов в более высоких измерениях (отображается только до 4-х измерений, но должна помочь определить, что нужно сделать!) Каждая точка представляет собой один из наших элементов stub SQL в n-мерной матрице ( и имеет связанное с ним предельное число). Линия представляет собой общее значение критерия (например, gender = male). Цель состоит в том, чтобы общая сумма по любой строке оставалась неизменной после завершения корректировки! Начнем с красной точки и продолжаем каждый дополнительный размер... В приведенном выше примере мы будем рассматривать только 2 измерения - квадрат, образованный из красной точки, 2 оранжевые точки выше и справа от него, а 1 зеленая точка - к NE, чтобы завершить квадрат.

adjustments

Ответ 7

Формирование бизнес-логики в 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 будет иметь абстракцию соединения и пересечения.

Ответ 8

Я бы использовал язык программирования для генерации операторов SQL, но ниже это решение в чистом mySQL. Сделано одно предположение: в одной области всегда достаточно мужчин и женщин, чтобы соответствовать цифрам (например, что, если на севере нет женщины, живущей на севере?).

Подпрограмма предварительно вычисляет необходимые количества строк. Предел не может быть задан с использованием переменной. Я больше оракул, у нас есть аналитические функции. MySQL также предоставляет это в некоторой степени, разрешая переменные. Поэтому я устанавливаю целевые регионы и пол и вычисляю разбивку. Затем я ограничиваю вывод, используя вычисления.

Этот запрос показывает подсчеты для доказательства концепции.

set @male=600;
set @female=400;
set @north=100;
set @south=200;
set @east=300;
set @west=400;
set @[email protected]*(@male/(@[email protected]));
set @[email protected]*(@male/(@[email protected]));
set @east_male [email protected] *(@male/(@[email protected]));
set @west_male [email protected] *(@male/(@[email protected]));
set @[email protected]*(@female/(@[email protected]));
set @[email protected]*(@female/(@[email protected]));
set @east_female [email protected] *(@female/(@[email protected]));
set @west_female [email protected] *(@female/(@[email protected]));

select gender, region, count(*) 
from (
          select * from (select @north_male  :[email protected]_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  :[email protected]_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   :[email protected]_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   :[email protected]_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:[email protected]_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:[email protected]_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 :[email protected]_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 :[email protected]_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 @[email protected]*(@male/(@[email protected]));
set @[email protected]*(@male/(@[email protected]));
set @east_male [email protected] *(@male/(@[email protected]));
set @west_male [email protected] *(@male/(@[email protected]));
set @[email protected]*(@female/(@[email protected]));
set @[email protected]*(@female/(@[email protected]));
set @east_female [email protected] *(@female/(@[email protected]));
set @west_female [email protected] *(@female/(@[email protected]));
          select * from (select @north_male  :[email protected]_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  :[email protected]_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   :[email protected]_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   :[email protected]_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:[email protected]_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:[email protected]_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 :[email protected]_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 :[email protected]_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.

Ответ 9

Ну, я думаю, вопрос заключается в случайном получении записей, а не в соотношении 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 (@[email protected][email protected])
    begin
     Insert into #Users select * from Users where region='north' 
    set @Northlimit=0
    set @[email protected]
    set @[email protected]
    set @MnorthCnt=0 
    set @FnorthCnt=0
    end

    If (@[email protected][email protected])
    begin
     Insert into #Users select * from Users where region='South' 
    set @Southlimit=0
    set @[email protected]
    set @[email protected]
    set @MsouthCnt=0
    set @FsouthCnt=0
    end

    If (@[email protected][email protected])
    begin
     Insert into #Users select * from Users where region='East' 
    set @Eastlimit=0
    set @[email protected]
    set @[email protected]
    set @MeastCnt=0
    set @FeastCnt=0
    end

    If (@[email protected][email protected])
    begin
     Insert into #Users select * from Users where region='West' 
    set @Westlimit=0
    set @[email protected]
    set @[email protected]
    set @MwestCnt=0
    set @FwestCnt=0
    end 

If @MnorthCnt<@Northlimit
 Begin
 insert into #Users select top (@[email protected]) * from Users where gender='female' and region='north'
 and userid not in (select userid from #users)
 set @Flimit-=(@[email protected])
 set @FNorthCnt-=(@[email protected])
 set @Northlimit-=(@[email protected])
 End

 If @FnorthCnt<@Northlimit
 Begin
 insert into #Users select top (@[email protected]) * from Users where gender='male' and region='north'
 and userid not in (select userid from #users)
 set @Mlimit-=(@[email protected])
 set @MNorthCnt-=(@[email protected])
 set @Northlimit-=(@[email protected])
 End

 if @MsouthCnt<@southlimit
 Begin
 insert into #Users select top (@[email protected]) * from Users where gender='female' and region='south'
 and userid not in (select userid from #users)
 set @Flimit-=(@[email protected])
 set @FSouthCnt-=(@[email protected])
 set @southlimit-=(@[email protected])
 End

 if @FsouthCnt<@southlimit
 Begin
 insert into #Users select top (@[email protected]) * from Users where gender='male' and region='south'
 and userid not in (select userid from #users)
 set @Mlimit-=(@[email protected])
 set @MSouthCnt-=(@[email protected])
 set @southlimit-=(@[email protected])
 End

if @MeastCnt<@eastlimit
 Begin
 insert into #Users select top (@[email protected]) * from Users where gender='female' and region='east'
 and userid not in (select userid from #users)
 set @Flimit-=(@[email protected])
 set @FEastCnt-=(@[email protected])
 set @eastlimit-=(@[email protected])
 End

if @FeastCnt<@eastlimit
 Begin
 insert into #Users select top (@[email protected]) * from Users where gender='male' and region='east'
 and userid not in (select userid from #users)
 set @Mlimit-=(@[email protected])
 set @MEastCnt-=(@[email protected])
 set @eastlimit-=(@[email protected])
End

if @MwestCnt<@westlimit
 Begin
 insert into #Users select top (@[email protected]) * from Users where gender='female' and region='west'
 and userid not in (select userid from #users)
 set @Flimit-=(@[email protected])
 set @FWestCnt-=(@[email protected])
 set @westlimit-=(@[email protected])
 End

if @FwestCnt<@westlimit
 Begin
 insert into #Users select top (@[email protected]) * from Users where gender='male' and region='west'
 and userid not in (select userid from #users)
 set @Mlimit-=(@[email protected])
 set @MWestCnt-=(@[email protected])
 set @westlimit-=(@[email protected])
 End     


    IF (@MnorthCnt>[email protected] and @FnorthCnt>[email protected] and @MsouthCnt>[email protected] and @FsouthCnt>[email protected] and @MeastCnt>[email protected] and @FeastCnt>[email protected] and @MwestCnt>[email protected] and @FwestCnt>[email protected] 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

Ответ 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|
+----------------------------------+