node-postgres: ¿cómo ejecutar la consulta "WHERE col IN ()"?


Estoy tratando de ejecutar una consulta como esta:

SELECT * FROM table WHERE id IN (1,2,3,4)

El problema es que la lista de ids que quiero filtrar no es constante y debe ser diferente en cada ejecución. También necesitaría escapar de los ID, porque podrían provenir de fuentes no confiables, aunque en realidad escaparía de cualquier cosa que vaya en una consulta independientemente de la confiabilidad de la fuente.

Node-postgres parece funcionar exclusivamente con parámetros enlazados: client.query('SELECT * FROM table WHERE id = $1', [ id ]); esto funcionará si tuviera un número conocido de valores (client.query('SELECT * FROM table WHERE id IN ($1, $2, $3)', [ id1, id2, id3 ])), pero no funcionará directamente con una matriz: client.query('SELECT * FROM table WHERE id IN ($1)', [ arrayOfIds ]), ya que no parece haber ningún manejo especial de los parámetros de la matriz.

Construir la plantilla de consulta dinámicamente de acuerdo con el número de elementos en la matriz y expandir la matriz ids en la matriz de parámetros de consulta (que en mi caso real también contiene otros parámetros además de la lista de ids) parece una carga irrazonable. La codificación rígida de la lista de ids en la plantilla de consulta tampoco parece viable, ya que node-postgres no proporciona ningún método de escape de valor.

Este parece un caso de uso muy común, por lo que mi conjetura es que en realidad estoy pasando por alto algo, y no que no sea posible usar el operador SQL común IN (values) con node-postgres.

Si alguien ha resuelto este problema de una manera más elegante que los que mencioné anteriormente, o si realmente me falta algo sobre node-postgres, por favor ayúdeme.

Author: lanzz, 2012-05-23

6 answers

Hemos visto esta pregunta antes en la lista de problemas de github. La forma correcta es generar dinámicamente su lista de parámetros basados en la matriz. Algo como esto:

var arr = [1, 2, "hello"];
var params = [];
for(var i = 1; i <= arr.length; i++) {
  params.push('$' + i);
}
var queryText = 'SELECT id FROM my_table WHERE something IN (' + params.join(',') + ')';
client.query(queryText, arr, function(err, cb) {
 ...
});

De esta manera se obtiene el escape con parámetros postgres.

 30
Author: brianc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-14 22:41:26

Parece que has estado cerca basado en tu comentario a la respuesta de @ebohlman . Puede usar WHERE id = ANY($1::int[]). PostgreSQL convertirá el array al tipo al que se castea el parámetro en $1::int[]. Así que aquí hay un ejemplo artificial que funciona para mí:

var ids = [1,3,4]; 

var q = client.query('SELECT Id FROM MyTable WHERE Id = ANY($1::int[])',[ids]);

q.on('row', function(row) {
  console.log(row);
})

// outputs: { id: 1 }
//          { id: 3 }
//          { id: 4 }
 50
Author: Pero P.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:54:53

La mejor solución que he encontrado ha sido usar la función ANY con la coerción de matriz de Postgres. Esto le permite hacer coincidir una columna con una matriz arbitraria de valores como si hubiera escrito col IN (v1, v2, v3). Este es el enfoque en la respuesta de pero pero aquí muestro que el desempeño de ANY es el mismo que IN.

Consulta

Su consulta debería tener el siguiente aspecto:

SELECT * FROM table WHERE id = ANY($1::int[])

Ese bit al final que dice $1::int[] se puede cambiar para que coincida con el tipo de su columna "id". Para ejemplo, si el tipo de sus IDs es uuid, escribiría $1::uuid[] para coaccionar el argumento a una matriz de UUID. Vea aquí la lista de tipos de datos Postgres.

Esto es más simple que escribir código para construir una cadena de consulta y es seguro contra inyecciones SQL.

Ejemplo

Con node-postgres, un ejemplo completo de JavaScript se ve como:

var pg = require('pg');

