31

For example, let's use some simple data set

+---------+------+------+------------+
| name    | age  | sex  | position   |
+---------+------+------+------------+
| Antony  |   34 | M    | programmer |
| Sally   |   30 | F    | manager    |
| Matthew |   28 | M    | designer   |
+---------+------+------+------------+

What we are trying to get is array organized this way

Array
(
  [Antony] => Array
    (
      [age] => 34
      [sex] => M
      [position] => programmer
    )

  [Sally] => Array
    (
      [age] => 30
      [sex] => F
      [position] => manager
    )

  [Matthew] => Array
    (
      [age] => 28
      [sex] => M
      [position] => designer
    )
)

As a rough approximation we can use

$pdo->query('SELECT * FROM employee')->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_ASSOC);

But as result we have unnecessary nesting level

Array
(
    [Antony] => Array
        (
            [0] => Array
                (
                    [age] => 34
                    [sex] => M
                    [position] => programmer
                )

        )

    [Sally] => Array
        (
            [0] => Array
                (
                    [age] => 30
                    [sex] => F
                    [position] => manager
                )

        )

    [Matthew] => Array
        (
            [0] => Array
                (
                    [age] => 28
                    [sex] => M
                    [position] => designer
                )

        )

)

I tried to get rid of this unnecessary nesting level by using callback function

$stmt->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_ASSOC|PDO::FETCH_FUNC, 'current');

But for some reasons It passes not

Array
  (
   [0] => Array
    (
        [age] => 34
        [sex] => M
        [position] => programmer
    )
  ) 

