50

I have two tables, locations and sensors. Each entry in sensors has a foreign key pointing to locations. Using Sequelize, how do I get all entries from locations and total count of entries in sensors that are associated with each entry in locations?

Raw SQL:

SELECT 
    `locations`.*,
    COUNT(`sensors`.`id`) AS `sensorCount` 
FROM `locations` 
JOIN `sensors` ON `sensors`.`location`=`locations`.`id`;
GROUP BY `locations`.`id`;

Models:

module.exports = function(sequelize, DataTypes) {
    var Location = sequelize.define("Location", {
        id: {
            type: DataTypes.INTEGER.UNSIGNED,
            primaryKey: true
        },
        name: DataTypes.STRING(255)
    }, {
        classMethods: {
            associate: function(models) {
                Location.hasMany(models.Sensor, {
                    foreignKey: "location"
                });
            }
        }
    });

    return Location;
};


module.exports = function(sequelize, DataTypes) {
    var Sensor = sequelize.define("Sensor", {
        id: {
            type: DataTypes.INTEGER.UNSIGNED,
            primaryKey: true
        },
        name: DataTypes.STRING(255),
        type: {
            type: DataTypes.INTEGER.UNSIGNED,
            references: {
                model: "sensor_types",
                key: "id"
            }
        },
        location: {
            type: DataTypes.INTEGER.UNSIGNED,
            references: {
                model: "locations",
                key: "id"
            }
        }
    }, {
        classMethods: {
            associate: function(models) {
                Sensor.belongsTo(models.Location, {
                    foreignKey: "location"
                });

                Sensor.belongsTo(models.SensorType, { 
                    foreignKey: "type"
                });
            }
        }
    });

    return Sensor;
};
MikkoP
  • 4,864
  • 16
  • 58
  • 106
  • Is that actually the `SQL` you want? I don't think that's going to do what you think it will. In fact, I'm not sure that query will run without throwing an error. – dvlsg Jun 14 '16 at 18:52
  • @dvlsg I run it and it correctly returned all the rows and fields in the `locations` table and for each row the right number of associated entries in `sensors`. – MikkoP Jun 14 '16 at 18:53
  • Actually @dvlsg, it isn't right. I did some more testing (with more entries in `locations` table) and it turned out I had forgot a `GROUP BY` statement. I've edited the question. – MikkoP Jun 14 '16 at 18:58
  • Ah, okay. That makes more sense. I thought maybe MySQL was pulling some shenanigans I wasn't aware of (and I know they do that with implicit `GROUP` statements, so it wasn't entirely unreasonable). – dvlsg Jun 14 '16 at 19:05
  • https://stackoverflow.com/questions/52496842/sequelize-hasmany-associatedmodel-count-in-attributes-in-query-execution – Ansari Maksud Sep 25 '18 at 12:33

5 Answers5

78

Use findAll() with include() and sequelize.fn() for the COUNT:

Location.findAll({
    attributes: { 
        include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]] 
    },
    include: [{
        model: Sensor, attributes: []
    }]
});

Or, you may need to add a group as well:

