sql >> Base de Datos >  >> RDS >> PostgreSQL

Tablesample y otros métodos para obtener tuplas aleatorias

TABLESAMPLE de PostgreSQL ofrece algunas ventajas más en comparación con otras formas tradicionales de obtener tuplas aleatorias.

TABLESAMPLE es una cláusula SQL SELECT y proporciona dos métodos de muestreo que son SYSTEM y BERNOULLI . Con la ayuda de TABLESAMPLE podemos recuperar fácilmente filas aleatorias de una tabla. Para obtener más información sobre TABLESAMPLE, puede consultar la publicación de blog anterior .

En esta publicación de blog, hablaremos sobre formas alternativas de obtener filas aleatorias. Cómo la gente seleccionaba filas aleatorias antes de TABLESAMPLE , cuáles son los pros y los contras de los otros métodos y qué ganamos con TABLESAMPLE ?

Hay publicaciones de blog increíbles sobre la selección de filas aleatorias, puede comenzar a leer las siguientes publicaciones de blog para obtener una comprensión profunda de este tema.

Mis pensamientos sobre obtener una fila aleatoria

Obtener filas aleatorias de una tabla de base de datos

aleatorio_agg()

Comparemos las formas tradicionales de obtener filas aleatorias de una tabla con las nuevas formas proporcionadas por TABLESAMPLE.

Antes del TABLESAMPLE cláusula, había 3 métodos comúnmente utilizados para seleccionar filas aleatoriamente de una tabla.

1- Ordenar al azar()

Para fines de prueba, necesitamos crear una tabla y poner algunos datos dentro de ella.

Creemos la tabla ts_test e insertemos 1M de filas en ella:

CREATE TABLE ts_test (
  id SERIAL PRIMARY KEY,
  title TEXT
);

INSERT INTO ts_test (title)
SELECT
    'Record #' || i
FROM
    generate_series(1, 1000000) i;

Teniendo en cuenta la siguiente instrucción SQL para seleccionar 10 filas aleatorias:

SELECT * FROM ts_test ORDER BY random() LIMIT 10;

Hace que PostgreSQL realice un escaneo completo de la tabla y también ordene. Por lo tanto, este método no es el preferido para tablas con un gran número de filas por motivos de rendimiento.

Analicemos EXPLAIN ANALYZE resultado de esta consulta anterior:

random=# EXPLAIN ANALYZE SELECT * FROM ts_test ORDER BY random() LIMIT 10;
 QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Limit (cost=33959.03..33959.05 rows=10 width=36) (actual time=1956.786..1956.807 rows=10 loops=1)
 -> Sort (cost=33959.03..35981.18 rows=808863 width=36) (actual time=1956.780..1956.789 rows=10 loops=1)
 Sort Key: (random())
 Sort Method: top-N heapsort Memory: 25kB
 -> Seq Scan on ts_test (cost=0.00..16479.79 rows=808863 width=36) (actual time=0.276..1001.109 rows=1000000 loops=1)
 Planning time: 1.434 ms
 Execution time: 1956.900 ms
(7 rows)

Como EXPLAIN ANALYZE señala, seleccionar 10 de 1 millón de filas tomó casi 2 segundos. La consulta también usó la salida de random() como clave de clasificación para ordenar los resultados. Ordenar parece ser la tarea que consume más tiempo aquí. Ejecutemos esto con escenario usando TABLESAMPLE .

En PostgreSQL 9.5, para obtener el número exacto de filas al azar, podemos usar el método de muestreo SYSTEM_ROWS. Proporcionado por tsm_system_rows módulo contrib, nos permite especificar cuántas filas deben devolverse por muestreo. Normalmente, solo se puede especificar el porcentaje solicitado con TABLESAMPLE SYSTEMBERNOULLI métodos.

Primero, debemos crear tsm_system_rows extensión para usar este método ya que tanto TABLESAMPLE SYSTEM y TABLESAMPLE BERNOULLI Los métodos no garantizan que el porcentaje proporcionado resulte en el número esperado de filas. Consulte el TABLESAMPLE anterior p ost para recordar por qué funcionan así.

Comencemos creando tsm_system_rows extensión:

random=# CREATE EXTENSION tsm_system_rows;
CREATE EXTENSION

Ahora comparemos “ORDER BY random()EXPLAIN ANALYZE salida con EXPLAIN ANALYZE salida de tsm_system_rows consulta que devuelve 10 filas aleatorias de una tabla de 1M de filas.

