Función de clasificación en MySQL


155

Necesito averiguar el rango de clientes. Aquí estoy agregando la consulta SQL estándar ANSI correspondiente para mi requisito. Por favor, ayúdame a convertirlo a MySQL.

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

¿Hay alguna función para averiguar el rango en MySQL?

Respuestas:


266

Una opción es usar una variable de clasificación, como la siguiente:

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

La (SELECT @curRank := 0)parte permite la inicialización de la variable sin requerir un SETcomando separado .

Caso de prueba:

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 la desviada inicialización en línea, es un truco hermoso.
Charles

28
Sin embargo, ¿no pidió una partición? Mi comprensión de las particiones es que el conjunto de resultados tendría clasificaciones separadas para hombres y mujeres.
Jesse Dhillon

2
@Jesse: Si ese es el caso, recientemente respondí una pregunta similar: stackoverflow.com/questions/3162389/multiple-ranks-in-one-table
Daniel Vassallo

66
¿Qué pasa si quiero darle un rango de 4 a Anne y Bob?
Fahim Parkar

8
Esto no implementa el ejemplo de la pregunta ya que pierde la partition by genderparte de la función analítica (que "numera" el valor de rango por género no para el resultado general)
a_horse_with_no_name

53

Aquí hay una solución genérica que asigna un rango denso sobre la partición a las filas. Utiliza variables de usuario:

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 las asignaciones de variables se colocan dentro de la CASEexpresión. Esto (en teoría) se ocupa del orden de la cuestión de la evaluación. Se IS NOT NULLagrega para manejar la conversión de tipo de datos y los problemas de cortocircuito.

PD: Se puede convertir fácilmente al número de fila sobre la partición eliminando todas las condiciones que verifican el 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    |

Demostración en db <> fiddle


2
Esta solución, o la solución de Mukesh, debería ser la solución correcta. Aunque técnicamente creo que las soluciones de ustedes dos representan una clasificación densa y no una clasificación regular. Aquí hay una buena explicación de las diferencias: sqlservercurry.com/2009/04/… .
modulitos

¿También puede decirnos cómo debe ser exactamente el código .php? Traté de seguirlo, pero el código anterior no funciona. ¿Cómo ingresar al formato .php?
creador

Esta solución no es muy genérica; no funcionará si rank_column tiene un valor de 0. sqlfiddle.com/#!2/9c5dd/1
mike

1
@mike Agregue una sección ELSE a la declaración CASE:ELSE @rank_count := @rank_count + 1
Prince Odame

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

52

Si bien la respuesta más votada se clasifica, no se divide, puedes hacer una auto-unión para que todo se particione también:

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');

Respuesta :

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 es una respuesta maravillosa precisamente porque necesito hacer una clasificación de partición. ¡Gracias Señor!
Kim Stacks

En mi opinión, tiene la misma complejidad que la subselección en la respuesta de @Sam Kidman: O (n ^ 2). Pero no sé si es posible hacerlo mejor en MySQL.
xmedeko

Visite onlamp.com/pub/a/mysql/2007/03/29/… para obtener un excelente tutorial en la misma línea
ferics2

¡Únete a ti mismo para obtener el rango! Eso es genial. Por fin, una solución sin variables y sin funciones de ventana MySQL 8 . :)
Timo

24

Un ajuste de la versión de Daniel para calcular el percentil junto con el rango. También dos personas con las mismas marcas obtendrán el mismo rango.

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 de la consulta para una muestra de datos -

+----+-------+------+---------------+---------------+-----------------+
| 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
Aunque esto no es realmente óptimo en rendimiento, ¡es increíble!
Gaspa79

18

Combinación de la respuesta de Daniel y Salman. Sin embargo, el rango no cederá a medida que continúe la secuencia con los lazos. En cambio, salta el rango al siguiente. Por lo tanto, el máximo siempre alcanza el recuento de filas.

    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 y caso de prueba:

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');

Salida:

+------------+------+--------+------+--------------------------+-----------------+
| 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
Soy nuevo en MySQL, pero ¿está bien esta solución? En los documentos de MySQL dice "el orden de evaluación para las expresiones que involucran variables de usuario no está definido". dev.mysql.com/doc/refman/5.7/en/user-variables.html
narduk

13

Comenzando con MySQL 8, finalmente puede usar funciones de ventana también en MySQL: https://dev.mysql.com/doc/refman/8.0/en/window-functions.html

Su consulta se puede escribir exactamente de la misma manera:

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

No está mal, simplemente no funciona con versiones anteriores de SQL. Además, era un poco copia y pasado de su pregunta, por lo que no parece que se ajuste a la respuesta.
newdark-it

