2009-08-21 8 views
29

Essayez simplement Postgresql pour la première fois depuis MySQL. Dans notre application Rails, nous avons deux ou trois endroits avec SQL comme ceci:Simulation de l'ordre de MySQL par FIELD() dans Postgresql

SELECT * FROM `currency_codes` ORDER BY FIELD(code, 'GBP', 'EUR', 'BBD', 'AUD', 'CAD', 'USD') DESC, name ASC 

Il n'a pas fallu longtemps pour découvrir que cela ne soit pas pris en charge/autorisé dans Postgresql.

Est-ce que quelqu'un sait comment simuler ce comportement dans Postgres ou devons-nous tirer pour trier dans le code?

Merci

Peer

+3

Il peut être utile d'expliquer ce que vous voulez réaliser. Aussi difficile à imaginer que ce soit - tout le monde ne connaît pas MySQL :) –

+0

Excellent point depesz! Fondamentalement, un ordre de tri personnalisé est ce que nous recherchons. La fonction FIELD vous permet de créer un ensemble personnalisé pour faire votre tri avec. –

Répondre

48

Ah, gahooa était si proche:

SELECT * FROM currency_codes 
    ORDER BY 
    CASE 
    WHEN code='USD' THEN 1 
    WHEN code='CAD' THEN 2 
    WHEN code='AUD' THEN 3 
    WHEN code='BBD' THEN 4 
    WHEN code='EUR' THEN 5 
    WHEN code='GBP' THEN 6 
    ELSE 7 
    END,name; 
+1

Oups !! un peu de dyslexie en fin de soirée ... Vous avez mon vote! – gahooa

+4

Cette méthode ne fonctionne pas lorsque vous utilisez DISTINCT. D'autres idées pour ce scénario? – Corey

+2

Je ne sais pas pourquoi cela fonctionne, mais j'ai trouvé une alternative. Si vous voulez que les résultats soient ordonnés par j, a, k, e, alors vous commandez par id = e, id = k, id = a, id = j'. – jakeonrails

1

Vous pouvez le faire ...

SELECT 
    ..., code 
FROM 
    tablename 
ORDER BY 
    CASE 
     WHEN code='GBP' THEN 1 
     WHEN code='EUR' THEN 2 
     WHEN code='BBD' THEN 3 
     ELSE 4 
    END 

Mais pourquoi êtes-vous hardcoding ceux-ci dans la requête - ne serait pas une table de support plus approprié?

-

Edit: flipped autour de commentaires selon

+0

@gahooa, je pense que vous avez le sens du "code" inversé - le code est l'abréviation de trois alpha, que l'OP souhaite trier d'une manière non-alpha. – pilcrow

+0

exactement droite pilcrow –

+0

Je voudrais pouvoir prendre le crédit pour pourquoi le SQL est la façon dont il est, nous travaillons sur un refactor, mais j'admets que je suis à la recherche de la solution rapide en ce moment –

11

Mise à jour, étoffant suggestion formidable par @Tometzky.

Cela devrait vous donner une fonction MySQL FIELD() -alike en pg 8.4:

-- SELECT FIELD(varnames, 'foo', 'bar', 'baz') 
CREATE FUNCTION field(anyelement, VARIADIC anyarray) RETURNS numeric AS $$ 
    SELECT 
    COALESCE(
    (SELECT i FROM generate_subscripts($2, 1) gs(i) 
     WHERE $2[i] = $1), 
    0); 
$$ LANGUAGE SQL STABLE 

Mea culpa, mais je ne peux pas vérifier ce qui précède sur 8.4 en ce moment; cependant, je peux travailler à rebours à un « moralement » version équivalente qui fonctionne sur l'instance 8.1 devant moi:

-- SELECT FIELD(varname, ARRAY['foo', 'bar', 'baz']) 
CREATE OR REPLACE FUNCTION field(anyelement, anyarray) RETURNS numeric AS $$ 
    SELECT 
    COALESCE((SELECT i 
       FROM generate_series(1, array_upper($2, 1)) gs(i) 
       WHERE $2[i] = $1), 
      0); 
$$ LANGUAGE SQL STABLE 

Plus gauchement, vous pouvez toujours portably utiliser un (peut-être dérivée) table des classements de code monétaire, comme ceci:

pg=> select cc.* from currency_codes cc 
    left join 
     (select 'GBP' as code, 0 as rank union all 
     select 'EUR', 1 union all 
     select 'BBD', 2 union all 
     select 'AUD', 3 union all 
     select 'CAD', 4 union all 
     select 'USD', 5) cc_weights 
    on cc.code = cc_weights.code 
    order by rank desc, name asc; 
code |   name 
------+--------------------------- 
USD | USA bits 
CAD | Canadian maple tokens 
AUD | Australian diwallarangoos 
BBD | Barbadian tridents 
EUR | Euro chits 
GBP | British haypennies 
(6 rows) 
1

