F.32. pg_stat_statements — registrar estadísticas de planificación y ejecución de SQL #

F.32.1. La vista pg_stat_statements
F.32.2. La vista pg_stat_statements_info
F.32.3. Funciones
F.32.4. Parámetros de configuración
F.32.5. Ejemplo de salida
F.32.6. Autores

El módulo pg_stat_statements proporciona un medio para registrar las estadísticas de planificación y ejecución de todas las sentencias SQL ejecutadas por un servidor.

El módulo debe cargarse añadiendo pg_stat_statements a shared_preload_libraries en postgresql.conf, ya que requiere memoria compartida adicional. Esto significa que se necesita reiniciar el servidor para añadir o eliminar el módulo. Además, el cálculo del identificador de consulta debe estar habilitado para que el módulo esté activo, lo cual se hace automáticamente si compute_query_id está configurado en auto u on, o si se carga cualquier módulo de terceros que calcule identificadores de consultas.

Cuando pg_stat_statements está activo, realiza el seguimiento de las estadísticas en todas las bases de datos del servidor. Para acceder y manipular estas estadísticas, el módulo proporciona las vistas pg_stat_statements y pg_stat_statements_info, y las funciones de utilidad pg_stat_statements_reset y pg_stat_statements. Estas no están disponibles globalmente, pero se pueden habilitar para una base de datos específica con CREATE EXTENSION pg_stat_statements.

F.32.1. La vista pg_stat_statements #

Las estadísticas recopiladas por el módulo están disponibles a través de una vista llamada pg_stat_statements. Esta vista contiene una fila para cada combinación distinta de ID de base de datos, ID de usuario, ID de consulta y si se trata de una sentencia de nivel superior o no (hasta el número máximo de sentencias distintas que el módulo puede registrar). Las columnas de la vista se muestran en la Table F.22.

Table F.22. Columnas de pg_stat_statements

Columna Tipo

Descripción

userid oid (hace referencia a pg_authid.oid)

OID del usuario que ejecutó la sentencia

dbid oid (hace referencia a pg_database.oid)

OID de la base de datos en la que se ejecutó la sentencia

toplevel bool

Verdadero si la consulta se ejecutó como una sentencia de nivel superior (siempre verdadero si pg_stat_statements.track está establecido en top)

queryid bigint

Código hash para identificar consultas normalizadas idénticas.

query text

Texto de una sentencia representativa

plans bigint

Número de veces que se planificó la sentencia (si pg_stat_statements.track_planning está habilitado; de lo contrario, cero)

total_plan_time double precision

Tiempo total dedicado a planificar la sentencia, en milisegundos (si pg_stat_statements.track_planning está habilitado; de lo contrario, cero)

min_plan_time double precision

Tiempo mínimo dedicado a planificar la sentencia, en milisegundos. Este campo será cero si pg_stat_statements.track_planning está deshabilitado, o si el contador se ha restablecido utilizando la función pg_stat_statements_reset con el parámetro minmax_only establecido en true y no se ha vuelto a planificar desde entonces.

max_plan_time double precision

Tiempo máximo dedicado a planificar la sentencia, en milisegundos. Este campo será cero si pg_stat_statements.track_planning está deshabilitado, o si el contador se ha restablecido utilizando la función pg_stat_statements_reset con el parámetro minmax_only establecido en true y no se ha vuelto a planificar desde entonces.

mean_plan_time double precision

Tiempo medio dedicado a planificar la sentencia, en milisegundos (si pg_stat_statements.track_planning está habilitado; de lo contrario, cero)

stddev_plan_time double precision

Desviación estándar de la población del tiempo dedicado a planificar la sentencia, en milisegundos (si pg_stat_statements.track_planning está habilitado; de lo contrario, cero)

calls bigint

Número de veces que se ejecutó la sentencia

total_exec_time double precision

Tiempo total dedicado a ejecutar la sentencia, en milisegundos

min_exec_time double precision

Tiempo mínimo dedicado a ejecutar la sentencia, en milisegundos. Este campo será cero hasta que esta sentencia se ejecute por primera vez después de un restablecimiento realizado por la función pg_stat_statements_reset con el parámetro minmax_only establecido en true.

