Função de classificação no MySQL


155

Preciso descobrir a classificação dos clientes. Aqui estou adicionando a consulta SQL padrão ANSI correspondente para o meu requisito. Por favor me ajude a convertê-lo para MySQL.

SELECT RANK() OVER (PARTITION BY Gender ORDER BY Age) AS [Partition by Gender], 
  FirstName, 
  Age,
  Gender 
FROM Person

Existe alguma função para descobrir a classificação no MySQL?

Respostas:


266

Uma opção é usar uma variável de classificação, como a seguinte:

SELECT    first_name,
          age,
          gender,
          @curRank := @curRank + 1 AS rank
FROM      person p, (SELECT @curRank := 0) r
ORDER BY  age;

A (SELECT @curRank := 0)peça permite a inicialização da variável sem a necessidade de um SETcomando separado .

Caso de teste:

CREATE TABLE person (id int, first_name varchar(20), age int, gender char(1));

INSERT INTO person VALUES (1, 'Bob', 25, 'M');
INSERT INTO person VALUES (2, 'Jane', 20, 'F');
INSERT INTO person VALUES (3, 'Jack', 30, 'M');
INSERT INTO person VALUES (4, 'Bill', 32, 'M');
INSERT INTO person VALUES (5, 'Nick', 22, 'M');
INSERT INTO person VALUES (6, 'Kathy', 18, 'F');
INSERT INTO person VALUES (7, 'Steve', 36, 'M');
INSERT INTO person VALUES (8, 'Anne', 25, 'F');

Resultado:

+------------+------+--------+------+
| first_name | age  | gender | rank |
+------------+------+--------+------+
| Kathy      |   18 | F      |    1 |
| Jane       |   20 | F      |    2 |
| Nick       |   22 | M      |    3 |
| Bob        |   25 | M      |    4 |
| Anne       |   25 | F      |    5 |
| Jack       |   30 | M      |    6 |
| Bill       |   32 | M      |    7 |
| Steve      |   36 | M      |    8 |
+------------+------+--------+------+
8 rows in set (0.02 sec)

52
+1 para a inicialização em linha desonesta, é um belo truque.
Charles

28
Ele não pediu uma partição? Meu entendimento das partições é que o conjunto de resultados teria classificações separadas para homens e mulheres.
Jesse Dhillon

2
@Jesse: Se for esse o caso, eu recentemente respondeu a uma pergunta semelhante: stackoverflow.com/questions/3162389/multiple-ranks-in-one-table
Daniel Vassallo

6
E se eu quiser atribuir a classificação de 4 a Anne e Bob?
Fahim Parkar

8
Isso não implementar o exemplo da questão, uma vez que perde a partition by genderparte da função analítica (que "números" o valor de classificação por sexo não para o resultado global)
a_horse_with_no_name

53

Aqui está uma solução genérica que atribui uma classificação densa sobre a partição às linhas. Ele usa variáveis ​​de usuário:

CREATE TABLE person (
    id INT NOT NULL PRIMARY KEY,
    firstname VARCHAR(10),
    gender VARCHAR(1),
    age INT
);

INSERT INTO person (id, firstname, gender, age) VALUES
(1,  'Adams',  'M', 33),
(2,  'Matt',   'M', 31),
(3,  'Grace',  'F', 25),
(4,  'Harry',  'M', 20),
(5,  'Scott',  'M', 30),
(6,  'Sarah',  'F', 30),
(7,  'Tony',   'M', 30),
(8,  'Lucy',   'F', 27),
(9,  'Zoe',    'F', 30),
(10, 'Megan',  'F', 26),
(11, 'Emily',  'F', 20),
(12, 'Peter',  'M', 20),
(13, 'John',   'M', 21),
(14, 'Kate',   'F', 35),
(15, 'James',  'M', 32),
(16, 'Cole',   'M', 25),
(17, 'Dennis', 'M', 27),
(18, 'Smith',  'M', 35),
(19, 'Zack',   'M', 35),
(20, 'Jill',   'F', 25);

SELECT person.*, @rank := CASE
    WHEN @partval = gender AND @rankval = age THEN @rank
    WHEN @partval = gender AND (@rankval := age) IS NOT NULL THEN @rank + 1
    WHEN (@partval := gender) IS NOT NULL AND (@rankval := age) IS NOT NULL THEN 1