Si vous exécutez ce souvent, ajouter une nouvelle colonne et un déclencheur de pré-insertion/mise à jour. Ensuite, vous définissez la valeur dans la nouvelle colonne en fonction de ce déclencheur et ordre par ce champ. Vous pouvez même ajouter un index sur ce champ.

+0

Les déclencheurs sont mauvais, mmkay? Évitez si possible! –

+0

@WillSheppard C'est faux. Ils ne peuvent pas être utilisés sans cervelle et souvent, mais vous ne pouvez pas dire "Les déclencheurs sont mauvais" – jirigracik

10

C'est je pense que la façon la plus simple:

create temporary table test (id serial, field text); 
insert into test(field) values 
    ('GBP'), ('EUR'), ('BBD'), ('AUD'), ('CAD'), ('USD'), 
    ('GBP'), ('EUR'), ('BBD'), ('AUD'), ('CAD'), ('USD'); 
select * from test 
order by field!='GBP', field!='EUR', field!='BBD', 
    field!='AUD', field!='CAD', field!='USD'; 
id | field 
----+------- 
    1 | GBP 
    7 | GBP 
    2 | EUR 
    8 | EUR 
    3 | BBD 
    9 | BBD 
    4 | AUD 
10 | AUD 
    5 | CAD 
11 | CAD 
    6 | USD 
12 | USD 
(12 rows) 

Dans PostgreSQL 8.4 vous pouvez également utiliser une function with variable number of arguments (fonction variadique) à la fonction de port field.

+0

+1 pour l'ordre par bang, et pour la suggestion 'VARIADIC', que je vais essayer de mettre en œuvre. – pilcrow

3

En fait, la version pour postgres 8.1 est un autre avantage. Lorsque vous appelez une fonction postgres, vous ne pouvez pas lui transmettre plus de 100 paramètres, donc votre commande peut être effectuée au maximum sur 99 éléments.

L'utilisation de la fonction en utilisant un tableau comme deuxième argument au lieu d'avoir un argument variadique supprime simplement cette limite.

2

Il vous suffit de définir la fonction FIELD et de l'utiliser. C'est assez facile à mettre en œuvre. Ce qui suit devrait fonctionner en 8.4, comme il l'a unnest et fonctions de la fenêtre comme row_number:

CREATE OR REPLACE FUNCTION field(text, VARIADIC text[]) RETURNS bigint AS $$ 
SELECT n FROM (
    SELECT row_number() OVER() AS n, x FROM unnest($2) x 
) numbered WHERE numbered.x = $1; 
$$ LANGUAGE 'SQL' IMMUTABLE STRICT; 

Vous pouvez également définir une autre copie avec la signature:

CREATE OR REPLACE FUNCTION field(anyelement, VARIADIC anyarray) RETURNS bigint AS $$ 

et le même corps si vous voulez soutenir field() pour tout type de données.

+0

Je pense que c'est la meilleure réponse, mais quand j'essaye de créer la fonction, pgadmin dit - "incompatibilité de type de retour dans la fonction déclarée pour retourner bigint, la déclaration finale de la fonction doit être sélectionner/insérer" idées? – chrismarx

+0

@chrismarx Pg version? 'Select version()' –

+0

aussi unnest ne semble pas être une fonction enregistrée, "PostgreSQL 9.3.4 sur x86_64-apple-darwin13.1.0, compilé par Apple LLVM version 5.1 (clang-503.0.38) (basé sur LLVM 3.4svn), 64 bits " – chrismarx

16

tri dans MySQL:

> ids = [11,31,29] 
=> [11, 31, 29] 
> User.where(id: ids).order("field(id, #{ids.join(',')})") 

dans Postgres:

def self.order_by_ids(ids) 
    order_by = ["CASE"] 
    ids.each_with_index do |id, index| 
    order_by << "WHEN id='#{id}' THEN #{index}" 
    end 
    order_by << "END" 
    order(order_by.join(" ")) 
end 

User.where(id: [3,2,1]).order_by_ids([3,2,1]).map(&:id) 
#=> [3,2,1] 
+1

aime cette idée! – mklb

+1

Bonne idée @ilgam –

+0

Cela fonctionne très bien pour moi! – armchairdj

0

Comme je l'ai répondu here, je vient de sortir un bijou (order_as_specified) qui vous permet de faire la commande SQL natif comme ceci:

CurrencyCode.order_as_specified(code: ['GBP', 'EUR', 'BBD', 'AUD', 'CAD', 'USD']) 

Il retourne une relation ActiveRecord, et peut donc être enchaîné h autres méthodes, et cela a fonctionné avec tous les SGBDR que j'ai testés.

2

Créer une migration avec cette fonction

CREATE OR REPLACE FUNCTION field(anyelement, VARIADIC anyarray) RETURNS bigint AS $$ 
    SELECT n FROM (
    SELECT row_number() OVER() AS n, x FROM unnest($2) x) 
     numbered WHERE numbered.x = $1; 
$$ LANGUAGE SQL IMMUTABLE STRICT; 

Alors faites juste ceci

sequence = [2,4,1,5] 
Model.order("field(id,#{sequence.join(',')})") 

le tour est joué!