DEV Community

Cover image for Add a JOIN to an Existing CodeIgniter Query Builder Select
Faizan
Faizan

Posted on Originally published at headsntail.com

Add a JOIN to an Existing CodeIgniter Query Builder Select

Can you add a second table to a SELECT query you've already started building in CodeIgniter? Yes. As long as the query hasn't been executed, you can call join() on the existing Query Builder, add columns with select(), and execute everything together using get().

The distinction is between an unfinished query and an executed query. CodeIgniter lets you extend the first, but you cannot retroactively add a SQL JOIN to results that have already been retrieved from MySQL.

Here's how to handle both cases, including the differences between CodeIgniter 3 and CodeIgniter 4.

How It Works: Adding a JOIN to an Existing SELECT

Imagine an application with two MySQL tables.

Table: orders

id customer_id total_amount status
101 1 250.00 paid
102 2 180.00 pending
103 3 420.00 paid

Table: customers

id name
1 Ahmed
2 Sara
3 Ali

Your application already selects paid orders, but now the dashboard also needs the customer's name.

The SQL you want is:

SELECT
    o.id,
    o.total_amount,
    o.status,
    c.name AS customer_name
FROM orders AS o
LEFT JOIN customers AS c
    ON c.id = o.customer_id
WHERE o.status = 'paid';
Enter fullscreen mode Exit fullscreen mode

Expected result:

id total_amount status customer_name
101 250.00 paid Ahmed
103 420.00 paid Ali

The JOIN connects orders.customer_id with customers.id.

The important part is that the JOIN belongs to the SQL query itself, not to the PHP array returned afterward.

1. Reproduce the problem in CodeIgniter 3

You might already have:

$this->db->select('o.id, o.total_amount, o.status');
$this->db->from('orders AS o');
$this->db->where('o.status', 'paid');
Enter fullscreen mode Exit fullscreen mode

At this point, no SQL query has run.

CodeIgniter is collecting the SELECT, FROM, and WHERE instructions.

You can still extend it.

2. Add the second table before calling get()

The fix is straightforward:

$this->db->join(
    'customers AS c',
    'c.id = o.customer_id',
    'left'
);

$this->db->select('c.name AS customer_name');

$query = $this->db->get();

$rows = $query->result_array();
Enter fullscreen mode Exit fullscreen mode

The existing SELECT and WHERE conditions remain part of the query.

Calling select() again adds another field rather than replacing the earlier fields.

Likewise, join() adds the JOIN clause to the query being assembled.

For clarity, here is the complete CodeIgniter 3 example as a controller.

Create application/controllers/Orders.php:

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Orders extends CI_Controller
{
    public function index()
    {
        $this->load->database();

        $this->db->select(
            'o.id, o.total_amount, o.status'
        );

        $this->db->from('orders AS o');

        $this->db->where('o.status', 'paid');

        $this->db->join(
            'customers AS c',
            'c.id = o.customer_id',
            'left'
        );

        $this->db->select(
            'c.name AS customer_name'
        );

        $query = $this->db->get();

        $rows = $query->result_array();

        $this->output
            ->set_content_type('application/json')
            ->set_output(json_encode($rows));
    }
}
Enter fullscreen mode Exit fullscreen mode

Assuming those tables and columns exist and your database is configured, opening /orders returns JSON containing the matching orders and customer names.

This uses CodeIgniter 3's documented Query Builder methods.

Reference: CodeIgniter 3 Query Builder documentation.

3. Apply the same fix in CodeIgniter 4

CodeIgniter 4 separates the database connection from the Query Builder instance.

Instead of building through $this->db, you normally create a builder using $db->table().

The equivalent code is:

<?php

$db = \Config\Database::connect();

$builder = $db->table('orders AS o');

$builder->select(
    'o.id, o.total_amount, o.status'
);

$builder->where('o.status', 'paid');

$builder->join(
    'customers AS c',
    'c.id = o.customer_id',
    'left'
);

$builder->select('c.name AS customer_name');

$query = $builder->get();

$rows = $query->getResultArray();
Enter fullscreen mode Exit fullscreen mode

Use this inside your existing CodeIgniter 4 controller or model, where the framework has already been initialized.

Notice the differences:

Operation CodeIgniter 3 CodeIgniter 4
Builder $this->db $db->table()
Add JOIN join() join()
Run SELECT get() get()
Array results result_array() getResultArray()
Preview SQL get_compiled_select() getCompiledSelect()

The underlying SQL concept is identical.

Reference: CodeIgniter 4 Query Builder documentation.

Where You Will Meet This in Real Applications

Adding customer details to existing reports

Suppose your reporting function already selects orders by status and date.

Later, the business asks to display customer information alongside each order.

Rather than running a separate customer query for every order, add one JOIN before executing the existing builder.

This usually produces simpler application code and avoids the N+1 query pattern.

Adding optional information with LEFT JOIN

Not every order necessarily has a matching customer record.

A LEFT JOIN retains the order even when the customer cannot be matched.

$builder->join(
    'customers AS c',
    'c.id = o.customer_id',
    'left'
);
Enter fullscreen mode Exit fullscreen mode

The unmatched customer columns will contain NULL.

If you use an INNER JOIN instead:

$builder->join(
    'customers AS c',
    'c.id = o.customer_id',
    'inner'
);
Enter fullscreen mode Exit fullscreen mode

