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 SYSTEM
y BERNOULLI
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ónrandom() <= 0.1
es similar aBERNOULLI
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 filastsm_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.