random=# EXPLAIN ANALYZE SELECT * FROM ts_test TABLESAMPLE SYSTEM_ROWS(10);
 QUERY PLAN
-------------------------------------------------------------------------------------------------------
 Sample Scan on ts_test (cost=0.00..4.10 rows=10 width=18) (actual time=0.069..0.083 rows=10 loops=1)
 Sampling: system_rows ('10'::bigint)
 Planning time: 0.646 ms
 Execution time: 0.159 ms
(4 rows)

Toda la consulta tomó 0.159 ms. Este método de muestreo es extremadamente rápido en comparación con el “ORDER BY random() ” método que tomó 1956.9 ms.

2- Comparar con aleatorio()

El siguiente SQL nos permite recuperar filas aleatorias con un 10 % de probabilidad

SELECT * FROM ts_test WHERE random() <= 0.1;

Este método es más rápido que ordenar al azar porque no ordena las filas seleccionadas. Devolverá un porcentaje aproximado de filas de la tabla como BERNOULLI o SYSTEM TABLESAMPLE métodos.

Revisemos el EXPLAIN ANALYZE salida de random() consulta anterior:

random=# EXPLAIN ANALYZE SELECT * FROM ts_test WHERE random() <= 0.1;
 QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Seq Scan on ts_test (cost=0.00..21369.00 rows=333333 width=18) (actual time=0.089..280.483 rows=100014 loops=1)
 Filter: (random() <= '0.1'::double precision)
 Rows Removed by Filter: 899986
 Planning time: 0.704 ms
 Execution time: 367.527 ms
(5 rows)

La consulta tardó 367,5 ms. Mucho mejor que ORDER BY random() . Pero es más difícil controlar el recuento exacto de filas. Comparemos “random()EXPLAIN ANALYZE salida con EXPLAIN ANALYZE salida de TABLESAMPLE BERNOULLI consulta que devuelve aproximadamente el 10 % de las filas aleatorias de la tabla de 1 millón de filas.

random=# EXPLAIN ANALYZE SELECT * FROM ts_test TABLESAMPLE BERNOULLI (10);
 QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
 Sample Scan on ts_test  (cost=0.00..7369.00 rows=100000 width=18) (actual time=0.015..147.002 rows=100155 loops=1)
   Sampling: bernoulli ('10'::real)
 Planning time: 0.076 ms
 Execution time: 239.289 ms
(4 rows)

Le dimos 10 como parámetro a BERNOULLI porque necesitamos el 10% de todos los registros.

Aquí podemos ver que el BERNOULLI El método tardó 239,289 ms en ejecutarse. Estos dos métodos son bastante similares en su funcionamiento, BERNOULLI es un poco más rápido ya que la selección aleatoria se realiza en un nivel inferior. Una ventaja de usar BERNOULLI comparado con WHERE random() <= 0.1 es el REPEATABLE cláusula que describimos en TABLESAMPLE anterior publicar.

3- Columna extra con un valor aleatorio

Este método sugiere agregar una nueva columna con valores asignados aleatoriamente, agregarle un índice, ordenar por esa columna y, opcionalmente, actualizar sus valores periódicamente para aleatorizar la distribución.

Esta estrategia permite un muestreo aleatorio mayormente repetible. Funciona mucho más rápido que el primer método, pero requiere un esfuerzo configurarlo por primera vez y genera un costo de rendimiento en las operaciones de inserción, actualización y eliminación.

Apliquemos este método en nuestro ts_test mesa.

Primero, agregaremos una nueva columna llamada randomcolumn con el tipo de doble precisión porque random() de PostgreSQL la función devuelve un número con doble precisión.

random=# ALTER TABLE ts_test ADD COLUMN randomcolumn DOUBLE PRECISION;
ALTER TABLE

Luego actualizaremos la nueva columna usando random() función.

random=# \timing
Timing is on.
random=# BEGIN;
BEGIN
Time: 2.071 ms
random=# UPDATE ts_test SET randomcolumn = RANDOM();
UPDATE 1000000
Time: 8483.741 ms
random=# COMMIT;
COMMIT
Time: 2.615 ms

Este método tiene un costo inicial para crear una nueva columna y llenar esa nueva columna con valores aleatorios (0.0-1.0), pero es un costo único. En este ejemplo, para 1 millón de filas, tomó casi 8,5 segundos.