max_exec_time double precision

Tiempo máximo dedicado a ejecutar la sentencia, en milisegundos. Este campo será cero hasta que esta sentencia se ejecute por primera vez después de un restablecimiento realizado por la función pg_stat_statements_reset con el parámetro minmax_only establecido en true.

mean_exec_time double precision

Tiempo medio dedicado a ejecutar la sentencia, en milisegundos

stddev_exec_time double precision

Desviación estándar de la población del tiempo dedicado a ejecutar la sentencia, en milisegundos

rows bigint

Número total de filas recuperadas o afectadas por la sentencia

shared_blks_hit bigint

Número total de aciertos en la caché de bloques compartidos (shared block cache hits) por la sentencia

shared_blks_read bigint

Número total de bloques compartidos leídos por la sentencia

shared_blks_dirtied bigint

Número total de bloques compartidos marcados como sucios (dirtied) por la sentencia

shared_blks_written bigint

Número total de bloques compartidos escritos por la sentencia

local_blks_hit bigint

Número total de aciertos en la caché de bloques locales por la sentencia

local_blks_read bigint

Número total de bloques locales leídos por la sentencia

local_blks_dirtied bigint

Número total de bloques locales marcados como sucios por la sentencia

local_blks_written bigint

Número total de bloques locales escritos por la sentencia

temp_blks_read bigint

Número total de bloques temporales leídos por la sentencia

temp_blks_written bigint

Número total de bloques temporales escritos por la sentencia

shared_blk_read_time double precision

Tiempo total que la sentencia dedicó a leer bloques compartidos, en milisegundos (si track_io_timing está habilitado; de lo contrario, cero)

shared_blk_write_time double precision

Tiempo total que la sentencia dedicó a escribir bloques compartidos, en milisegundos (si track_io_timing está habilitado; de lo contrario, cero)

local_blk_read_time double precision

Tiempo total que la sentencia dedicó a leer bloques locales, en milisegundos (si track_io_timing está habilitado; de lo contrario, cero)

local_blk_write_time double precision

Tiempo total que la sentencia dedicó a escribir bloques locales, en milisegundos (si track_io_timing está habilitado; de lo contrario, cero)

temp_blk_read_time double precision

Tiempo total que la sentencia dedicó a leer bloques de archivos temporales, en milisegundos (si track_io_timing está habilitado; de lo contrario, cero)

temp_blk_write_time double precision

Tiempo total que la sentencia dedicó a escribir bloques de archivos temporales, en milisegundos (si track_io_timing está habilitado; de lo contrario, cero)

wal_records bigint

Número total de registros WAL generados por la sentencia

wal_fpi bigint

Número total de imágenes de página completa WAL (WAL full page images) generadas por la sentencia

wal_bytes numeric

Cantidad total de WAL generado por la sentencia en bytes

wal_buffers_full bigint

Número de veces que los búferes WAL se llenaron por completo

jit_functions bigint

Número total de funciones JIT-compiladas por la sentencia

jit_generation_time double precision

Tiempo total dedicado por la sentencia a generar código JIT, en milisegundos

jit_inlining_count bigint

Número de veces que las funciones han sido integradas en línea (inlined)

jit_inlining_time double precision

Tiempo total dedicado por la sentencia a la integración en línea de funciones, en milisegundos

jit_optimization_count bigint

Número de veces que la sentencia ha sido optimizada

jit_optimization_time double precision

Tiempo total dedicado por la sentencia a la optimización, en milisegundos

jit_emission_count bigint

Número de veces que se ha emitido código

jit_emission_time double precision

Tiempo total dedicado por la sentencia a emitir código, en milisegundos

jit_deform_count bigint

Número total de funciones de deformación de tuplas (tuple deform functions) JIT-compiladas por la sentencia

jit_deform_time double precision

Tiempo total dedicado por la sentencia a la compilación JIT de funciones de deformación de tuplas, en milisegundos

parallel_workers_to_launch bigint

Número de trabajadores paralelos planificados para ser iniciados

parallel_workers_launched bigint

Número de trabajadores paralelos realmente iniciados