Only orders with matching customers will appear.

This distinction matters for incomplete imports, historical records, and optional relationships.

Adding multiple related tables

A report may also need payment information.

In CodeIgniter 4, you can add another JOIN:

$builder->join(
    'payments AS p',
    'p.order_id = o.id',
    'left'
);

$builder->select(
    'p.id AS payment_id, p.amount AS payment_amount'
);
Enter fullscreen mode Exit fullscreen mode

This assumes the payments table contains those columns.

Be careful: if an order has several payments, that order may appear in multiple result rows.

That is normal SQL JOIN behavior, not a CodeIgniter bug.

If you need one row per order, consider aggregation, a suitable subquery, or a different reporting query.

Common Misunderstandings About CodeIgniter JOINs

"I already called get(). Can I add another JOIN?"

No, not to the result that was already executed.

This approach is incorrect:

$query = $this->db->get('orders');

$this->db->join(
    'customers',
    'customers.id = orders.customer_id'
);
Enter fullscreen mode Exit fullscreen mode

The first statement already fetched orders.

The later join() builds part of another query; it cannot change the previous result.

Fix: Move the JOIN before get(), or rebuild and execute the required SELECT.

If the pre-existing query is a raw SQL string rather than an unfinished builder, rebuild it through Query Builder or use a correctly constructed SQL query. You cannot call join() directly on a SQL string.

"Why does LEFT JOIN behave like INNER JOIN?"

Consider:

$builder->join(
    'customers AS c',
    'c.id = o.customer_id',
    'left'
);

$builder->where('c.name', 'Ahmed');
Enter fullscreen mode Exit fullscreen mode

The WHERE condition excludes rows without a matching customer because their c.name is NULL.

Consequently, unmatched orders disappear.

If you genuinely need to retain unmatched rows while filtering matching customers, reconsider whether the condition belongs in the JOIN's ON clause instead.

The correct approach depends on your desired results.

"Why am I getting ambiguous column errors?"

Both tables may contain a column named id.

This is ambiguous:

$builder->select('id, name');
Enter fullscreen mode Exit fullscreen mode

Prefer qualified columns:

$builder->select('o.id, c.name AS customer_name');
Enter fullscreen mode Exit fullscreen mode

Use table aliases consistently in SELECT, JOIN, WHERE, and ORDER BY clauses.

Also avoid SELECT * when joining tables with duplicate column names. Duplicate keys can overwrite each other when results are converted into associative arrays.

"Why does my second query behave differently?"

CodeIgniter's builder maintains state until it is reset or executed.

In CodeIgniter 4, use a new $db->table() builder for a separate query, or explicitly reset an existing builder when appropriate.

In CodeIgniter 3, ordinary execution through get() resets the query-building state.

Do not assume that a previously executed query still contains its original SELECT and JOIN clauses.

"Is Query Builder automatically safe from SQL injection?"

Query Builder provides value escaping and identifier protection for supported operations, but it does not make arbitrary SQL fragments safe.

Never concatenate untrusted request parameters into a JOIN condition.

Keep table names and relationship expressions under application control, and pass user-supplied values through supported parameterized or value-binding methods.

Debug the Generated SQL Before Running It

When a JOIN produces unexpected results, I prefer inspecting the generated SQL before modifying more PHP code.

In CodeIgniter 3:

$sql = $this->db->get_compiled_select('', FALSE);

echo $sql;
Enter fullscreen mode Exit fullscreen mode

The FALSE argument prevents the builder from being reset.

That allows you to inspect the query and then execute it:

$query = $this->db->get();
Enter fullscreen mode Exit fullscreen mode

In CodeIgniter 4:

$sql = $builder->getCompiledSelect(false);

echo $sql;
Enter fullscreen mode Exit fullscreen mode

Then:

$rows = $builder->get()->getResultArray();
Enter fullscreen mode Exit fullscreen mode

Use this for local debugging rather than displaying SQL publicly in production.

Check that the generated SQL includes the intended:

  • SELECT columns.
  • Primary table and aliases.
  • JOIN type.
  • ON relationship.
  • WHERE conditions.

If the SQL is correct but the output is wrong, investigate the underlying data and relationship cardinality.

When Adding a JOIN Matters—and When It Doesn't

A JOIN is useful when related data must be retrieved together, especially for dashboards, search results, and reports.

It is not automatically the best choice for every relationship.

For large datasets, inspect indexes and query execution plans. An index on a frequently joined foreign-key column, such as orders.customer_id, may help the database find matching records efficiently.

For one-to-many relationships, joining all records can substantially increase the number of returned rows. Sometimes a grouped query or separate targeted retrieval is more appropriate.

For an already executed query, a JOIN requires another database operation. PHP cannot retroactively modify the SQL that MySQL has completed.

The practical rule is simple: finish constructing the SELECT, add the JOIN and required columns, then execute the query once.

That pattern works across both CodeIgniter 3 and CodeIgniter 4, even though their builder initialization and result-handling methods differ.

If your query still returns incorrect results, inspect the generated SQL first. Most problems come from mismatched relationship columns, duplicate column names, incorrect JOIN types, or filtering conditions—not from the join() method itself.


Originally published at headsntail.com. Free tools for PHP, Laravel and cPanel work: headsntail.com/tools.

Top comments (0)