diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 053030076255bb13c958cfec9c84ccd9c730d0f4..b6b9a90a589e033d879ee474b38d71ebc40b1da0 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -100,34 +100,24 @@ module Ci
       where.not(duration: nil).sum(:duration)
     end
 
-    def stages_query
-      statuses.group('stage').select(:stage)
-        .order('max(stage_idx)')
+    def stages_count
+      statuses.select(:stage).distinct.count
     end
 
     def stages
-      self.stages_query.pluck(:stage)
-    end
-
-    def stages_with_statuses
       status_sql = statuses.latest.where('stage=sg.stage').status_sql
 
-      stages_with_statuses = CommitStatus.from(self.stages_query, :sg).
+      stages_query = statuses.group('stage').select(:stage)
+                       .order('max(stage_idx)')
+
+      stages_with_statuses = CommitStatus.from(stages_query, :sg).
         pluck('sg.stage', status_sql)
 
       stages_with_statuses.map do |stage|
-        OpenStruct.new(
-          name: stage.first,
-          status: stage.last,
-          pipeline: self
-        )
+        Ci::Stage.new(self, stage.first, status: stage.last)
       end
     end
 
-    def stages_with_latest_statuses
-      statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
-    end
-
     def artifacts
       builds.latest.with_artifacts_not_expired
     end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f1cac09c4e9a1c7c4a7e2948b33e815496c8867e
--- /dev/null
+++ b/app/models/ci/stage.rb
@@ -0,0 +1,23 @@
+module Ci
+  class Stage < ActiveRecord::Base
+    include ActiveModel::Model
+
+    attr_reader :pipeline, :name
+
+    def initialize(pipeline, name: name, status: status = nil)
+      @pipeline, @name, @status = pipeline, name, status
+    end
+
+    def status
+      @status ||= statuses.latest.status
+    end
+
+    def statuses
+      pipeline.statuses.where(stage: stage)
+    end
+
+    def builds
+      pipeline.builds.where(stage: stage)
+    end
+  end
+end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index d9021a38ce328f801c94c3be716cc8ddbe182127..2a537dc2a1321a59575a57d2499523f9606cf7bf 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -41,8 +41,8 @@ class CommitStatus < ActiveRecord::Base
       where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
   end
 
-  scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
-  scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
+  scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
+  scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
 
   state_machine :status do
     event :enqueue do
@@ -117,11 +117,6 @@ class CommitStatus < ActiveRecord::Base
     name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
   end
 
-  def self.stages
-    # We group by stage name, but order stages by theirs' index
-    unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').select('sg.stage')
-  end
-
   def failed_but_allowed?
     allow_failure? && (failed? || canceled?)
   end
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 697c8d19257cf79dd54bfb9e53617dc84248c376..56c1949ab2b93cc13542069a261a0315fd1deebe 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -133,7 +133,7 @@
                         %tr.success-message
                           %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"}
                             - build_count = @pipeline.statuses.latest.size
-                            - stage_count = @pipeline.stages.size
+                            - stage_count = @pipeline.stages_count
                             Pipeline
                             %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
                               = "\##{@pipeline.id}"
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index ae22d474f2ca32fefb182670617f8ee8731233b1..40e5e306426338f2df5718f7b0971340c32b3fe5 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -16,7 +16,7 @@ Commit Author: <%= commit.author_name %>
 <% end -%>
 
 <% build_count = @pipeline.statuses.latest.size -%>
-<% stage_count = @pipeline.stages.size -%>
+<% stage_count = @pipeline.stages_count -%>
 Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
 
 You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index d5004f6a066a8bcbda75bc9fe8fb031aa1cf1be9..a45612fb28b12928c60c434725918f0eec8d2e41 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -111,7 +111,7 @@
           %span.label.label-primary
             = tag
 
-    - if @build.pipeline.stages.many?
+    - if @build.pipeline.stages_count.many?
       .dropdown.build-dropdown
         .title Stage
         %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