END AS rnk
FROM person, (SELECT @rank := NULL, @partval := NULL, @rankval := NULL) AS x
ORDER BY gender, age;

Observe que as atribuições de variáveis ​​são colocadas dentro da CASEexpressão. Isso (em teoria) cuida da ordem da questão da avaliação. oIS NOT NULL é adicionado para lidar com tipo de dados conversão e problemas de curto-circuito.

PS: pode ser facilmente convertido em número de linha sobre partição, removendo todas as condições que verificam o empate.

| id | firstname | gender | age | rank |
|----|-----------|--------|-----|------|
| 11 | Emily     | F      | 20  | 1    |
| 20 | Jill      | F      | 25  | 2    |
| 3  | Grace     | F      | 25  | 2    |
| 10 | Megan     | F      | 26  | 3    |
| 8  | Lucy      | F      | 27  | 4    |
| 6  | Sarah     | F      | 30  | 5    |
| 9  | Zoe       | F      | 30  | 5    |
| 14 | Kate      | F      | 35  | 6    |
| 4  | Harry     | M      | 20  | 1    |
| 12 | Peter     | M      | 20  | 1    |
| 13 | John      | M      | 21  | 2    |
| 16 | Cole      | M      | 25  | 3    |
| 17 | Dennis    | M      | 27  | 4    |
| 7  | Tony      | M      | 30  | 5    |
| 5  | Scott     | M      | 30  | 5    |
| 2  | Matt      | M      | 31  | 6    |
| 15 | James     | M      | 32  | 7    |
| 1  | Adams     | M      | 33  | 8    |
| 18 | Smith     | M      | 35  | 9    |
| 19 | Zack      | M      | 35  | 9    |

Demonstração no db <> fiddle


2
Essa solução, ou a solução de Mukesh, deve ser a solução correta. Embora tecnicamente eu acredite que as soluções de vocês representam um ranking denso e não regular. Aqui está uma boa explicação das diferenças: sqlservercurry.com/2009/04/… .
Modulitos

Você também pode nos informar como é exatamente o código .php? Eu tentei seguir, mas o código acima não funciona. Como inserir um formato .php?
criador

Esta solução não é muito genérica; não funcionará se rank_column tiver um valor de 0. sqlfiddle.com/#!2/9c5dd/1
mike

1
@mike Adicione uma seção ELSE à instrução CASE:ELSE @rank_count := @rank_count + 1
Prince Odame 8/08/17

1
@abhash ORDER BY gender, age DESC?
Salman A

52

Embora a resposta mais votada seja classificada, ela não particiona, você pode fazer uma junção automática para particionar tudo também:

SELECT    a.first_name,
      a.age,
      a.gender,
        count(b.age)+1 as rank
FROM  person a left join person b on a.age>b.age and a.gender=b.gender 
group by  a.first_name,
      a.age,
      a.gender

Caso de Uso

CREATE TABLE person (id int, first_name varchar(20), age int, gender char(1));

INSERT INTO person VALUES (1, 'Bob', 25, 'M');
INSERT INTO person VALUES (2, 'Jane', 20, 'F');
INSERT INTO person VALUES (3, 'Jack', 30, 'M');
INSERT INTO person VALUES (4, 'Bill', 32, 'M');
INSERT INTO person VALUES (5, 'Nick', 22, 'M');
INSERT INTO person VALUES (6, 'Kathy', 18, 'F');
INSERT INTO person VALUES (7, 'Steve', 36, 'M');
INSERT INTO person VALUES (8, 'Anne', 25, 'F');

Resposta :

Bill    32  M   4
Bob     25  M   2
Jack    30  M   3
Nick    22  M   1
Steve   36  M   5
Anne    25  F   3
Jane    20  F   2
Kathy   18  F   1

esta é uma resposta maravilhosa justamente porque preciso fazer um ranking de partição. Obrigado senhor!
Kim Stacks

OMI tem a mesma complexidade que a subseleção na resposta de @Sam Kidman: O (n ^ 2). Mas não sei se é possível fazê-lo melhor no MySQL.
precisa saber é o seguinte

Confira onlamp.com/pub/a/mysql/2007/03/29/... para um grande tutorial ao longo das mesmas linhas
ferics2