Intentemos observar si nuestros resultados son reproducibles consultando 100 filas con nuestro nuevo método:

random=# SELECT * FROM ts_test ORDER BY randomcolumn LIMIT 100;
 -------+---------------+----------------------
 13522  | Record #13522  | 6.4261257648468e-08
 671584 | Record #671584 | 6.4261257648468e-07
 714012 | Record #714012 | 1.95764005184174e-06
 162016 | Record #162016 | 3.44449654221535e-06
 106867 | Record #106867 | 3.66196036338806e-06
 865669 | Record #865669 | 3.96883115172386e-06
 927    | Record #927    | 4.65428456664085e-06
 526017 | Record #526017 | 4.65987250208855e-06
 98338  | Record #98338  | 4.91179525852203e-06
 769625 | Record #769625 | 4.91319224238396e-06
 ...
 462484 | Record #462484 | 9.83504578471184e-05
 (100 rows)

Cuando ejecutamos la consulta anterior, en general obtenemos el mismo conjunto de resultados, pero esto no está garantizado porque usamos random() función para llenar randomcolumn valores y, en este caso, más de una columna puede tener el mismo valor. Para asegurarnos de obtener los mismos resultados cada vez que se ejecuta, debemos mejorar nuestra consulta agregando la columna ID a ORDER BY cláusula, de esta manera aseguramos que ORDER BY La cláusula especifica un conjunto único de filas, porque la columna id tiene un índice de clave principal.

Ahora ejecutemos la consulta mejorada a continuación:

random=# SELECT * FROM ts_test ORDER BY randomcolumn, id LIMIT 100;
 id | title | randomcolumn
--------+----------------+----------------------
 13522  | Record #13522  | 6.4261257648468e-08
 671584 | Record #671584 | 6.4261257648468e-07
 714012 | Record #714012 | 1.95764005184174e-06
 162016 | Record #162016 | 3.44449654221535e-06
 106867 | Record #106867 | 3.66196036338806e-06
 865669 | Record #865669 | 3.96883115172386e-06
 927    | Record #927    | 4.65428456664085e-06
 526017 | Record #526017 | 4.65987250208855e-06
 98338  | Record #98338  | 4.91179525852203e-06
 769625 | Record #769625 | 4.91319224238396e-06 
 ...
 462484 | Record #462484 | 9.83504578471184e-05
 (100 rows)

Ahora estamos seguros de que podemos obtener muestras aleatorias reproducibles usando este método.

Es hora de ver EXPLAIN ANALYZE resultados de nuestra consulta final:

random=# EXPLAIN ANALYZE SELECT * FROM ts_test ORDER BY randomcolumn, id LIMIT 100;
 QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Limit (cost=55571.28..55571.53 rows=100 width=26) (actual time=1951.811..1952.039 rows=100 loops=1)
 -> Sort (cost=55571.28..58071.28 rows=1000000 width=26) (actual time=1951.804..1951.891 rows=100 loops=1)
 Sort Key: randomcolumn, id
 Sort Method: top-N heapsort Memory: 32kB
 -> Seq Scan on ts_test (cost=0.00..17352.00 rows=1000000 width=26) (actual time=0.058..995.011 rows=1000000 loops=1)
 Planning time: 0.481 ms
 Execution time: 1952.215 ms
(7 rows)

Para comparar este método con TABLESAMPLE métodos, elijamos SYSTEM método con REPEATABLE opción, ya que este método nos da resultados reproducibles.

TABLESAMPLE SYSTEM el método básicamente hace un muestreo a nivel de bloque/página; lee y devuelve páginas aleatorias del disco. Por lo tanto, proporciona aleatoriedad a nivel de página en lugar de a nivel de tupla. Es por eso que es difícil controlar exactamente el recuento de filas recuperadas. Si no hay páginas seleccionadas, es posible que no obtengamos ningún resultado. El muestreo a nivel de página también es propenso al efecto de agrupación.

TABLESAMPLE La SINTAXIS es la misma para BERNOULLI y SYSTEM métodos, especificaremos el porcentaje como lo hicimos en BERNOULLI método.

Recordatorio rápido: SINTAXIS

SELECT select_expression
FROM table_name
TABLESAMPLE sampling_method ( argument [, ...] ) [ REPEATABLE ( seed ) ]
...

Para recuperar aproximadamente 100 filas, necesitamos solicitar 100 * 100 / 1 000 000 =0,01 % de las filas de la tabla. Entonces nuestro porcentaje será 0.01.