Location.findAll({
    attributes: { 
        include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]] 
    },
    include: [{
        model: Sensor, attributes: []
    }],
    group: ['Location.id']
})
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
  • Thanks for your answer. This correctly counts the `sensourCount` field but it also includes fields from `sensors` table in the results. Also, although the SQL query it executes shows it includes all fields from the `locations` table, they aren't included in the result (the object it returns in the `then` clause). – MikkoP Jun 14 '16 at 17:17
  • @MikkoP okay, let me quickly build a sample and debug, thanks. – alecxe Jun 14 '16 at 17:18
  • @MikkoP could you please edit the question and post your model definitions? Thanks. – alecxe Jun 14 '16 at 17:39
  • I added the model definitions. Here's what your query outputs. http://pastebin.com/PwnctW1Y – MikkoP Jun 14 '16 at 17:43
  • @MikkoP thanks, please see the update. I think you should be able to achieve the desired result now. Thanks. – alecxe Jun 14 '16 at 18:34
  • Now it produces the correct SQL query, but the returned object is still lacking the fields from `locations`. Adding `raw: true` I see all the right fields. http://pastebin.com/arv2ip3D – MikkoP Jun 14 '16 at 18:55
  • Changing the `attributes` value to `attributes: { include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]] }` solved the problem. – MikkoP Jun 14 '16 at 19:05
  • @MikkoP great, fixed the answer to reflect your changes. Thanks. – alecxe Jun 14 '16 at 19:06
  • It still needs the `include` part, but great! Thanks for your help! – MikkoP Jun 14 '16 at 19:07
  • https://stackoverflow.com/questions/52496842/sequelize-hasmany-associatedmodel-count-in-attributes-in-query-execution – Ansari Maksud Sep 25 '18 at 12:33
  • 1
    @Shareef it will be fixed when you can add query option `subQuery:false` ex: `attributes:{},include:[{}],subQuery:false ` and you find more detail in https://github.com/sequelize/sequelize/issues/6073#issuecomment-322068888 – Bhavya Sanchaniya Dec 17 '18 at 11:45
10

For Counting associated entries with Sequelize

Location.findAll({
    attributes: { 
        include: [[Sequelize.fn('COUNT', Sequelize.col('sensors.location')), 'sensorCounts']] 
    }, // Sequelize.col() should contain a attribute which is referenced with parent table and whose rows needs to be counted
    include: [{
        model: Sensor, attributes: []
    }],
    group: ['sensors.location'] // groupBy is necessary else it will generate only 1 record with all rows count
})

Note :

Some how, this query generates a error like sensors.location is not exists in field list. This occur because of subQuery which is formed by above sequelize query.

So solution for this is to provide subQuery: false like example

Location.findAll({
        subQuery: false,
        attributes: { 
            include: [[Sequelize.fn('COUNT', Sequelize.col('sensors.location')), 'sensorCounts']] 
        },
        include: [{
            model: Sensor, attributes: []
        }],
        group: ['sensors.location']
    })

Note: **Sometime this could also generate a error bcz of mysql configuration which by default contains only-full-group-by in sqlMode, which needs to be removed for proper working.

The error will look like this..**

Error : Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'db.table.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

So to resolve this error follow this answer

SELECT list is not in GROUP BY clause and contains nonaggregated column .... incompatible with sql_mode=only_full_group_by

Now this will successfully generate all associated counts

Hope this will help you or somebody else!

Aman Kumar Gupta
  • 2,640
  • 20
  • 18
  • Cool, but what if you want to add a condition to the sensors table. For example, in the table there is a user_id field, how can we filter there? Is that possible? – Wisnu Oct 17 '21 at 10:23
  • 1
    Yes you can, pass the where attribute inside include 0th index object along with model and attributes – Aman Kumar Gupta Oct 20 '21 at 18:41
5
Location.findAll({
        attributes: { 
            include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]] 
        },
        include: [{
            model: Sensor, attributes: []
        }]
    });

and it works. but when i add "limit", i got error: sensors undefined

3

Example of HAVING, ORDER BY, INNER vs OUTER JOIN + several bugs/unintuitive behavior

I went into more detail at: Sequelize query with count in inner join but here's a quick summary list of points:

  • you must use row.get('count'), row.count does not work
  • you must parseInt on PostgreSQL
  • this code fails on PostgreSQL with column X must appear in the GROUP BY clause or be used in an aggregate function due to a sequelize bug

OUTER JOIN example which includes 0 counts by using required: false:

sqlite.js