Auto-junte-se para obter a classificação! Isso é ótimo. Por fim, uma solução sem variáveis e sem as funções da janela MySQL 8 . :)
Timo

24

Uma emenda da versão de Daniel para calcular o percentil junto com a classificação. Além disso, duas pessoas com as mesmas notas receberão a mesma classificação.

set @totalStudents = 0;
select count(*) into @totalStudents from marksheets;
SELECT id, score, @curRank := IF(@prevVal=score, @curRank, @studentNumber) AS rank, 
@percentile := IF(@prevVal=score, @percentile, (@totalStudents - @studentNumber + 1)/(@totalStudents)*100),
@studentNumber := @studentNumber + 1 as studentNumber, 
@prevVal:=score
FROM marksheets, (
SELECT @curRank :=0, @prevVal:=null, @studentNumber:=1, @percentile:=100
) r
ORDER BY score DESC

Resultados da consulta para dados de amostra -

+----+-------+------+---------------+---------------+-----------------+
| id | score | rank | percentile    | studentNumber | @prevVal:=score |
+----+-------+------+---------------+---------------+-----------------+
| 10 |    98 |    1 | 100.000000000 |             2 |              98 |
|  5 |    95 |    2 |  90.000000000 |             3 |              95 |
|  6 |    91 |    3 |  80.000000000 |             4 |              91 |
|  2 |    91 |    3 |  80.000000000 |             5 |              91 |
|  8 |    90 |    5 |  60.000000000 |             6 |              90 |
|  1 |    90 |    5 |  60.000000000 |             7 |              90 |
|  9 |    84 |    7 |  40.000000000 |             8 |              84 |
|  3 |    83 |    8 |  30.000000000 |             9 |              83 |
|  4 |    72 |    9 |  20.000000000 |            10 |              72 |
|  7 |    60 |   10 |  10.000000000 |            11 |              60 |
+----+-------+------+---------------+---------------+-----------------+

1
Mesmo que esse desempenho não seja realmente ótimo, é incrível!
Gaspa79

18

Combinação das respostas de Daniel e Salman. No entanto, a classificação não dará como continua a sequência com os empates. Em vez disso, pula a classificação para a próxima. Portanto, o máximo sempre alcança a contagem de linhas.

    SELECT    first_name,
              age,
              gender,
              IF(age=@_last_age,@curRank:=@curRank,@curRank:=@_sequence) AS rank,
              @_sequence:=@_sequence+1,@_last_age:=age
    FROM      person p, (SELECT @curRank := 1, @_sequence:=1, @_last_age:=0) r
    ORDER BY  age;

Esquema e caso de teste:

CREATE TABLE person (id int, first_name varchar(20), age int, gender char(1));

INSERT INTO person VALUES (1, 'Bob', 25, 'M');
INSERT INTO person VALUES (2, 'Jane', 20, 'F');
INSERT INTO person VALUES (3, 'Jack', 30, 'M');
INSERT INTO person VALUES (4, 'Bill', 32, 'M');
INSERT INTO person VALUES (5, 'Nick', 22, 'M');
INSERT INTO person VALUES (6, 'Kathy', 18, 'F');
INSERT INTO person VALUES (7, 'Steve', 36, 'M');
INSERT INTO person VALUES (8, 'Anne', 25, 'F');
INSERT INTO person VALUES (9, 'Kamal', 25, 'M');
INSERT INTO person VALUES (10, 'Saman', 32, 'M');

Resultado:

+------------+------+--------+------+--------------------------+-----------------+
| first_name | age  | gender | rank | @_sequence:=@_sequence+1 | @_last_age:=age |
+------------+------+--------+------+--------------------------+-----------------+
| Kathy      |   18 | F      |    1 |                        2 |              18 |
| Jane       |   20 | F      |    2 |                        3 |              20 |
| Nick       |   22 | M      |    3 |                        4 |              22 |
| Kamal      |   25 | M      |    4 |                        5 |              25 |
| Anne       |   25 | F      |    4 |                        6 |              25 |
| Bob        |   25 | M      |    4 |                        7 |              25 |
| Jack       |   30 | M      |    7 |                        8 |              30 |
| Bill       |   32 | M      |    8 |                        9 |              32 |
| Saman      |   32 | M      |    8 |                       10 |              32 |
| Steve      |   36 | M      |   10 |                       11 |              36 |
+------------+------+--------+------+--------------------------+-----------------+