but just a bunch of scalars 34, 'M', 'programmer' to callback function :(

You can see it using such function as callback

function what_do_you_pass_me() {

  $numargs = func_num_args();
  $arg_list = func_get_args();
  for ($i = 0; $i < $numargs; $i++) {
    echo "Argument $i is: " . $arg_list[$i] . "\n";
  };
  echo "\n\n";
};

So is there a way to get desired resultset using PDO::FETCH_* modes without using array_map('current', $result) after fetching results ?

HongKilDong
  • 1,276
  • 3
  • 16
  • 23

8 Answers8

59

It's quite old topic, but I found very easy solution:

->fetchAll(\PDO::FETCH_GROUP|\PDO::FETCH_UNIQUE)

First col will be set as key, rest will be set as value.

No need to walk over the array or use array_map.

imclickingmaniac
  • 1,467
  • 1
  • 16
  • 28
  • 4
    What are the slashes ahead of the PDO:: here for? – Gideon Mar 31 '14 at 20:34
  • 5
    @Gideon Short answer is to avoid any [Namespace](http://www.php.net/manual/en/language.namespaces.php) conflicts. The \ in front of a constant like that will use the global namespace. In this case where we're not sure where the code will end up, it's the safe thing to write with a \ in front of the constants in case the code is put into a class within a Namespace other than the global one. Common practice on Stack Overflow and other places is to leave the \ out as it depends on the end implementation. – Jeremy Zerr Mar 31 '14 at 20:44
  • 1
    this should be the correct answer - the argument to fetchAll can also be OR'd with PDO::FETCH_OBJ to get an array of objects – Stephen Apr 01 '14 at 11:57
  • 1
    what if I want to group by the second column? – oddtwelve Apr 02 '14 at 11:25
  • 1
    If I understand correct just change the fetch order of selected elements (SELECT a,b,c to SELECT c,a,b). – imclickingmaniac Apr 03 '14 at 11:57
  • 1
    can also be OR'd to use FETCH_ASSOC if you want associative array instead of objects. Either way, seems like a nice solution. – ldg Apr 14 '14 at 14:58
  • 5
    What does including FETCH_GROUP achieve? Surely using just FETCH_UNIQUE alone will achieve the desired result - this is the whole point of FETCH_UNIQUE. – Gannet Sep 14 '15 at 02:09
  • 1
    What if I want to select all fields using `SELECT * FROM` and want to group by a specific field which is not the first column. How can I achieve this? – Елин Й. Nov 03 '16 at 08:40
  • @Gannet old comment, but... `FETCH_GROUP` is what's indexing the returned results by the `name` column... that's not the function of `FETCH_UNIQUE` at all – Brad Kent Nov 22 '17 at 22:00
  • 4
    @BradKent I suggest you test this out. `var_dump((PDO::FETCH_GROUP|PDO::FETCH_UNIQUE) === PDO::FETCH_UNIQUE); // true` – Gannet Nov 23 '17 at 23:03
  • 2
    @BradKent really? Surely you have a proof for such a statement? – Your Common Sense Feb 21 '18 at 10:34
19

The accepted answer is essentially a cargo cult code, that does its job only by accident, but makes no sense by itself.

PDO::FETCH_GROUP and PDO::FETCH_UNIQUE are mutual exclusive fetch modes, that cannot be used together. Only one of them would work. If you combine them, the latter takes over and \PDO::FETCH_GROUP|\PDO::FETCH_UNIQUE is actually just PDO::FETCH_UNIQUE.

Besides, the question is ambiguous by itself: the OP wants the resulting array to be indexed by the unique field, whereas they called it grouping, which raised a controversy in the answers as well.

So to make it straight:

  • to index the resulting array with unique values (when you want it to be indexed by the employee's name, given they are unique), the fetch mode must be PDO::FETCH_UNIQUE:

      $pdo->query('SELECT name, e.* FROM employee e')->fetchAll(PDO::FETCH_UNIQUE);
    
  • to group the results (when you want to group employees by department, for example), the fetch mode must be PDO::FETCH_GROUP:

      $pdo->query('SELECT dept_id, e.* FROM employee e')->fetchAll(PDO::FETCH_GROUP);
    

in both cases the field to be used as the first level array index, must be listed first in the SELECT field list.

A note on the PDO::FETCH_ASSOC. Given that preferred fetch mode is best to be set once for all in the PDO constructor, most of time it can be omitted here.

Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
13

Key assoc array

PDO::FETCH_GROUP|PDO::FETCH_UNIQUE|PDO::FETCH_ASSOC
Mariusz Jamro
  • 30,615
  • 24
  • 120
  • 162
Xakki
  • 172
  • 1
  • 7
  • 3
    **Explanation:** `FETCH_ASSOC` gets fields as `column_name => value`, `GROUP` takes 1st column and makes it the main key for the result array, however it will still be an array of single-item-arrays ***(assuming you have only 1 item per main key in the result set)*** - `UNIQUE` removes the unwanted level, basically saying "Hey dude, there is only 1 item per main key - so tidy up! :)". – jave.web Aug 04 '17 at 11:25
13

to reduce a unnecessary nesting array level:

$res = $pdo->query('SELECT * FROM employee')->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_ASSOC);
$res = array_map('reset', $res);
Charles
  • 50,943
  • 13
  • 104
  • 142
Stefan
  • 147
  • 2
  • 4
    It's not safe to call `fetchAll` from the result of `query` like that, as `query` can return false. This will result in a fatal error as PHP tries to call a method on a non-object. – Charles Sep 25 '13 at 18:03
3

This answer is out of date, please see this other answer instead.


It looks like there's no way to do this as part of fetchAll.

Your best bet is going to be creating a class that extends PDO, adding a utility method to it.

public function queryKeyedAssoc($query, $params, $key) {
    $sth = $this->prepare($query);
    $sth->execute($params);
    $res = array();
    while($row = $sth->fetch(PDO::FETCH_ASSOC))
        $res[ $row[$key] ] = $row;
    return $res;
}
Community
  • 1
  • 1
Charles
  • 50,943
  • 13
  • 104
  • 142
3

It doesn't look like anyone has mentioned this variation, so, for the benefit of future Googlers:

Combine the \PDO::FETCH_GROUP and \PDO::FETCH_COLUMN flags. This vastly simplified my SQL statement and returned the exact result set I wanted. It's fast, too.

$this->database->query('SELECT t.fk, t.id FROM my_table t ORDER BY t.fk ASC, t.id ASC')
  ->fetchAll(\PDO::FETCH_GROUP|\PDO::FETCH_COLUMN);

Where t.fk has a one-to-many relationship with t.id.

I didn't have to concern myself with a GROUP BY statement or MySQL's finicky handling of grouping on multiple fields. Best of all, I received results in the form of:

[
  foreign_key_1 => [
    0 => 11111,
    1 => 22222,
    2 => 33333,
  ],
  foreign_key_2 => [
    0 => 44444,
    1 => 55555,
    2 => 66666,
  ],
  foreign_key_3 => [
    0 => 77777,
    1 => 88888,
    2 => 99999,
  ],
];

Rather than:

[      
  foreign_key_1 => [
    0 => [
      id => 11111,
    ],
    1 => [
      id => 22222,
    ],
    2 => [
      id => 33333,
    ],
  ],
  foreign_key_2 => [
    0 => [
      id => 44444,
    ],
    1 => [
      id => 55555,
    ],
    2 => [
      id => 66666,
    ],
  ],
  foreign_key_3 => [
    0 => [
      id => 77777,
    ],
    1 => [
      id => 88888,
    ],
    2 => [
      id => 99999,
    ],
  ],

];

Hope it helps someone out there!


For reference: https://phpdelusions.net/pdo/fetch_modes

Beau
  • 201
  • 3
  • 4
  • Your example is a bit confusing, as it shows only one group and looks like the result of just PDO::FETCH_COLUMN alone. I would advise to add another group just to show how it works – Your Common Sense Apr 23 '20 at 17:13
  • I appreciate your feedback, @YourCommonSense. I've updated the answer with more illustrative code examples. – Beau Apr 23 '20 at 18:02
2

We can make Charles' solution a little nicer by extending the statement class instead:

class MyPdo extends PDO {
    function __construct($host, $database_name, $username, $password, $options=array()) {
        $options = self::merge(array(
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_STATEMENT_CLASS => array('PdoPlusStatement', array()),
            PDO::ATTR_EMULATE_PREPARES => true,
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
        ), $options);
        $dsn = "mysql:host=$host;dbname=$database_name;charset=utf8";
        parent::__construct($dsn, $username, $password, $options);
    }
}

class PdoPlusStatement extends PDOStatement {
    protected function __construct() {}

    /**
     * @param array|mixed $input_parameters An array of values with as many elements as there are bound parameters in the SQL statement being executed, or one or more non-array arguments to be matched with sequential parameter markers.
     * @throws PDOException
     * @return PdoPlusStatement
     */
    public function execute($input_parameters=null) {
        $args = func_get_args();
        $argc = func_num_args();
        if($argc===0) {
            parent::execute();
        } else {
            if($argc===1 && is_array($args[0])) {
                $args = $args[0];
            }
            parent::execute($args);
        }
        return $this;
    }

    /**
     * Returns an array containing all of the remaining rows in the result set
     * @return array An associative array using the first column as the key, and the remainder as associative values
     */
    public function fetchKeyAssoc() {
        return array_map('reset', $this->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_ASSOC));
    }
}

Usage:

$users = $pcs->query("SELECT name, user_id, discipline_id FROM wx_user")->fetchKeyAssoc();
Charles
  • 50,943
  • 13
  • 104
  • 142
mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • @YourCommonSense Yep. You're right. I couldn't find the right combination of PDO fetch flags that would do this when I first wrote this. – mpen Feb 21 '18 at 20:18
0

Not sure why no one has posted the following solution, but it works perfectly for me:

PDO::FETCH_UNIQUE | PDO::FETCH_ASSOC

So, changing your statement to:

$pdo->query('SELECT * FROM employee')->fetchAll(PDO::FETCH_UNIQUE|PDO::FETCH_ASSOC);

should be exactly what you want.

kojow7
  • 10,308
  • 17
  • 80
  • 135
  • It would work but it contradicts with the wording in the OP as it does not group the results. Besides, PDO::FETCH_ASSOC is superfluous. – Your Common Sense Feb 21 '18 at 08:41
  • @YourCommonSense I believe that you are incorrect on both counts. 1) PDO::FETCH_UNIQUE does group the results. 2) PDO::FETCH_ASSOC has the benefit of eliminating the numeric indexes so you are only left with the associative ones. Based on the OP this is the exact output that they were looking for. – kojow7 Feb 21 '18 at 17:15
  • 1
    I believe you don't quite understand what does "group" mean. – Your Common Sense Feb 22 '18 at 08:36