44
@ brand-it Para aquellos de MySQL 8+, esta respuesta es importante ya que nos permite saber que Rank ya está disponible. Si no me hubiera desplazado tanto, supongo que las respuestas anteriores fueron la única solución.
Steve Smith

1
@SteveSmith Buen punto, es bueno tener esta respuesta para aquellos que usan la versión más reciente de MYSQL.
newdark-it

Sí, estoy desanimado por muchas respuestas con las variables de usuario y los bloques lógicos. Una nueva versión de MySQL permite hacerlo MUCHO simple con la función RANK () que ofrece una agrupación integrada por particiones.
James Bond

5

@Sam, su punto es excelente en concepto, pero creo que entendió mal lo que dicen los documentos de MySQL en la página de referencia, o no entiendo :-), y solo quería agregar esto para que si alguien se siente incómodo con el @ La respuesta de Daniel será más tranquilizador o al menos profundizará un poco más.

Verá que el "@curRank := @curRank + 1 AS rank"interior SELECTno es "una declaración", es una parte "atómica" de la declaración, por lo que debe ser segura.

El documento al que hace referencia continúa para mostrar ejemplos en los que la misma variable definida por el usuario en 2 partes (atómicas) de la declaración, por ejemplo "SELECT @curRank, @curRank := @curRank + 1 AS rank",.

Se podría argumentar que @curRankse usa dos veces en la respuesta de @ Daniel: (1) el "@curRank := @curRank + 1 AS rank"y (2) el "(SELECT @curRank := 0) r"pero ya que el segundo uso es parte delFROM cláusula, estoy bastante seguro de que se garantiza que se evaluará primero; esencialmente haciéndolo una segunda declaración anterior.

De hecho, en la misma página de documentos de MySQL a la que hizo referencia, verá la misma solución en los comentarios: podría ser de donde lo obtuvo @Daniel; Sí, sé que son los comentarios, pero son comentarios en la página de documentos oficiales y eso tiene algo de peso.


Nada de esto está justificado por la documentación. Es solo especulación (difusa). Como todas las respuestas usan y escriben la misma variable, que el manual dice que no está explícitamente definida, aunque el manual tiene una gran cantidad de texto inútil sobre lo que podría funcionar como espera, sin decir lo que cree que espera o qué uso Una descripción del comportamiento no garantizado es. PS A partir de 8.0, la asignación de variables fuera de SET está en desuso.
filipina

4

La solución más directa para determinar el rango de un valor dado es contar la cantidad de valores que tiene delante . Supongamos que tenemos los siguientes valores:

10 20 30 30 30 40
  • Todos los 30valores se consideran 3er.
  • Todas 40 valores se consideran 6to (rango) o 4to (rango denso)

Ahora volvamos a la pregunta original. Aquí hay algunos datos de muestra que se ordenan como se describe en OP (los rangos esperados se agregan a la derecha):

+------+-----------+------+--------+    +------+------------+
| 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 |
+------+-----------+------+--------+    +------+------------+

Calcular RANK() OVER (PARTITION BY Gender ORDER BY Age) para Sarah , puede 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 |
+------+------------+

Calcular RANK() OVER (PARTITION BY Gender ORDER BY Age) de todas las filas se puede utilizar esta 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

Y aquí está el resultado (los valores unidos se agregan a la derecha):

+------+------+------------+    +-----------+-----+--------+
| 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

Si desea clasificar a una sola persona, puede hacer lo siguiente:

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

Esta clasificación corresponde a la función RANGO de oráculo (donde si tiene personas con la misma edad obtienen la misma clasificación, y la clasificación posterior no es consecutiva).

Es un poco más rápido que usar una de las soluciones anteriores en una subconsulta y seleccionarla para obtener la clasificación de una persona.

Esto se puede usar para clasificar a todos, pero es más lento que las soluciones anteriores.

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

Puede ser mucho más lento que las soluciones anteriores cuando Personaumenta el número de filas en la tabla. Es O (n ^ 2) vs O (n) más lento.
xmedeko

2

Para evitar el " sin embargo " en la respuesta de Erandac en combinación con las respuestas de Daniel y Salman, uno puede usar una de las siguientes "soluciones alternativas de partición"

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

La clasificación de la partición en la tercera variante en este fragmento de código devolverá números de clasificación continua. Esto conducirá a una estructura de datos similar al rank() over partition byresultado. Como ejemplo, ver abajo. En particular, la secuencia de partición siempre comenzará con 1 para cada nueva partición de banco , utilizando 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');
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.