1
Eu sou novo no MySQL, mas esta solução está ok? No MySQL, os documentos dizem "a ordem de avaliação para expressões envolvendo variáveis ​​de usuário é indefinida". dev.mysql.com/doc/refman/5.7/en/user-variables.html
narduk

13

A partir do MySQL 8, você pode finalmente usar as funções de janela também no MySQL: https://dev.mysql.com/doc/refman/8.0/en/window-functions.html

Sua consulta pode ser escrita exatamente da mesma maneira:

SELECT RANK() OVER (PARTITION BY Gender ORDER BY Age) AS `Partition by Gender`, 
  FirstName, 
  Age,
  Gender 
FROM Person

Não está errado, simplesmente não funciona com versões mais antigas do SQL. além disso, era meio que cópia e passado da pergunta dele, para que não parecesse a resposta certa.
Newdark-it

4
@ brand-it Para os que estão no MySQL 8+, essa resposta é importante, pois permite que o Rank esteja disponível. Se eu não tivesse rolado até aqui, eu assumiria que as respostas anteriores eram a única solução.
Steve Smith

1
@SteveSmith Bom ponto, é bom ter essa resposta para aqueles que usam a versão mais recente do MYSQL.
Newdark-it 5/09/19

Sim, estou desencorajado por muitas respostas com as variáveis ​​do usuário e os blocos lógicos. Uma nova versão do MySQL permite MUITO simples com a função RANK () que oferece um agrupamento integrado por partições.
James Bond

5

@ Sam, seu ponto de vista é excelente em termos de conceito, mas acho que você não entendeu o que os documentos do MySQL estão dizendo na página referenciada - ou entendi errado :-) - e eu só queria adicionar isso para que, se alguém se sentir desconfortável com o @ A resposta de Daniel será mais tranqüila ou, pelo menos, um pouco mais profunda.

Você vê que o "@curRank := @curRank + 1 AS rank"interior SELECTnão é "uma declaração", é uma parte "atômica" da declaração, portanto deve ser seguro.

O documento que você faz referência mostra exemplos em que a mesma variável definida pelo usuário em 2 partes (atômicas) da instrução, por exemplo, "SELECT @curRank, @curRank := @curRank + 1 AS rank" .

Pode-se argumentar que @curRanké usado duas vezes na resposta de @ Daniel: (1) "@curRank := @curRank + 1 AS rank"oe (2) o "(SELECT @curRank := 0) r"mas, já que o segundo uso faz parte doFROM cláusula, tenho certeza de que é garantido que ele seja avaliado primeiro; essencialmente fazendo dela uma segunda e anterior declaração.

De fato, na mesma página de documentos do MySQL que você referenciou, você verá a mesma solução nos comentários - pode ser de onde o @Daniel conseguiu; Sim, eu sei que são os comentários, mas são comentários na página oficial de documentos e isso tem algum peso.


Nada disso é justificado pela documentação. É apenas especulação (confusa). Como todas as respostas, usando e escrevendo a mesma variável, o que o manual diz explicitamente não está definido, embora o manual tenha muito texto inútil sobre o que pode funcionar como você espera, sem dizer o que acha que espera ou que uso é uma descrição do comportamento não garantido. PS A partir da 8.0, a atribuição de variáveis ​​fora do SET está obsoleta.
Philipxy # 17/18

4

A solução mais direta para determinar a classificação de um determinado valor é contar o número de valores antes dele. Suponha que tenhamos os seguintes valores:

10 20 30 30 30 40
  • Todos os 30valores são considerados
  • Todos os 40valores são considerados 6 (classificação) ou 4 (classificação densa)

Agora, de volta à pergunta original. Aqui estão alguns dados de amostra que são classificados conforme descrito em OP (classificações esperadas são adicionadas à direita):