stats_since timestamp with time zone

Momento en el que comenzó la recopilación de estadísticas para esta sentencia

minmax_stats_since timestamp with time zone

Momento en el que comenzó la recopilación de estadísticas de mínimos/máximos para esta sentencia (campos min_plan_time, max_plan_time, min_exec_time y max_exec_time)


Por razones de seguridad, solo a los superusuarios y a los roles con privilegios del rol pg_read_all_stats se les permite ver el texto SQL y el queryid de las consultas ejecutadas por otros usuarios. Sin embargo, otros usuarios pueden ver las estadísticas si la vista ha sido instalada en su base de datos.

Las consultas planificables (es decir, SELECT, INSERT, UPDATE, DELETE y MERGE) y los comandos de utilidad se combinan en una única entrada de pg_stat_statements siempre que tengan estructuras de consulta idénticas según un cálculo hash interno. Típicamente, dos consultas se considerarán la misma para este propósito si son semánticamente equivalentes excepto por los valores de las constantes literales que aparecen en la consulta.

Note

Los siguientes detalles sobre el reemplazo de constantes y queryid solo se aplican cuando compute_query_id está habilitado. Si en su lugar utilizas un módulo externo para calcular el queryid, deberías consultar su documentación para obtener más detalles.

Cuando el valor de una constante se ha ignorado a efectos de hacer coincidir la consulta con otras consultas, la constante se reemplaza por un símbolo de parámetro, como $1, en la presentación de pg_stat_statements. El resto del texto de la consulta es el de la primera consulta que tuvo el valor hash queryid particular asociado con la entrada de pg_stat_statements.

Las consultas sobre las cuales se puede aplicar normalización pueden observarse con valores constantes en pg_stat_statements, especialmente cuando hay una alta tasa de desasignaciones de entradas (deallocations). Para reducir la probabilidad de que esto ocurra, considera aumentar pg_stat_statements.max. La vista pg_stat_statements_info, que se detalla más adelante en Section F.32.2, proporciona estadísticas sobre las desasignaciones de entradas.

En algunos casos, consultas con textos visiblemente diferentes pueden llegar a fusionarse en una única entrada de pg_stat_statements; como se explicó anteriormente, se espera que esto suceda para consultas semánticamente equivalentes. Además, si la única diferencia entre las consultas es el número de elementos en una lista de constantes, la lista se reducirá a un solo elemento pero se mostrará con un indicador de lista comentado:

=# SELECT pg_stat_statements_reset();
=# SELECT * FROM test WHERE a IN (1, 2, 3, 4, 5, 6, 7);
=# SELECT * FROM test WHERE a IN (1, 2, 3, 4, 5, 6, 7, 8);
=# SELECT query, calls FROM pg_stat_statements
   WHERE query LIKE 'SELECT%';
-[ RECORD 1 ]------------------------------
query | SELECT * FROM test WHERE a IN ($1 /*, ... */)
calls | 2

Además de estos casos, existe una pequeña probabilidad de que las colisiones de hash provoquen que consultas no relacionadas se fusionen en una sola entrada. (Sin embargo, esto no puede suceder para consultas pertenecientes a diferentes usuarios o bases de datos).

Dado que el valor hash queryid se calcula sobre la representación de las consultas posterior al análisis sintáctico (post-parse-analysis representation), también es posible lo contrario: consultas con textos idénticos pueden aparecer como entradas separadas si tienen significados diferentes como resultado de factores como configuraciones distintas de search_path.

Los consumidores de pg_stat_statements pueden querer usar el queryid (tal vez en combinación con dbid y userid) como un identificador más estable y confiable para cada entrada que su texto de consulta. Sin embargo, es importante entender que solo existen garantías limitadas sobre la estabilidad del valor hash del queryid. Dado que el identificador se deriva del árbol posterior al análisis sintáctico, su valor es una función de, entre otras cosas, los identificadores internos de objetos que aparecen en esta representación. Esto tiene algunas implicaciones contraintuitivas. Por ejemplo, pg_stat_statements considerará que dos consultas aparentemente idénticas son distintas si hacen referencia, por ejemplo, a una función que fue eliminada y recreada entre las ejecuciones de las dos consultas. Por el contrario, si una tabla se elimina y se vuelve a crear entre las ejecuciones de las consultas, dos consultas aparentemente idénticas pueden considerarse iguales. Sin embargo, si el alias de una tabla es diferente para consultas que por lo demás son similares, estas consultas se considerarán distintas. El proceso de hashing también es sensible a las diferencias en la arquitectura de la máquina y otras facetas de la plataforma. Además, no es seguro asumir que queryid se mantendrá estable entre versiones mayores de PostgreSQL.