deleted file mode 100644
index 3a3d750439f7f5e2a36ba2a73ff3fed8f755da40..0000000000000000000000000000000000000000
--- a/app/views/projects/commit/_ci_stage.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-%tr
-  %th{colspan: 10}
-    %strong
-      %a{name: stage}
-      - status = statuses.latest.status
-      %span{class: "ci-status-link ci-status-icon-#{status}"}
-        = ci_icon_for_status(status)
-      - if stage
-        &nbsp;
-        = stage.titleize
-  = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true
-  = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true
-%tr
-  %td{colspan: 10}
-    &nbsp;
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index cd9ed46d2c15cb7f6ca164c0aff930523f11db69..2cd40bb1106ccfab9618d6e5f276449fd02391fb 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -27,16 +27,15 @@
   .row-content-block.build-content.middle-block.pipeline-graph.hidden
     .pipeline-visualization
       %ul.stage-column-list
-        - stages = pipeline.stages_with_latest_statuses
-        - stages.each do |stage, statuses|
+        - pipeline.stages.each do |stage|
           %li.stage-column
             .stage-name
-              %a{name: stage}
-              - if stage
-                = stage.titleize
+              %a{name: stage.name}
+              - if stage.name
+                = stage.name.titleize
             .builds-container
               %ul
-                = render "projects/commit/pipeline_stage", statuses: statuses
+                = render "projects/commit/pipeline_stage", statuses: stage.statuses
 
 
 - if pipeline.yaml_errors.present?
@@ -62,5 +61,4 @@
         - if pipeline.project.build_coverage_enabled?
           %th Coverage
         %th
-    - pipeline.stages.each do |stage|
-      = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
+      = render pipeline.stages
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 3464e155a1bc2b8c5217cf32242a670e3af5f730..57e793d2e59fc9becfa411a46815640c57f88c97 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -15,13 +15,12 @@
     .build-content.middle-block.pipeline-graph
       .pipeline-visualization
         %ul.stage-column-list
-          - stages = pipeline.stages_with_latest_statuses
-          - stages.each do |stage, statuses|
+          - pipeline.stages.each do |stage|
             %li.stage-column
               .stage-name
-                %a{name: stage}
-                - if stage
-                  = stage.titleize
+                %a{name: stage.name}
+                - if stage.name
+                  = stage.name.titleize
               .builds-container
                 %ul
                   = render "projects/commit/pipeline_stage", statuses: statuses
@@ -50,5 +49,4 @@
             - if pipeline.project.build_coverage_enabled?
               %th Coverage
             %th
-        - pipeline.statuses.relevant.stages.each do |stage|
-          = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
+        = render pipeline.stages
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 8340ce516db570e115618cfd8579d901d088bf7f..e1e787dbde434306b6bc78ee63d2ad1fe8a426cf 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -37,7 +37,6 @@
           %span CI Lint
 
   %div.content-list.pipelines
-    - stages = @pipelines.stages
     - if @pipelines.blank?
       %div
         .nothing-here-block No pipelines to show
diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..717075620d97826750aba60052643e410a6bbcf1
--- /dev/null
+++ b/app/views/projects/stage/_stage.html.haml
@@ -0,0 +1,14 @@
+%tr
+  %th{colspan: 10}
+    %strong
+      %a{name: subject.name}
+      %span{class: "ci-status-link ci-status-icon-#{subject.status}"}
+        = ci_icon_for_status(subject.status)
+      - if subject.name
+        &nbsp;
+        = subject.name.titleize
+  = render subject.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true
+  = render subject.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true
+%tr
+  %td{colspan: 10}
+    &nbsp;
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index 06a783ebc1c2636a9ed63512342b601d4dae3210..e0284d55da8d9ff05a68f0fb496ea2b7e95b3c6c 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -22,7 +22,7 @@ module Gitlab
           sha: pipeline.sha,
           before_sha: pipeline.before_sha,
           status: pipeline.status,
-          stages: pipeline.stages,
+          stages: pipeline.stages.map(&:name),
           created_at: pipeline.created_at,
           finished_at: pipeline.finished_at,
           duration: pipeline.duration