+------+-----------+------+--------+    +------+------------+
| id   | firstname | age  | gender |    | rank | dense_rank |
+------+-----------+------+--------+    +------+------------+
|   11 | Emily     |   20 | F      |    |    1 |          1 |
|    3 | Grace     |   25 | F      |    |    2 |          2 |
|   20 | Jill      |   25 | F      |    |    2 |          2 |
|   10 | Megan     |   26 | F      |    |    4 |          3 |
|    8 | Lucy      |   27 | F      |    |    5 |          4 |
|    6 | Sarah     |   30 | F      |    |    6 |          5 |
|    9 | Zoe       |   30 | F      |    |    6 |          5 |
|   14 | Kate      |   35 | F      |    |    8 |          6 |
|    4 | Harry     |   20 | M      |    |    1 |          1 |
|   12 | Peter     |   20 | M      |    |    1 |          1 |
|   13 | John      |   21 | M      |    |    3 |          2 |
|   16 | Cole      |   25 | M      |    |    4 |          3 |
|   17 | Dennis    |   27 | M      |    |    5 |          4 |
|    5 | Scott     |   30 | M      |    |    6 |          5 |
|    7 | Tony      |   30 | M      |    |    6 |          5 |
|    2 | Matt      |   31 | M      |    |    8 |          6 |
|   15 | James     |   32 | M      |    |    9 |          7 |
|    1 | Adams     |   33 | M      |    |   10 |          8 |
|   18 | Smith     |   35 | M      |    |   11 |          9 |
|   19 | Zack      |   35 | M      |    |   11 |          9 |
+------+-----------+------+--------+    +------+------------+

Para calcular RANK() OVER (PARTITION BY Gender ORDER BY Age)para Sarah , você pode usar esta consulta:

SELECT COUNT(id) + 1 AS rank, COUNT(DISTINCT age) + 1 AS dense_rank
FROM testdata
WHERE gender = (SELECT gender FROM testdata WHERE id = 6)
AND age < (SELECT age FROM testdata WHERE id = 6)

+------+------------+
| rank | dense_rank |
+------+------------+
|    6 |          5 |
+------+------------+

Para calcular RANK() OVER (PARTITION BY Gender ORDER BY Age)para Todos linhas que você pode usar essa consulta:

SELECT testdata.id, COUNT(lesser.id) + 1 AS rank, COUNT(DISTINCT lesser.age) + 1 AS dense_rank
FROM testdata
LEFT JOIN testdata AS lesser ON lesser.age < testdata.age AND lesser.gender = testdata.gender
GROUP BY testdata.id

E aqui está o resultado (os valores agregados são adicionados à direita):

+------+------+------------+    +-----------+-----+--------+
| id   | rank | dense_rank |    | firstname | age | gender |
+------+------+------------+    +-----------+-----+--------+
|   11 |    1 |          1 |    | Emily     |  20 | F      |
|    3 |    2 |          2 |    | Grace     |  25 | F      |
|   20 |    2 |          2 |    | Jill      |  25 | F      |
|   10 |    4 |          3 |    | Megan     |  26 | F      |
|    8 |    5 |          4 |    | Lucy      |  27 | F      |
|    6 |    6 |          5 |    | Sarah     |  30 | F      |
|    9 |    6 |          5 |    | Zoe       |  30 | F      |
|   14 |    8 |          6 |    | Kate      |  35 | F      |
|    4 |    1 |          1 |    | Harry     |  20 | M      |
|   12 |    1 |          1 |    | Peter     |  20 | M      |
|   13 |    3 |          2 |    | John      |  21 | M      |
|   16 |    4 |          3 |    | Cole      |  25 | M      |
|   17 |    5 |          4 |    | Dennis    |  27 | M      |
|    5 |    6 |          5 |    | Scott     |  30 | M      |
|    7 |    6 |          5 |    | Tony      |  30 | M      |
|    2 |    8 |          6 |    | Matt      |  31 | M      |
|   15 |    9 |          7 |    | James     |  32 | M      |
|    1 |   10 |          8 |    | Adams     |  33 | M      |
|   18 |   11 |          9 |    | Smith     |  35 | M      |
|   19 |   11 |          9 |    | Zack      |  35 | M      |
+------+------+------------+    +-----------+-----+--------+

3

Se você deseja classificar apenas uma pessoa, pode fazer o seguinte:

SELECT COUNT(Age) + 1
 FROM PERSON
WHERE(Age < age_to_rank)

Essa classificação corresponde à função RANK da Oracle (onde, se você tem pessoas com a mesma idade, elas obtêm a mesma classificação, e a classificação depois disso é não consecutiva).