Se puede esperar que dos servidores que participan en una replicación basada en la reproducción física de WAL tengan valores de queryid idénticos para la misma consulta. Sin embargo, los esquemas de replicación lógica no prometen mantener las réplicas idénticas en todos los detalles relevantes, por lo que el queryid no será un identificador útil para acumular costos a través de un conjunto de réplicas lógicas. En caso de duda, se recomienda realizar pruebas directas.

Consumidores de queryid generalmente pueden asumir que los valores son estables entre versiones menores de PostgreSQL, siempre que las instancias se ejecuten en la misma arquitectura de máquina y los detalles de los metadatos del catálogo coincidan. La compatibilidad solo se romperá entre versiones menores como último recurso.

Los símbolos de parámetros utilizados para reemplazar constantes en los textos de consulta representativos comienzan a partir del siguiente número después del parámetro $n más alto en el texto original de la consulta, o $1 si no había ninguno. Vale la pena señalar que en algunos casos puede haber símbolos de parámetros ocultos que afecten a esta numeración. Por ejemplo, PL/pgSQL utiliza símbolos de parámetros ocultos para insertar valores de variables locales de función en las consultas, de modo que una sentencia de PL/pgSQL como SELECT i + 1 INTO j tendría un texto representativo como SELECT i + $2.

Los textos representativos de las consultas se guardan en un archivo de disco externo y no consumen memoria compartida. Por lo tanto, incluso textos de consultas muy largos pueden almacenarse con éxito. Sin embargo, si se acumulan muchos textos de consultas largos, el archivo externo podría crecer de forma desmesurada. Como método de recuperación si eso sucede, pg_stat_statements puede optar por descartar los textos de las consultas, tras lo cual todas las entradas existentes en la vista pg_stat_statements mostrarán campos query nulos, aunque se conservan las estadísticas asociadas con cada queryid. Si esto sucede, considera reducir pg_stat_statements.max para evitar que vuelva a ocurrir.

No siempre se espera que plans y calls coincidan porque las estadísticas de planificación y ejecución se actualizan en sus respectivas fases finales, y solo para operaciones exitosas. Por ejemplo, si una sentencia se planifica con éxito pero falla durante la fase de ejecución, solo se actualizarán sus estadísticas de planificación. Si se omite la planificación porque se utiliza un plan almacenado en caché, solo se actualizarán sus estadísticas de ejecución.

F.32.2. La vista pg_stat_statements_info #

Las estadísticas del propio módulo pg_stat_statements se registran y están disponibles a través de una vista llamada pg_stat_statements_info. Esta vista contiene una única fila. Las columnas de la vista se muestran en la Table F.23.

Table F.23. Columnas de pg_stat_statements_info

Columna Tipo

Descripción

dealloc bigint

Número total de veces que se desasignaron (deallocated) entradas de pg_stat_statements correspondientes a las sentencias menos ejecutadas, debido a que se observaron más sentencias distintas que el límite establecido en pg_stat_statements.max.

stats_reset timestamp with time zone

Momento en el que se restablecieron por última vez todas las estadísticas en la vista pg_stat_statements.


F.32.3. Funciones #

pg_stat_statements_reset(userid Oid, dbid Oid, queryid bigint, minmax_only boolean) returns timestamp with time zone