var client = new pg.Client('postgres://username:password@localhost/database');
client.connect(function(err) {
  if (err) {
    throw err;
  }

  var ids = [23, 65, 73, 99, 102];
  client.query(
    'SELECT * FROM table WHERE id = ANY($1::int[])',
    [ids],  // array of query arguments
    function(err, result) {
      console.log(result.rows);
    }
  );
});

Rendimiento

Una de las mejores maneras de entender el rendimiento de una consulta SQL es mirar cómo la base de datos lo procesa. La tabla de ejemplo tiene aproximadamente 400 filas y una clave primaria llamada "id" de tipo text.

EXPLAIN SELECT * FROM tests WHERE id = ANY('{"test-a", "test-b"}');
EXPLAIN SELECT * FROM tests WHERE id IN ('test-a', 'test-b');

En ambos casos, Postgres reportó el mismo plan de consulta:

Bitmap Heap Scan on tests  (cost=8.56..14.03 rows=2 width=79)
  Recheck Cond: (id = ANY ('{test-a,test-b}'::text[]))
  ->  Bitmap Index Scan on tests_pkey  (cost=0.00..8.56 rows=2 width=0)
        Index Cond: (id = ANY ('{test-a,test-b}'::text[]))

Es posible que vea un plan de consulta diferente dependiendo del tamaño de su tabla, donde hay un índice y su consulta. Pero para consultas como las anteriores, ANY y IN se procesan de la misma manera.

 12
Author: ide,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-30 23:19:45

Usando pg-promise , esto funciona bien a través del filtro CSV (valores separados por comas):

const values = [1, 2, 3, 4];

db.any('SELECT * FROM table WHERE id IN ($1:csv)', [values])
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.log(error);
    });

Y para abordar la preocupación sobre varios tipos de datos, :csv modifier serializa la matriz en csv, mientras convierte todos los valores en su formato PostgreSQL adecuado, de acuerdo con su tipo JavaScript, incluso soportando el Formato de Tipo personalizado .

Y si tiene valores de tipo mixto como este: const values = [1, 'two', null, true], todavía obtendrá los escapados correctamente SQL:

SELECT * FROM table WHERE id IN (1, 'two', null, true)

UPDATE

De v7.5.1, pg-promise comenzó a soportar :list como un alias intercambiable para el filtro :csv:

db.any('SELECT * FROM table WHERE id IN ($1:list)', [values])
 11
Author: vitaly-t,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-02 20:59:37

Otra posible solución es usar la función UNNEST así:

 var ids = [23, 65, 73, 99, 102];
 var strs = ['bar', 'tar', 'far']
 client.query(
   'SELECT * FROM table WHERE id IN(SELECT(UNNEST($1))',
    [ids],  // array of query arguments
    function(err, result) {
       console.log(result.rows);
    }
);
client.query(
   'SELECT * FROM table WHERE id IN(SELECT(UNNEST($1))',
    [strs],  // array of query arguments
    function(err, result) {
       console.log(result.rows);
    }
);

He utilizado esto en un procedimiento almacenado y funciona bien. Creo que debería funcionar también desde el código node-pg.

Puedes leer sobre la función UNNEST aquí.

 0
Author: Yaki Klein,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-26 12:08:52

Otra solución posible es, por ejemplo, para la API REST en el NODO JS:

var name = req.body;//Body is a objetc that has properties for example provinces
var databaseRB = "DATABASENAME"
var conStringRB = "postgres://"+username+":"+password+"@"+host+"/"+databaseRB; 

var filter_query = "SELECT row_to_json(fc) FROM ( SELECT 'FeatureCollection' As type, array_to_json(array_agg(f)) As features FROM (SELECT 'Feature' As type, ST_AsGeoJSON(lg.geom)::json As geometry, row_to_json((parameters) As properties FROM radiobases As lg WHERE lg.parameter= ANY($1) )As f) As fc";

var client = new pg.Client(conStringRB);
client.connect();
var query = client.query(new Query(filter_query,[name.provinces]));
query.on("row", function (row, result) {
  result.addRow(row);
});
query.on("end", function (result) {
 var data = result.rows[0].row_to_json
   res.json({
     title: "Express API",
     jsonData: data
     });
});

Tenga en cuenta que cualquier tipo de matriz se puede utilizar

 0
Author: C. Marca,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-28 17:14:13