Antes de comenzar a consultar, recordemos cómo REPEATABLE obras. Básicamente, elegiremos un parámetro semilla y obtendremos los mismos resultados cada vez que usemos la misma semilla con la consulta anterior. Si proporcionamos una semilla diferente, la consulta producirá un conjunto de resultados bastante diferente.

Intentemos ver los resultados con consultas.

random=# Select * from ts_test tablesample system (0.01) repeatable (60);
 id | title | randomcolumn
--------+----------------+---------------------
 659598 | Record #659598 | 0.964113113470376
 659599 | Record #659599 | 0.531714483164251
 659600 | Record #659600 | 0.477636905387044
 659601 | Record #659601 | 0.861940925940871
 659602 | Record #659602 | 0.545977566856891
 659603 | Record #659603 | 0.326795583125204
 659604 | Record #659604 | 0.469275736715645
 659605 | Record #659605 | 0.734953186474741
 659606 | Record #659606 | 0.41613544523716
 ...
 659732 | Record #659732 | 0.893704727292061
 659733 | Record #659733 | 0.847225237637758
 (136 rows)

Obtenemos 136 filas, como puede considerar, este número depende de cuántas filas se almacenen en una sola página.

Ahora intentemos ejecutar la misma consulta con el mismo número inicial:

random=# Select * from ts_test tablesample system (0.01) repeatable (60);
 id | title | randomcolumn
--------+----------------+---------------------
 659598 | Record #659598 | 0.964113113470376
 659599 | Record #659599 | 0.531714483164251
 659600 | Record #659600 | 0.477636905387044
 659601 | Record #659601 | 0.861940925940871
 659602 | Record #659602 | 0.545977566856891
 659603 | Record #659603 | 0.326795583125204
 659604 | Record #659604 | 0.469275736715645
 659605 | Record #659605 | 0.734953186474741
 659606 | Record #659606 | 0.41613544523716 
 ...
 659732 | Record #659732 | 0.893704727292061
 659733 | Record #659733 | 0.847225237637758
 (136 rows)

Obtenemos el mismo conjunto de resultados gracias a REPEATABLE opción. Ahora podemos comparar TABLESAMPLE SYSTEM método con método de columna aleatoria mirando el EXPLAIN ANALYZE salida.

random=# EXPLAIN ANALYZE SELECT * FROM ts_test TABLESAMPLE SYSTEM (0.01) REPEATABLE (60);
 QUERY PLAN
---------------------------------------------------------------------------------------------------------
 Sample Scan on ts_test (cost=0.00..5.00 rows=100 width=26) (actual time=0.091..0.230 rows=136 loops=1)
 Sampling: system ('0.01'::real) REPEATABLE ('60'::double precision)
 Planning time: 0.323 ms
 Execution time: 0.398 ms
(4 rows)

SYSTEM El método utiliza un escaneo de muestra y es extremadamente rápido. El único inconveniente de este método es que no podemos garantizar que el porcentaje proporcionado resulte en el número esperado de filas.

Conclusión

En esta publicación de blog, comparamos TABLESAMPLE estándar (SYSTEM , BERNOULLI ) y tsm_system_rows módulo con los métodos aleatorios más antiguos.

Aquí puede revisar los hallazgos rápidamente:

  • ORDER BY random() es lento debido a la clasificación
  • random() <= 0.1 es similar a BERNOULLI método
  • Agregar una columna con un valor aleatorio requiere un trabajo inicial y puede provocar problemas de rendimiento
  • SYSTEM es rápido pero es difícil controlar el número de filas
  • tsm_system_rows es rápido y puede controlar el número de filas

Como resultado tsm_system_rows supera a cualquier otro método para seleccionar solo unas pocas filas aleatorias.

Pero el verdadero ganador es definitivamente TABLESAMPLE sí mismo. Gracias a su extensibilidad, las extensiones personalizadas (es decir, tsm_system_rows , tsm_system_time ) se puede desarrollar usando TABLESAMPLE funciones de método.

Nota del desarrollador: Puede encontrar más información sobre cómo escribir métodos de muestreo personalizados en el capítulo Escritura de un método de muestreo de tablas de la documentación de PostgreSQL.

Nota para el futuro: Hablaremos sobre AXLE Project y la extensión tsm_system_time en nuestro próximo TABLESAMPLE entrada de blog.