pg_stat_statements_reset descarta las estadísticas recopiladas hasta el momento por pg_stat_statements que correspondan a los valores especificados de userid, dbid y queryid. Si alguno de los parámetros no se especifica, se utiliza el valor por defecto 0 (inválido) para cada uno de ellos y se restablecerán las estadísticas que coincidan con los demás parámetros. Si no se especifica ningún parámetro o si todos los parámetros especificados son 0 (inválidos), se descartarán todas las estadísticas. Si se descartan todas las estadísticas en la vista pg_stat_statements, también se restablecerán las estadísticas en la vista pg_stat_statements_info. Cuando minmax_only es true, solo se restablecerán los valores de los tiempos mínimos y máximos de planificación y ejecución (es decir, los campos min_plan_time, max_plan_time, min_exec_time y max_exec_time). El valor por defecto para el parámetro minmax_only es false. El momento del último restablecimiento de mínimos/máximos realizado se muestra en el campo minmax_stats_since de la vista pg_stat_statements. Esta función devuelve la hora del restablecimiento. Esta hora se guarda en el campo stats_reset de la vista pg_stat_statements_info o en el campo minmax_stats_since de la vista pg_stat_statements si el restablecimiento correspondiente se llevó a cabo realmente. Por defecto, esta función solo puede ser ejecutada por superusuarios. El acceso puede otorgarse a otros mediante el uso de GRANT.

pg_stat_statements(showtext boolean) returns setof record

La vista pg_stat_statements se define en términos de una función también llamada pg_stat_statements. Es posible que los clientes llamen a la función pg_stat_statements directamente y, especificando showtext := false, se omita el texto de la consulta (es decir, el argumento OUT que corresponde a la columna query de la vista devolverá nulos). Esta característica está pensada para dar soporte a herramientas externas que puedan querer evitar la sobrecarga de recuperar repetidamente textos de consultas de longitud indeterminada. En su lugar, estas herramientas pueden almacenar en caché ellas mismas el primer texto de consulta observado para cada entrada, ya que eso es todo lo que hace el propio pg_stat_statements, y luego recuperar los textos de las consultas solo cuando sea necesario. Dado que el servidor almacena los textos de las consultas en un archivo, este enfoque puede reducir las E/S físicas para el examen repetido de los datos de pg_stat_statements.

F.32.4. Parámetros de configuración #

pg_stat_statements.max (integer)

pg_stat_statements.max es el número máximo de sentencias registradas por el módulo (es decir, el número máximo de filas en la vista pg_stat_statements). Si se observan más sentencias distintas que ese número, se descarta la información sobre las sentencias menos ejecutadas. El número de veces que se descartó dicha información se puede ver en la vista pg_stat_statements_info. El valor por defecto es 5000. Este parámetro solo se puede establecer al iniciar el servidor.

pg_stat_statements.track (enum)

pg_stat_statements.track controla qué sentencias son contabilizadas por el módulo. Especifica top para realizar el seguimiento de las sentencias de nivel superior (aquellas emitidas directamente por los clientes), all para realizar también el seguimiento de las sentencias anidadas (como las sentencias invocadas dentro de funciones) o none para desactivar la recopilación de estadísticas de sentencias. El valor por defecto es top. Solo los superusuarios pueden cambiar esta configuración.

pg_stat_statements.track_utility (boolean)

pg_stat_statements.track_utility controla si el módulo realiza el seguimiento de los comandos de utilidad. Los comandos de utilidad son todos aquellos distintos de SELECT, INSERT, UPDATE, DELETE y MERGE. El valor por defecto es on. Solo los superusuarios pueden cambiar esta configuración.

pg_stat_statements.track_planning (boolean)

pg_stat_statements.track_planning controla si el módulo realiza el seguimiento de las operaciones de planificación y su duración. La habilitación de este parámetro puede implicar una penalización de rendimiento perceptible, especialmente cuando sentencias con estructura de consulta idéntica son ejecutadas por muchas conexiones concurrentes que compiten por actualizar un número pequeño de entradas de pg_stat_statements. El valor por defecto es off. Solo los superusuarios pueden cambiar esta configuración.

pg_stat_statements.save (boolean)

pg_stat_statements.save especifica si se deben guardar las estadísticas de las sentencias a través de los reinicios y apagados del servidor. Si está en off, las estadísticas no se guardan al apagar el servidor ni se vuelven a cargar al iniciarlo. El valor por defecto es on. Este parámetro solo se puede establecer en el archivo postgresql.conf o en la línea de comandos del servidor.