É um pouco mais rápido do que usar uma das soluções acima em uma subconsulta e selecionar uma delas para obter a classificação de uma pessoa.

Isso pode ser usado para classificar todos, mas é mais lento que as soluções acima.

SELECT
  Age AS age_var,
(
  SELECT COUNT(Age) + 1
  FROM Person
  WHERE (Age < age_var)
 ) AS rank
 FROM Person

Pode ficar muito mais lento que as soluções acima quando o número de linhas na Persontabela aumenta. É O (n ^ 2) vs O (n) mais lento.
xmedeko

2

Para evitar o " porém " na resposta de Erandac em combinação com as respostas de Daniel e Salman, pode-se usar uma das seguintes "soluções alternativas de partição"

SELECT customerID, myDate

  -- partition ranking works only with CTE / from MySQL 8.0 on
  , RANK() OVER (PARTITION BY customerID ORDER BY dateFrom) AS rank, 

  -- Erandac's method in combination of Daniel's and Salman's
  -- count all items in sequence, maximum reaches row count.
  , IF(customerID=@_lastRank, @_curRank:=@_curRank, @_curRank:=@_sequence+1) AS sequenceRank
  , @_sequence:=@_sequence+1 as sequenceOverAll

  -- Dense partition ranking, works also with MySQL 5.7
  -- remember to set offset values in from clause
  , IF(customerID=@_lastRank, @_nxtRank:=@_nxtRank, @_nxtRank:=@_nxtRank+1 ) AS partitionRank
  , IF(customerID=@_lastRank, @_overPart:=@_overPart+1, @_overPart:=1 ) AS partitionSequence

  , @_lastRank:=customerID
FROM myCustomers, 
  (SELECT @_curRank:=0, @_sequence:=0, @_lastRank:=0, @_nxtRank:=0, @_overPart:=0 ) r
ORDER BY customerID, myDate

A classificação da partição na 3ª variante neste trecho de código retornará números de classificação contínuos. isso levará a uma estrutura de dados semelhante ao rank() over partition byresultado. Como um exemplo, veja abaixo. Em particular, o partitionSequence sempre começará com 1 para cada novo partitionRank , usando este método:

customerID    myDate   sequenceRank (Erandac)
                          |    sequenceOverAll
                          |     |   partitionRank
                          |     |     | partitionSequence
                          |     |     |    | lastRank
... lines ommitted for clarity
40    09.11.2016 11:19    1     44    1   44    40
40    09.12.2016 12:08    1     45    1   45    40
40    09.12.2016 12:08    1     46    1   46    40
40    09.12.2016 12:11    1     47    1   47    40
40    09.12.2016 12:12    1     48    1   48    40
40    13.10.2017 16:31    1     49    1   49    40
40    15.10.2017 11:00    1     50    1   50    40
76    01.07.2015 00:24    51    51    2    1    76
77    04.08.2014 13:35    52    52    3    1    77
79    15.04.2015 20:25    53    53    4    1    79
79    24.04.2018 11:44    53    54    4    2    79
79    08.10.2018 17:37    53    55    4    3    79
117   09.07.2014 18:21    56    56    5    1   117
119   26.06.2014 13:55    57    57    6    1   119
119   02.03.2015 10:23    57    58    6    2   119
119   12.10.2015 10:16    57    59    6    3   119
119   08.04.2016 09:32    57    60    6    4   119
119   05.10.2016 12:41    57    61    6    5   119
119   05.10.2016 12:42    57    62    6    6   119
...

0
select id,first_name,gender,age,
rank() over(partition by gender order by age) rank_g
from person

CREATE TABLE person (id int, first_name varchar(20), age int, gender char(1));

INSERT INTO person VALUES (1, 'Bob', 25, 'M');
INSERT INTO person VALUES (2, 'Jane', 20, 'F');
INSERT INTO person VALUES (3, 'Jack', 30, 'M');
INSERT INTO person VALUES (4, 'Bill', 32, 'M');
INSERT INTO person VALUES (5, 'Nick', 22, 'M');
INSERT INTO person VALUES (6, 'Kathy', 18, 'F');
INSERT INTO person VALUES (7, 'Steve', 36, 'M');
INSERT INTO person VALUES (8, 'Anne', 25, 'F');
INSERT INTO person VALUES (9,'AKSH',32,'M');
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.