const assert = require('assert');
const { DataTypes, Op, Sequelize } = require('sequelize');
const sequelize = new Sequelize('tmp', undefined, undefined, Object.assign({
  dialect: 'sqlite',
  storage: 'tmp.sqlite'
}));
;(async () => {
const User = sequelize.define('User', {
  name: { type: DataTypes.STRING },
}, {});
const Post = sequelize.define('Post', {
  body: { type: DataTypes.STRING },
}, {});
User.belongsToMany(Post, {through: 'UserLikesPost'});
Post.belongsToMany(User, {through: 'UserLikesPost'});
await sequelize.sync({force: true});
const user0 = await User.create({name: 'user0'})
const user1 = await User.create({name: 'user1'})
const user2 = await User.create({name: 'user2'})
const post0 = await Post.create({body: 'post0'})
const post1 = await Post.create({body: 'post1'})
const post2 = await Post.create({body: 'post2'})
// Set likes for each user.
await user0.addPosts([post0, post1])
await user1.addPosts([post0, post2])

let rows = await User.findAll({
  attributes: [
    'name',
    [sequelize.fn('COUNT', sequelize.col('Posts.id')), 'count'],
  ],
  include: [
    {
      model: Post,
      attributes: [],
      required: false,
      through: {attributes: []},
      where: { id: { [Op.ne]: post2.id }},
    },
  ],
  group: ['User.name'],
  order: [[sequelize.col('count'), 'DESC']],
  having: sequelize.where(sequelize.fn('COUNT', sequelize.col('Posts.id')), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows[1].name, 'user2')
assert.strictEqual(parseInt(rows[1].get('count'), 10), 0)
assert.strictEqual(rows.length, 2)
})().finally(() => { return sequelize.close() });

with:

package.json

{
  "name": "tmp",
  "private": true,
  "version": "1.0.0",
  "dependencies": {
    "pg": "8.5.1",
    "pg-hstore": "2.3.3",
    "sequelize": "6.5.1",
    "sqlite3": "5.0.2"
  }
}

and Node v14.17.0.

INNER JOIN version excluding 0 counts:

let rows = await User.findAll({
  attributes: [
    'name',
    [sequelize.fn('COUNT', '*'), 'count'],
  ],
  include: [
    {
      model: Post,
      attributes: [],
      through: {attributes: []},
      where: { id: { [Op.ne]: post2.id }},
    },
  ],
  group: ['User.name'],
  order: [[sequelize.col('count'), 'DESC']],
  having: sequelize.where(sequelize.fn('COUNT', '*'), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows.length, 1)
Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
0

How about defining a database view for it and then a model for that view? You can just get the relationship to the view included in your query whenever you need the number of sensors. The code may look cleaner this way, but I'm not aware if there will be performance costs. Somebody else may answer that...

CREATE OR REPLACE VIEW view_location_sensors_count AS
select "locations".id as "locationId", count("sensors".id) as "locationSensorsCount"
from locations
left outer join sensors on sensors."locationId" = location.id
group by location.id

When defining the model for the view you remove the id attribute and set the locationId as the primary key. Your model could look like this:

const { Model, DataTypes } = require('sequelize')

const attributes = {
    locationID: {
        type: DataTypes.UUIDV4, // Or whatever data type is your location ID
        primaryKey: true,
        unique: true
    },
    locationSensorsCount: DataTypes.INTEGER
}

const options = {
    paranoid: false,
    modelName: 'ViewLocationSensorsCount',
    tableName: 'view_location_sensors_count',
    timestamps: false
}


/**
 * This is only a database view. It is not an actual table, so 
 * DO NOT ATTEMPT insert, update or delete statements on this model
 */
class ViewLocationSensorsCount extends Model {
    static associate(models) {
        ViewLocationSensorsCount.removeAttribute('id')
        ViewLocationSensorsCount.belongsTo(models.Location, { as:'location', foreignKey: 'locationID' })
    }


    static init(sequelize) {
        this.sequelize = sequelize
        return super.init(attributes, {...options, sequelize})
    }
}

module.exports = ViewLocationSensorsCount

In the end, in your Location model you set a hasOne relationship to the Sensor model.

Alex J Gr
  • 83
  • 6