El módulo requiere memoria compartida adicional proporcional a pg_stat_statements.max. Ten en cuenta que esta memoria se consume siempre que el módulo se carga, incluso si pg_stat_statements.track se establece en none.

Estos parámetros deben establecerse en el archivo postgresql.conf. El uso típico podría ser:

# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'

compute_query_id = on
pg_stat_statements.max = 10000
pg_stat_statements.track = all

F.32.5. Ejemplo de salida #

bench=# SELECT pg_stat_statements_reset();

$ pgbench -i bench
$ pgbench -c10 -t300 bench

bench=# \x
bench=# SELECT query, calls, total_exec_time, rows, 100.0 * shared_blks_hit /
               nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
          FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;
-[ RECORD 1 ]---+--------------------------------------------------------------------
query           | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2
calls           | 3000
total_exec_time | 25565.855387
rows            | 3000
hit_percent     | 100.0000000000000000
-[ RECORD 2 ]---+--------------------------------------------------------------------
query           | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid = $2
calls           | 3000
total_exec_time | 20756.669379
rows            | 3000
hit_percent     | 100.0000000000000000
-[ RECORD 3 ]---+--------------------------------------------------------------------
query           | copy pgbench_accounts from stdin
calls           | 1
total_exec_time | 291.865911
rows            | 100000
hit_percent     | 100.0000000000000000
-[ RECORD 4 ]---+--------------------------------------------------------------------
query           | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid = $2
calls           | 3000
total_exec_time | 271.232977
rows            | 3000
hit_percent     | 98.8454011741682975
-[ RECORD 5 ]---+--------------------------------------------------------------------
query           | alter table pgbench_accounts add primary key (aid)
calls           | 1
total_exec_time | 160.588563
rows            | 0
hit_percent     | 100.0000000000000000


bench=# SELECT pg_stat_statements_reset(0,0,s.queryid) FROM pg_stat_statements AS s
            WHERE s.query = 'UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2';

bench=# SELECT query, calls, total_exec_time, rows, 100.0 * shared_blks_hit /
               nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
          FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;
-[ RECORD 1 ]---+--------------------------------------------------------------------
query           | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid = $2
calls           | 3000
total_exec_time | 20756.669379
rows            | 3000
hit_percent     | 100.0000000000000000
-[ RECORD 2 ]---+--------------------------------------------------------------------
query           | copy pgbench_accounts from stdin
calls           | 1
total_exec_time | 291.865911
rows            | 100000
hit_percent     | 100.0000000000000000
-[ RECORD 3 ]---+--------------------------------------------------------------------
query           | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid = $2
calls           | 3000
total_exec_time | 271.232977
rows            | 3000
hit_percent     | 98.8454011741682975
-[ RECORD 4 ]---+--------------------------------------------------------------------
query           | alter table pgbench_accounts add primary key (aid)
calls           | 1
total_exec_time | 160.588563
rows            | 0
hit_percent     | 100.0000000000000000
-[ RECORD 5 ]---+--------------------------------------------------------------------
query           | vacuum analyze pgbench_accounts
calls           | 1
total_exec_time | 136.448116
rows            | 0
hit_percent     | 99.9201915403032721

bench=# SELECT pg_stat_statements_reset(0,0,0);

bench=# SELECT query, calls, total_exec_time, rows, 100.0 * shared_blks_hit /
               nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
          FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;
-[ RECORD 1 ]---+-----------------------------------------------------------------------------
query           | SELECT pg_stat_statements_reset(0,0,0)
calls           | 1
total_exec_time | 0.189497
rows            | 1
hit_percent     |
-[ RECORD 2 ]---+-----------------------------------------------------------------------------
query           | SELECT query, calls, total_exec_time, rows, $1 * shared_blks_hit /          +
                |                nullif(shared_blks_hit + shared_blks_read, $2) AS hit_percent+
                |           FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT $3
calls           | 0
total_exec_time | 0
rows            | 0
hit_percent     |

F.32.6. Autores #

Takahiro Itagaki . Normalización de consultas añadida por Peter Geoghegan .